diff --git a/.coveragerc b/.coveragerc index afdf2b3acb9..5b966b817a3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,14 +3,16 @@ source = homeassistant omit = homeassistant/__main__.py homeassistant/helpers/signal.py - homeassistant/helpers/typing.py - homeassistant/scripts/*.py + homeassistant/scripts/__init__.py + homeassistant/scripts/check_config.py + homeassistant/scripts/ensure_config.py + homeassistant/scripts/benchmark/__init__.py + homeassistant/scripts/macos/__init__.py # omit pieces of code that rely on external devices being present homeassistant/components/acer_projector/* homeassistant/components/acmeda/__init__.py homeassistant/components/acmeda/base.py - homeassistant/components/acmeda/const.py homeassistant/components/acmeda/cover.py homeassistant/components/acmeda/errors.py homeassistant/components/acmeda/helpers.py @@ -22,12 +24,10 @@ omit = homeassistant/components/adax/__init__.py homeassistant/components/adax/climate.py homeassistant/components/adguard/__init__.py - homeassistant/components/adguard/const.py homeassistant/components/adguard/entity.py homeassistant/components/adguard/sensor.py homeassistant/components/adguard/switch.py homeassistant/components/ads/* - homeassistant/components/advantage_air/diagnostics.py homeassistant/components/aemet/weather_update_coordinator.py homeassistant/components/aftership/* homeassistant/components/agent_dvr/alarm_control_panel.py @@ -43,7 +43,6 @@ omit = homeassistant/components/airthings_ble/sensor.py homeassistant/components/airtouch4/__init__.py homeassistant/components/airtouch4/climate.py - homeassistant/components/airtouch4/const.py homeassistant/components/airvisual/__init__.py homeassistant/components/airvisual/sensor.py homeassistant/components/airvisual_pro/__init__.py @@ -51,21 +50,16 @@ omit = homeassistant/components/alarmdecoder/__init__.py homeassistant/components/alarmdecoder/alarm_control_panel.py homeassistant/components/alarmdecoder/binary_sensor.py - homeassistant/components/alarmdecoder/const.py homeassistant/components/alarmdecoder/sensor.py homeassistant/components/alpha_vantage/sensor.py homeassistant/components/amazon_polly/* - homeassistant/components/amberelectric/__init__.py homeassistant/components/ambiclimate/climate.py 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/binary_sensor.py - homeassistant/components/android_ip_webcam/sensor.py homeassistant/components/android_ip_webcam/switch.py - homeassistant/components/androidtv/diagnostics.py homeassistant/components/anel_pwrctrl/switch.py homeassistant/components/anthemav/media_player.py homeassistant/components/apcupsd/__init__.py @@ -91,18 +85,14 @@ omit = homeassistant/components/aseko_pool_live/sensor.py homeassistant/components/asterisk_cdr/mailbox.py homeassistant/components/asterisk_mbox/* - homeassistant/components/asuswrt/diagnostics.py homeassistant/components/aten_pe/* homeassistant/components/atome/* homeassistant/components/aurora/__init__.py 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 - homeassistant/components/azure_devops/const.py homeassistant/components/azure_devops/sensor.py homeassistant/components/azure_service_bus/* homeassistant/components/baf/__init__.py @@ -124,7 +114,6 @@ omit = homeassistant/components/blink/alarm_control_panel.py homeassistant/components/blink/binary_sensor.py homeassistant/components/blink/camera.py - homeassistant/components/blink/const.py homeassistant/components/blink/sensor.py homeassistant/components/blinksticklight/light.py homeassistant/components/blockchain/sensor.py @@ -135,26 +124,19 @@ omit = homeassistant/components/bmw_connected_drive/binary_sensor.py homeassistant/components/bmw_connected_drive/button.py homeassistant/components/bmw_connected_drive/coordinator.py - homeassistant/components/bmw_connected_drive/device_tracker.py homeassistant/components/bmw_connected_drive/lock.py homeassistant/components/bmw_connected_drive/notify.py homeassistant/components/bmw_connected_drive/sensor.py homeassistant/components/bosch_shc/__init__.py homeassistant/components/bosch_shc/binary_sensor.py - homeassistant/components/bosch_shc/const.py homeassistant/components/bosch_shc/cover.py homeassistant/components/bosch_shc/entity.py homeassistant/components/bosch_shc/sensor.py homeassistant/components/bosch_shc/switch.py - homeassistant/components/braviatv/__init__.py homeassistant/components/braviatv/button.py - homeassistant/components/braviatv/const.py homeassistant/components/braviatv/coordinator.py - homeassistant/components/braviatv/entity.py homeassistant/components/braviatv/media_player.py homeassistant/components/braviatv/remote.py - homeassistant/components/broadlink/__init__.py - homeassistant/components/broadlink/const.py homeassistant/components/broadlink/light.py homeassistant/components/broadlink/remote.py homeassistant/components/broadlink/switch.py @@ -162,17 +144,13 @@ omit = homeassistant/components/brottsplatskartan/sensor.py homeassistant/components/browser/* homeassistant/components/brunt/__init__.py - homeassistant/components/brunt/const.py homeassistant/components/brunt/cover.py homeassistant/components/bsblan/climate.py - homeassistant/components/bsblan/const.py - homeassistant/components/bsblan/entity.py homeassistant/components/bt_home_hub_5/device_tracker.py homeassistant/components/bt_smarthub/device_tracker.py homeassistant/components/buienradar/sensor.py homeassistant/components/buienradar/util.py homeassistant/components/buienradar/weather.py - homeassistant/components/caldav/calendar.py homeassistant/components/canary/camera.py homeassistant/components/cert_expiry/helper.py homeassistant/components/channels/* @@ -192,15 +170,10 @@ omit = homeassistant/components/concord232/alarm_control_panel.py homeassistant/components/concord232/binary_sensor.py homeassistant/components/control4/__init__.py - homeassistant/components/control4/const.py homeassistant/components/control4/director_utils.py homeassistant/components/control4/light.py - homeassistant/components/coolmaster/__init__.py - homeassistant/components/coolmaster/climate.py - homeassistant/components/coolmaster/const.py homeassistant/components/cppm_tracker/device_tracker.py homeassistant/components/crownstone/__init__.py - homeassistant/components/crownstone/const.py homeassistant/components/crownstone/devices.py homeassistant/components/crownstone/entry_manager.py homeassistant/components/crownstone/helpers.py @@ -213,7 +186,6 @@ omit = 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 homeassistant/components/decora/light.py homeassistant/components/decora_wifi/light.py @@ -233,6 +205,9 @@ omit = homeassistant/components/discord/notify.py homeassistant/components/dlib_face_detect/image_processing.py homeassistant/components/dlib_face_identify/image_processing.py + homeassistant/components/dlink/__init__.py + homeassistant/components/dlink/data.py + homeassistant/components/dlink/entity.py homeassistant/components/dlink/switch.py homeassistant/components/dominos/* homeassistant/components/doods/* @@ -240,7 +215,6 @@ omit = 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/* @@ -250,7 +224,6 @@ omit = homeassistant/components/dte_energy_bridge/sensor.py homeassistant/components/dublin_bus_transport/sensor.py homeassistant/components/dunehd/__init__.py - homeassistant/components/dunehd/const.py homeassistant/components/dunehd/media_player.py homeassistant/components/dwd_weather_warnings/sensor.py homeassistant/components/dweet/* @@ -260,20 +233,17 @@ omit = homeassistant/components/ecobee/__init__.py homeassistant/components/ecobee/binary_sensor.py homeassistant/components/ecobee/climate.py - homeassistant/components/ecobee/humidifier.py homeassistant/components/ecobee/notify.py homeassistant/components/ecobee/sensor.py homeassistant/components/ecobee/weather.py homeassistant/components/econet/__init__.py homeassistant/components/econet/binary_sensor.py homeassistant/components/econet/climate.py - homeassistant/components/econet/const.py homeassistant/components/econet/sensor.py homeassistant/components/econet/water_heater.py homeassistant/components/ecovacs/* homeassistant/components/ecowitt/__init__.py homeassistant/components/ecowitt/binary_sensor.py - homeassistant/components/ecowitt/diagnostics.py homeassistant/components/ecowitt/entity.py homeassistant/components/ecowitt/sensor.py homeassistant/components/eddystone_temperature/sensor.py @@ -288,16 +258,13 @@ omit = homeassistant/components/elkm1/alarm_control_panel.py homeassistant/components/elkm1/binary_sensor.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/alarm_control_panel.py homeassistant/components/elmax/binary_sensor.py homeassistant/components/elmax/common.py - homeassistant/components/elmax/const.py homeassistant/components/elmax/binary_sensor.py homeassistant/components/elmax/switch.py homeassistant/components/elv/* @@ -309,7 +276,6 @@ omit = homeassistant/components/enigma2/media_player.py homeassistant/components/enocean/__init__.py homeassistant/components/enocean/binary_sensor.py - homeassistant/components/enocean/const.py homeassistant/components/enocean/device.py homeassistant/components/enocean/dongle.py homeassistant/components/enocean/light.py @@ -325,7 +291,6 @@ omit = homeassistant/components/envisalink/* homeassistant/components/ephember/climate.py homeassistant/components/epson/__init__.py - homeassistant/components/epson/const.py homeassistant/components/epson/media_player.py homeassistant/components/epsonworkforce/sensor.py homeassistant/components/eq3btsmart/climate.py @@ -351,12 +316,13 @@ omit = homeassistant/components/esphome/switch.py homeassistant/components/etherscan/sensor.py homeassistant/components/eufy/* + homeassistant/components/eufylife_ble/__init__.py + homeassistant/components/eufylife_ble/sensor.py homeassistant/components/everlights/light.py homeassistant/components/evohome/* homeassistant/components/ezviz/__init__.py homeassistant/components/ezviz/binary_sensor.py homeassistant/components/ezviz/camera.py - homeassistant/components/ezviz/const.py homeassistant/components/ezviz/coordinator.py homeassistant/components/ezviz/entity.py homeassistant/components/ezviz/sensor.py @@ -372,20 +338,16 @@ omit = homeassistant/components/fibaro/cover.py homeassistant/components/fibaro/light.py homeassistant/components/fibaro/lock.py - homeassistant/components/fibaro/scene.py homeassistant/components/fibaro/sensor.py homeassistant/components/fibaro/switch.py - homeassistant/components/filesize/sensor.py homeassistant/components/fints/sensor.py homeassistant/components/fireservicerota/__init__.py homeassistant/components/fireservicerota/binary_sensor.py - homeassistant/components/fireservicerota/const.py homeassistant/components/fireservicerota/sensor.py homeassistant/components/fireservicerota/switch.py homeassistant/components/firmata/__init__.py homeassistant/components/firmata/binary_sensor.py homeassistant/components/firmata/board.py - homeassistant/components/firmata/const.py homeassistant/components/firmata/entity.py homeassistant/components/firmata/light.py homeassistant/components/firmata/pin.py @@ -398,7 +360,6 @@ omit = homeassistant/components/fixer/sensor.py homeassistant/components/fjaraskupan/__init__.py homeassistant/components/fjaraskupan/binary_sensor.py - homeassistant/components/fjaraskupan/const.py homeassistant/components/fjaraskupan/fan.py homeassistant/components/fjaraskupan/light.py homeassistant/components/fjaraskupan/number.py @@ -407,7 +368,6 @@ omit = homeassistant/components/flexit/climate.py homeassistant/components/flic/binary_sensor.py homeassistant/components/flick_electric/__init__.py - homeassistant/components/flick_electric/const.py homeassistant/components/flick_electric/sensor.py homeassistant/components/flock/notify.py homeassistant/components/flume/__init__.py @@ -416,8 +376,7 @@ omit = homeassistant/components/flume/entity.py homeassistant/components/flume/sensor.py homeassistant/components/flume/util.py - homeassistant/components/folder/sensor.py - homeassistant/components/folder_watcher/* + homeassistant/components/folder_watcher/__init__.py homeassistant/components/foobot/sensor.py homeassistant/components/fortios/device_tracker.py homeassistant/components/foscam/__init__.py @@ -425,18 +384,14 @@ omit = homeassistant/components/foursquare/* homeassistant/components/free_mobile/notify.py homeassistant/components/freebox/device_tracker.py - homeassistant/components/freebox/router.py homeassistant/components/freebox/sensor.py homeassistant/components/freebox/switch.py - homeassistant/components/fritz/binary_sensor.py homeassistant/components/fritz/common.py - homeassistant/components/fritz/const.py homeassistant/components/fritz/device_tracker.py homeassistant/components/fritz/services.py homeassistant/components/fritz/switch.py homeassistant/components/fritzbox_callmonitor/__init__.py homeassistant/components/fritzbox_callmonitor/base.py - homeassistant/components/fritzbox_callmonitor/const.py homeassistant/components/fritzbox_callmonitor/sensor.py homeassistant/components/frontier_silicon/const.py homeassistant/components/frontier_silicon/media_player.py @@ -448,21 +403,16 @@ omit = homeassistant/components/gc100/* homeassistant/components/geniushub/* homeassistant/components/geocaching/__init__.py - homeassistant/components/geocaching/const.py homeassistant/components/geocaching/coordinator.py homeassistant/components/geocaching/oauth.py homeassistant/components/geocaching/sensor.py - homeassistant/components/github/__init__.py homeassistant/components/github/coordinator.py - homeassistant/components/github/sensor.py homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py - homeassistant/components/glances/const.py homeassistant/components/glances/sensor.py homeassistant/components/goalfeed/* homeassistant/components/goodwe/__init__.py homeassistant/components/goodwe/button.py - homeassistant/components/goodwe/const.py homeassistant/components/goodwe/number.py homeassistant/components/goodwe/select.py homeassistant/components/goodwe/sensor.py @@ -471,9 +421,7 @@ omit = homeassistant/components/google_pubsub/__init__.py homeassistant/components/gpsd/sensor.py homeassistant/components/greenwave/light.py - homeassistant/components/group/notify.py homeassistant/components/growatt_server/__init__.py - homeassistant/components/growatt_server/const.py homeassistant/components/growatt_server/sensor.py homeassistant/components/growatt_server/sensor_types/* homeassistant/components/gstreamer/media_player.py @@ -485,10 +433,8 @@ omit = homeassistant/components/guardian/switch.py homeassistant/components/guardian/util.py homeassistant/components/habitica/__init__.py - homeassistant/components/habitica/const.py homeassistant/components/habitica/sensor.py homeassistant/components/harman_kardon_avr/media_player.py - homeassistant/components/harmony/const.py homeassistant/components/harmony/data.py homeassistant/components/harmony/remote.py homeassistant/components/harmony/util.py @@ -518,7 +464,16 @@ omit = homeassistant/components/home_connect/switch.py homeassistant/components/home_plus_control/api.py homeassistant/components/home_plus_control/switch.py - homeassistant/components/homematic/* + homeassistant/components/homematic/__init__.py + homeassistant/components/homematic/binary_sensor.py + homeassistant/components/homematic/climate.py + homeassistant/components/homematic/cover.py + homeassistant/components/homematic/entity.py + homeassistant/components/homematic/light.py + homeassistant/components/homematic/lock.py + homeassistant/components/homematic/notify.py + homeassistant/components/homematic/sensor.py + homeassistant/components/homematic/switch.py homeassistant/components/homeworks/* homeassistant/components/honeywell/__init__.py homeassistant/components/honeywell/climate.py @@ -530,15 +485,11 @@ omit = 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/button.py homeassistant/components/hunterdouglas_powerview/coordinator.py homeassistant/components/hunterdouglas_powerview/cover.py - homeassistant/components/hunterdouglas_powerview/diagnostics.py homeassistant/components/hunterdouglas_powerview/entity.py - homeassistant/components/hunterdouglas_powerview/model.py - homeassistant/components/hunterdouglas_powerview/scene.py homeassistant/components/hunterdouglas_powerview/select.py homeassistant/components/hunterdouglas_powerview/sensor.py homeassistant/components/hunterdouglas_powerview/shade_data.py @@ -561,19 +512,18 @@ omit = homeassistant/components/idteck_prox/* 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/__init__.py + homeassistant/components/imap/coordinator.py homeassistant/components/imap/sensor.py homeassistant/components/imap_email_content/sensor.py homeassistant/components/incomfort/* homeassistant/components/insteon/binary_sensor.py homeassistant/components/insteon/climate.py - homeassistant/components/insteon/const.py homeassistant/components/insteon/cover.py homeassistant/components/insteon/fan.py homeassistant/components/insteon/insteon_entity.py - homeassistant/components/insteon/ipdb.py homeassistant/components/insteon/light.py homeassistant/components/insteon/schemas.py homeassistant/components/insteon/switch.py @@ -584,6 +534,7 @@ omit = homeassistant/components/intellifire/coordinator.py homeassistant/components/intellifire/entity.py homeassistant/components/intellifire/fan.py + homeassistant/components/intellifire/light.py homeassistant/components/intellifire/number.py homeassistant/components/intellifire/sensor.py homeassistant/components/intellifire/switch.py @@ -599,6 +550,7 @@ omit = homeassistant/components/iss/sensor.py homeassistant/components/isy994/__init__.py homeassistant/components/isy994/binary_sensor.py + homeassistant/components/isy994/button.py homeassistant/components/isy994/climate.py homeassistant/components/isy994/cover.py homeassistant/components/isy994/entity.py @@ -606,6 +558,9 @@ omit = homeassistant/components/isy994/helpers.py homeassistant/components/isy994/light.py homeassistant/components/isy994/lock.py + homeassistant/components/isy994/models.py + homeassistant/components/isy994/number.py + homeassistant/components/isy994/select.py homeassistant/components/isy994/sensor.py homeassistant/components/isy994/services.py homeassistant/components/isy994/switch.py @@ -615,16 +570,13 @@ omit = homeassistant/components/izone/__init__.py homeassistant/components/izone/climate.py homeassistant/components/izone/discovery.py - homeassistant/components/jellyfin/media_source.py homeassistant/components/joaoapps_join/* homeassistant/components/juicenet/__init__.py - homeassistant/components/juicenet/const.py homeassistant/components/juicenet/device.py homeassistant/components/juicenet/entity.py homeassistant/components/juicenet/number.py homeassistant/components/juicenet/sensor.py homeassistant/components/juicenet/switch.py - homeassistant/components/justnimbus/const.py homeassistant/components/justnimbus/coordinator.py homeassistant/components/justnimbus/entity.py homeassistant/components/justnimbus/sensor.py @@ -633,30 +585,24 @@ omit = homeassistant/components/keba/* homeassistant/components/keenetic_ndms2/__init__.py homeassistant/components/keenetic_ndms2/binary_sensor.py - homeassistant/components/keenetic_ndms2/const.py homeassistant/components/keenetic_ndms2/device_tracker.py homeassistant/components/keenetic_ndms2/router.py homeassistant/components/kef/* homeassistant/components/keyboard/* homeassistant/components/keyboard_remote/* homeassistant/components/keymitt_ble/__init__.py - homeassistant/components/keymitt_ble/const.py homeassistant/components/keymitt_ble/entity.py homeassistant/components/keymitt_ble/switch.py homeassistant/components/keymitt_ble/coordinator.py - homeassistant/components/kira/* homeassistant/components/kiwi/lock.py homeassistant/components/kodi/__init__.py homeassistant/components/kodi/browse_media.py - homeassistant/components/kodi/const.py homeassistant/components/kodi/media_player.py homeassistant/components/kodi/notify.py 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 homeassistant/components/kostal_plenticore/select.py homeassistant/components/kostal_plenticore/sensor.py @@ -666,13 +612,14 @@ omit = homeassistant/components/lannouncer/notify.py homeassistant/components/lastfm/sensor.py homeassistant/components/launch_library/__init__.py - homeassistant/components/launch_library/const.py - homeassistant/components/launch_library/diagnostics.py homeassistant/components/launch_library/sensor.py homeassistant/components/lcn/climate.py homeassistant/components/lcn/helpers.py - homeassistant/components/lcn/scene.py homeassistant/components/lcn/services.py + homeassistant/components/ld2410_ble/__init__.py + homeassistant/components/ld2410_ble/binary_sensor.py + homeassistant/components/ld2410_ble/coordinator.py + homeassistant/components/ld2410_ble/sensor.py homeassistant/components/led_ble/__init__.py homeassistant/components/led_ble/light.py homeassistant/components/lg_netcast/media_player.py @@ -682,10 +629,8 @@ omit = homeassistant/components/lidarr/coordinator.py homeassistant/components/lidarr/sensor.py homeassistant/components/life360/__init__.py - homeassistant/components/life360/const.py homeassistant/components/life360/coordinator.py homeassistant/components/life360/device_tracker.py - homeassistant/components/lifx_cloud/scene.py homeassistant/components/lightwave/* homeassistant/components/limitlessled/light.py homeassistant/components/linksys_smart/device_tracker.py @@ -695,7 +640,6 @@ omit = homeassistant/components/llamalab_automate/notify.py homeassistant/components/logi_circle/__init__.py homeassistant/components/logi_circle/camera.py - homeassistant/components/logi_circle/const.py homeassistant/components/logi_circle/sensor.py homeassistant/components/london_underground/sensor.py homeassistant/components/lookin/__init__.py @@ -704,20 +648,21 @@ omit = homeassistant/components/lookin/entity.py homeassistant/components/lookin/light.py homeassistant/components/lookin/media_player.py - homeassistant/components/lookin/models.py homeassistant/components/lookin/sensor.py homeassistant/components/luci/device_tracker.py homeassistant/components/luftdaten/sensor.py homeassistant/components/lupusec/* - homeassistant/components/lutron/* + homeassistant/components/lutron/__init__.py + homeassistant/components/lutron/binary_sensor.py + homeassistant/components/lutron/cover.py + homeassistant/components/lutron/light.py + homeassistant/components/lutron/switch.py homeassistant/components/lutron_caseta/__init__.py homeassistant/components/lutron_caseta/binary_sensor.py homeassistant/components/lutron_caseta/cover.py homeassistant/components/lutron_caseta/fan.py homeassistant/components/lutron_caseta/light.py - homeassistant/components/lutron_caseta/scene.py homeassistant/components/lutron_caseta/switch.py - homeassistant/components/lutron_caseta/util.py homeassistant/components/lw12wifi/light.py homeassistant/components/lyric/__init__.py homeassistant/components/lyric/api.py @@ -730,29 +675,23 @@ omit = homeassistant/components/matrix/* homeassistant/components/matter/__init__.py homeassistant/components/meater/__init__.py - homeassistant/components/meater/const.py homeassistant/components/meater/sensor.py homeassistant/components/media_extractor/* homeassistant/components/mediaroom/media_player.py homeassistant/components/melcloud/__init__.py homeassistant/components/melcloud/climate.py - homeassistant/components/melcloud/const.py homeassistant/components/melcloud/sensor.py homeassistant/components/melcloud/water_heater.py homeassistant/components/melnor/__init__.py - homeassistant/components/melnor/const.py - homeassistant/components/melnor/models.py homeassistant/components/message_bird/notify.py homeassistant/components/met/weather.py homeassistant/components/met_eireann/__init__.py homeassistant/components/met_eireann/weather.py homeassistant/components/meteo_france/__init__.py - homeassistant/components/meteo_france/const.py homeassistant/components/meteo_france/sensor.py homeassistant/components/meteo_france/weather.py homeassistant/components/meteoalarm/* homeassistant/components/meteoclimatic/__init__.py - homeassistant/components/meteoclimatic/const.py homeassistant/components/meteoclimatic/sensor.py homeassistant/components/meteoclimatic/weather.py homeassistant/components/metoffice/sensor.py @@ -761,27 +700,22 @@ omit = homeassistant/components/miflora/sensor.py homeassistant/components/mikrotik/hub.py homeassistant/components/mill/climate.py - homeassistant/components/mill/const.py homeassistant/components/mill/sensor.py homeassistant/components/minecraft_server/__init__.py - homeassistant/components/minecraft_server/binary_sensor.py - homeassistant/components/minecraft_server/const.py - homeassistant/components/minecraft_server/helpers.py - homeassistant/components/minecraft_server/sensor.py - homeassistant/components/minio/* + homeassistant/components/minio/minio_helper.py homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mjpeg/camera.py homeassistant/components/mjpeg/util.py - homeassistant/components/mochad/* + homeassistant/components/mochad/__init__.py + homeassistant/components/mochad/light.py + homeassistant/components/mochad/switch.py homeassistant/components/modem_callerid/button.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/moehlenhoff_alpha2/__init__.py homeassistant/components/moehlenhoff_alpha2/binary_sensor.py homeassistant/components/moehlenhoff_alpha2/climate.py - homeassistant/components/moehlenhoff_alpha2/const.py homeassistant/components/moehlenhoff_alpha2/sensor.py homeassistant/components/motion_blinds/__init__.py - homeassistant/components/motion_blinds/const.py homeassistant/components/motion_blinds/cover.py homeassistant/components/motion_blinds/sensor.py homeassistant/components/mpd/media_player.py @@ -799,7 +733,6 @@ omit = homeassistant/components/mysensors/__init__.py homeassistant/components/mysensors/climate.py homeassistant/components/mysensors/cover.py - homeassistant/components/mysensors/device.py homeassistant/components/mysensors/gateway.py homeassistant/components/mysensors/handler.py homeassistant/components/mysensors/helpers.py @@ -811,8 +744,6 @@ 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 homeassistant/components/neato/__init__.py @@ -823,7 +754,6 @@ omit = homeassistant/components/neato/switch.py homeassistant/components/neato/vacuum.py homeassistant/components/nederlandse_spoorwegen/sensor.py - homeassistant/components/nest/const.py homeassistant/components/nest/legacy/* homeassistant/components/netdata/sensor.py homeassistant/components/netgear/__init__.py @@ -845,7 +775,6 @@ omit = homeassistant/components/nibe_heatpump/__init__.py homeassistant/components/nibe_heatpump/climate.py homeassistant/components/nibe_heatpump/binary_sensor.py - homeassistant/components/nibe_heatpump/button.py homeassistant/components/nibe_heatpump/number.py homeassistant/components/nibe_heatpump/select.py homeassistant/components/nibe_heatpump/sensor.py @@ -868,11 +797,9 @@ omit = homeassistant/components/nsw_fuel_station/sensor.py homeassistant/components/nuki/__init__.py homeassistant/components/nuki/binary_sensor.py - homeassistant/components/nuki/const.py homeassistant/components/nuki/lock.py - homeassistant/components/nut/diagnostics.py + homeassistant/components/nuki/sensor.py homeassistant/components/nx584/alarm_control_panel.py - homeassistant/components/nzbget/coordinator.py homeassistant/components/oasa_telematics/sensor.py homeassistant/components/obihai/* homeassistant/components/octoprint/__init__.py @@ -885,19 +812,15 @@ omit = homeassistant/components/omnilogic/switch.py homeassistant/components/ondilo_ico/__init__.py homeassistant/components/ondilo_ico/api.py - homeassistant/components/ondilo_ico/const.py - homeassistant/components/ondilo_ico/oauth_impl.py homeassistant/components/ondilo_ico/sensor.py homeassistant/components/onkyo/media_player.py homeassistant/components/onvif/__init__.py - homeassistant/components/onvif/base.py homeassistant/components/onvif/binary_sensor.py homeassistant/components/onvif/camera.py homeassistant/components/onvif/device.py homeassistant/components/onvif/event.py homeassistant/components/onvif/parsers.py homeassistant/components/onvif/sensor.py - homeassistant/components/open_meteo/diagnostics.py homeassistant/components/open_meteo/weather.py homeassistant/components/opencv/* homeassistant/components/openevse/sensor.py @@ -923,9 +846,9 @@ omit = homeassistant/components/openuv/coordinator.py homeassistant/components/openuv/sensor.py homeassistant/components/openweathermap/sensor.py - homeassistant/components/openweathermap/weather.py homeassistant/components/openweathermap/weather_update_coordinator.py - homeassistant/components/opnsense/* + homeassistant/components/opnsense/__init__.py + homeassistant/components/opnsense/device_tracker.py homeassistant/components/opple/light.py homeassistant/components/oru/* homeassistant/components/orvibo/switch.py @@ -940,13 +863,11 @@ omit = homeassistant/components/overkiz/coordinator.py homeassistant/components/overkiz/cover.py homeassistant/components/overkiz/cover_entities/* - homeassistant/components/overkiz/diagnostics.py homeassistant/components/overkiz/entity.py homeassistant/components/overkiz/executor.py homeassistant/components/overkiz/light.py homeassistant/components/overkiz/lock.py homeassistant/components/overkiz/number.py - homeassistant/components/overkiz/scene.py homeassistant/components/overkiz/select.py homeassistant/components/overkiz/sensor.py homeassistant/components/overkiz/siren.py @@ -954,31 +875,30 @@ omit = homeassistant/components/overkiz/water_heater.py homeassistant/components/overkiz/water_heater_entities/* homeassistant/components/ovo_energy/__init__.py - homeassistant/components/ovo_energy/const.py homeassistant/components/ovo_energy/sensor.py homeassistant/components/panasonic_bluray/media_player.py homeassistant/components/panasonic_viera/media_player.py homeassistant/components/pandora/media_player.py homeassistant/components/pencom/switch.py homeassistant/components/philips_js/__init__.py - homeassistant/components/philips_js/diagnostics.py - homeassistant/components/philips_js/helpers.py homeassistant/components/philips_js/light.py homeassistant/components/philips_js/media_player.py homeassistant/components/philips_js/remote.py homeassistant/components/philips_js/switch.py homeassistant/components/pi_hole/sensor.py homeassistant/components/picotts/tts.py - homeassistant/components/pilight/* + homeassistant/components/pilight/base_class.py + homeassistant/components/pilight/binary_sensor.py + homeassistant/components/pilight/const.py + homeassistant/components/pilight/light.py + homeassistant/components/pilight/switch.py homeassistant/components/ping/__init__.py homeassistant/components/ping/binary_sensor.py - homeassistant/components/ping/const.py homeassistant/components/ping/device_tracker.py homeassistant/components/pioneer/media_player.py homeassistant/components/pjlink/media_player.py homeassistant/components/plaato/__init__.py homeassistant/components/plaato/binary_sensor.py - homeassistant/components/plaato/const.py homeassistant/components/plaato/entity.py homeassistant/components/plaato/sensor.py homeassistant/components/plex/cast.py @@ -1002,9 +922,7 @@ omit = homeassistant/components/proxmoxve/* homeassistant/components/proxy/camera.py homeassistant/components/pulseaudio_loopback/switch.py - homeassistant/components/purpleair/__init__.py homeassistant/components/purpleair/coordinator.py - homeassistant/components/purpleair/sensor.py homeassistant/components/pushbullet/api.py homeassistant/components/pushbullet/notify.py homeassistant/components/pushbullet/sensor.py @@ -1023,7 +941,6 @@ omit = homeassistant/components/rachio/switch.py homeassistant/components/rachio/webhooks.py homeassistant/components/radio_browser/__init__.py - homeassistant/components/radio_browser/media_source.py homeassistant/components/radiotherm/__init__.py homeassistant/components/radiotherm/climate.py homeassistant/components/radiotherm/coordinator.py @@ -1035,33 +952,30 @@ omit = homeassistant/components/rainmachine/__init__.py homeassistant/components/rainmachine/binary_sensor.py homeassistant/components/rainmachine/button.py - homeassistant/components/rainmachine/model.py homeassistant/components/rainmachine/select.py homeassistant/components/rainmachine/sensor.py homeassistant/components/rainmachine/switch.py homeassistant/components/rainmachine/update.py homeassistant/components/rainmachine/util.py homeassistant/components/raspyrfm/* - homeassistant/components/recollect_waste/__init__.py homeassistant/components/recollect_waste/sensor.py homeassistant/components/recorder/repack.py homeassistant/components/recswitch/switch.py - homeassistant/components/reddit/* + homeassistant/components/reddit/sensor.py homeassistant/components/rejseplanen/sensor.py homeassistant/components/remember_the_milk/__init__.py homeassistant/components/remote_rpi_gpio/* homeassistant/components/reolink/__init__.py + homeassistant/components/reolink/binary_sensor.py homeassistant/components/reolink/camera.py - homeassistant/components/reolink/const.py homeassistant/components/reolink/entity.py homeassistant/components/reolink/host.py homeassistant/components/repetier/__init__.py homeassistant/components/repetier/sensor.py homeassistant/components/rest/notify.py homeassistant/components/rest/switch.py - homeassistant/components/rfxtrx/diagnostics.py homeassistant/components/ridwell/__init__.py - homeassistant/components/ridwell/sensor.py + homeassistant/components/ridwell/coordinator.py homeassistant/components/ridwell/switch.py homeassistant/components/ring/camera.py homeassistant/components/ripple/sensor.py @@ -1074,7 +988,6 @@ omit = homeassistant/components/roomba/sensor.py homeassistant/components/roomba/vacuum.py homeassistant/components/roon/__init__.py - homeassistant/components/roon/const.py homeassistant/components/roon/media_browser.py homeassistant/components/roon/media_player.py homeassistant/components/roon/server.py @@ -1082,8 +995,13 @@ omit = homeassistant/components/rova/sensor.py homeassistant/components/rpi_camera/* homeassistant/components/rtorrent/sensor.py + homeassistant/components/ruuvi_gateway/__init__.py + homeassistant/components/ruuvi_gateway/bluetooth.py + homeassistant/components/ruuvi_gateway/coordinator.py homeassistant/components/russound_rio/media_player.py homeassistant/components/russound_rnet/media_player.py + homeassistant/components/rympro/__init__.py + homeassistant/components/rympro/sensor.py homeassistant/components/sabnzbd/__init__.py homeassistant/components/sabnzbd/sensor.py homeassistant/components/saj/sensor.py @@ -1092,7 +1010,6 @@ omit = homeassistant/components/screenlogic/__init__.py homeassistant/components/screenlogic/binary_sensor.py homeassistant/components/screenlogic/climate.py - homeassistant/components/screenlogic/diagnostics.py homeassistant/components/screenlogic/light.py homeassistant/components/screenlogic/number.py homeassistant/components/screenlogic/sensor.py @@ -1104,7 +1021,6 @@ omit = homeassistant/components/sense/binary_sensor.py homeassistant/components/sense/sensor.py homeassistant/components/senseme/__init__.py - homeassistant/components/senseme/binary_sensor.py homeassistant/components/senseme/discovery.py homeassistant/components/senseme/entity.py homeassistant/components/senseme/fan.py @@ -1123,11 +1039,9 @@ omit = homeassistant/components/sia/__init__.py homeassistant/components/sia/alarm_control_panel.py homeassistant/components/sia/binary_sensor.py - homeassistant/components/sia/const.py homeassistant/components/sia/hub.py homeassistant/components/sia/sia_entity_base.py homeassistant/components/sia/utils.py - homeassistant/components/sigfox/sensor.py homeassistant/components/simplepush/__init__.py homeassistant/components/simplepush/notify.py homeassistant/components/simplisafe/__init__.py @@ -1136,7 +1050,6 @@ omit = homeassistant/components/simplisafe/button.py homeassistant/components/simplisafe/lock.py homeassistant/components/simplisafe/sensor.py - homeassistant/components/simulated/sensor.py homeassistant/components/sinch/* homeassistant/components/sisyphus/* homeassistant/components/sky_hub/* @@ -1170,11 +1083,12 @@ omit = homeassistant/components/sms/sensor.py homeassistant/components/smtp/notify.py homeassistant/components/snapcast/* - homeassistant/components/snmp/* + homeassistant/components/snmp/device_tracker.py + homeassistant/components/snmp/sensor.py + homeassistant/components/snmp/switch.py homeassistant/components/snooz/__init__.py homeassistant/components/solaredge/__init__.py homeassistant/components/solaredge/coordinator.py - homeassistant/components/solaredge/sensor.py homeassistant/components/solaredge_local/sensor.py homeassistant/components/solarlog/__init__.py homeassistant/components/solarlog/sensor.py @@ -1188,7 +1102,6 @@ omit = homeassistant/components/somfy_mylink/cover.py homeassistant/components/sonos/__init__.py homeassistant/components/sonos/alarms.py - homeassistant/components/sonos/diagnostics.py homeassistant/components/sonos/entity.py homeassistant/components/sonos/favorites.py homeassistant/components/sonos/helpers.py @@ -1199,7 +1112,9 @@ omit = homeassistant/components/sonos/speaker.py homeassistant/components/sonos/switch.py homeassistant/components/sony_projector/switch.py - homeassistant/components/spc/* + homeassistant/components/spc/__init__.py + homeassistant/components/spc/alarm_control_panel.py + homeassistant/components/spc/binary_sensor.py homeassistant/components/spider/__init__.py homeassistant/components/spider/climate.py homeassistant/components/spider/sensor.py @@ -1213,6 +1128,7 @@ omit = homeassistant/components/squeezebox/__init__.py homeassistant/components/squeezebox/browse_media.py homeassistant/components/squeezebox/media_player.py + homeassistant/components/starlink/coordinator.py homeassistant/components/starline/__init__.py homeassistant/components/starline/account.py homeassistant/components/starline/binary_sensor.py @@ -1226,8 +1142,14 @@ omit = homeassistant/components/stiebel_eltron/* homeassistant/components/stookalert/__init__.py homeassistant/components/stookalert/binary_sensor.py - homeassistant/components/stookalert/diagnostics.py - homeassistant/components/stream/* + homeassistant/components/stookwijzer/__init__.py + homeassistant/components/stookwijzer/sensor.py + homeassistant/components/stream/__init__.py + homeassistant/components/stream/core.py + homeassistant/components/stream/fmp4utils.py + homeassistant/components/stream/hls.py + homeassistant/components/stream/recorder.py + homeassistant/components/stream/worker.py homeassistant/components/streamlabswater/* homeassistant/components/suez_water/* homeassistant/components/supervisord/sensor.py @@ -1249,7 +1171,6 @@ omit = homeassistant/components/switchbee/switch.py homeassistant/components/switchbot/__init__.py homeassistant/components/switchbot/binary_sensor.py - homeassistant/components/switchbot/const.py homeassistant/components/switchbot/coordinator.py homeassistant/components/switchbot/cover.py homeassistant/components/switchbot/entity.py @@ -1262,7 +1183,6 @@ omit = homeassistant/components/syncthing/__init__.py homeassistant/components/syncthing/sensor.py homeassistant/components/syncthru/__init__.py - homeassistant/components/syncthru/binary_sensor.py homeassistant/components/syncthru/sensor.py homeassistant/components/synology_chat/notify.py homeassistant/components/synology_dsm/__init__.py @@ -1271,7 +1191,6 @@ omit = homeassistant/components/synology_dsm/camera.py homeassistant/components/synology_dsm/common.py homeassistant/components/synology_dsm/coordinator.py - homeassistant/components/synology_dsm/diagnostics.py homeassistant/components/synology_dsm/entity.py homeassistant/components/synology_dsm/sensor.py homeassistant/components/synology_dsm/service.py @@ -1281,9 +1200,7 @@ omit = homeassistant/components/syslog/notify.py homeassistant/components/system_bridge/__init__.py homeassistant/components/system_bridge/binary_sensor.py - homeassistant/components/system_bridge/const.py homeassistant/components/system_bridge/coordinator.py - homeassistant/components/system_bridge/media_source.py homeassistant/components/system_bridge/sensor.py homeassistant/components/systemmonitor/sensor.py homeassistant/components/tado/__init__.py @@ -1295,7 +1212,6 @@ omit = homeassistant/components/tank_utility/sensor.py homeassistant/components/tankerkoenig/__init__.py homeassistant/components/tankerkoenig/binary_sensor.py - homeassistant/components/tankerkoenig/const.py homeassistant/components/tankerkoenig/sensor.py homeassistant/components/tapsaff/binary_sensor.py homeassistant/components/tautulli/__init__.py @@ -1303,7 +1219,9 @@ omit = homeassistant/components/tautulli/sensor.py homeassistant/components/ted5000/sensor.py homeassistant/components/telegram/notify.py - homeassistant/components/telegram_bot/* + homeassistant/components/telegram_bot/__init__.py + homeassistant/components/telegram_bot/polling.py + homeassistant/components/telegram_bot/webhooks.py homeassistant/components/tellduslive/__init__.py homeassistant/components/tellduslive/binary_sensor.py homeassistant/components/tellduslive/cover.py @@ -1330,7 +1248,6 @@ omit = homeassistant/components/time_date/sensor.py homeassistant/components/tmb/sensor.py homeassistant/components/todoist/calendar.py - homeassistant/components/todoist/const.py homeassistant/components/tolo/__init__.py homeassistant/components/tolo/binary_sensor.py homeassistant/components/tolo/button.py @@ -1340,11 +1257,9 @@ omit = homeassistant/components/tolo/number.py homeassistant/components/tolo/select.py homeassistant/components/tolo/sensor.py - homeassistant/components/tomato/device_tracker.py homeassistant/components/toon/__init__.py homeassistant/components/toon/binary_sensor.py homeassistant/components/toon/climate.py - homeassistant/components/toon/const.py homeassistant/components/toon/coordinator.py homeassistant/components/toon/helpers.py homeassistant/components/toon/models.py @@ -1354,15 +1269,12 @@ omit = homeassistant/components/torque/sensor.py homeassistant/components/totalconnect/__init__.py homeassistant/components/totalconnect/binary_sensor.py - homeassistant/components/totalconnect/const.py homeassistant/components/touchline/climate.py homeassistant/components/tplink_lte/* - homeassistant/components/traccar/const.py homeassistant/components/traccar/device_tracker.py homeassistant/components/tractive/__init__.py homeassistant/components/tractive/binary_sensor.py homeassistant/components/tractive/device_tracker.py - homeassistant/components/tractive/diagnostics.py homeassistant/components/tractive/entity.py homeassistant/components/tractive/sensor.py homeassistant/components/tractive/switch.py @@ -1379,8 +1291,6 @@ omit = homeassistant/components/trafikverket_weatherstation/__init__.py homeassistant/components/trafikverket_weatherstation/coordinator.py homeassistant/components/trafikverket_weatherstation/sensor.py - homeassistant/components/transmission/const.py - homeassistant/components/transmission/errors.py homeassistant/components/transmission/sensor.py homeassistant/components/transmission/switch.py homeassistant/components/travisci/sensor.py @@ -1391,14 +1301,11 @@ omit = homeassistant/components/tuya/button.py homeassistant/components/tuya/camera.py homeassistant/components/tuya/climate.py - homeassistant/components/tuya/const.py homeassistant/components/tuya/cover.py - homeassistant/components/tuya/diagnostics.py homeassistant/components/tuya/fan.py homeassistant/components/tuya/humidifier.py homeassistant/components/tuya/light.py homeassistant/components/tuya/number.py - homeassistant/components/tuya/scene.py homeassistant/components/tuya/select.py homeassistant/components/tuya/sensor.py homeassistant/components/tuya/siren.py @@ -1412,12 +1319,9 @@ omit = homeassistant/components/ue_smart_radio/media_player.py homeassistant/components/ukraine_alarm/__init__.py homeassistant/components/ukraine_alarm/binary_sensor.py - homeassistant/components/ukraine_alarm/const.py homeassistant/components/unifiled/* homeassistant/components/upb/__init__.py - homeassistant/components/upb/const.py homeassistant/components/upb/light.py - homeassistant/components/upb/scene.py homeassistant/components/upc_connect/* homeassistant/components/upcloud/__init__.py homeassistant/components/upcloud/binary_sensor.py @@ -1430,14 +1334,14 @@ omit = homeassistant/components/velbus/binary_sensor.py homeassistant/components/velbus/button.py homeassistant/components/velbus/climate.py - homeassistant/components/velbus/const.py homeassistant/components/velbus/cover.py - homeassistant/components/velbus/diagnostics.py homeassistant/components/velbus/entity.py homeassistant/components/velbus/light.py homeassistant/components/velbus/sensor.py homeassistant/components/velbus/switch.py - homeassistant/components/velux/* + homeassistant/components/velux/__init__.py + homeassistant/components/velux/cover.py + homeassistant/components/velux/light.py homeassistant/components/venstar/__init__.py homeassistant/components/venstar/binary_sensor.py homeassistant/components/venstar/climate.py @@ -1447,14 +1351,12 @@ omit = homeassistant/components/verisure/binary_sensor.py homeassistant/components/verisure/camera.py homeassistant/components/verisure/coordinator.py - homeassistant/components/verisure/diagnostics.py homeassistant/components/verisure/lock.py homeassistant/components/verisure/sensor.py homeassistant/components/verisure/switch.py homeassistant/components/versasense/* homeassistant/components/vesync/__init__.py homeassistant/components/vesync/common.py - homeassistant/components/vesync/const.py homeassistant/components/vesync/fan.py homeassistant/components/vesync/light.py homeassistant/components/vesync/sensor.py @@ -1464,12 +1366,9 @@ omit = homeassistant/components/vicare/binary_sensor.py homeassistant/components/vicare/button.py homeassistant/components/vicare/climate.py - homeassistant/components/vicare/const.py - homeassistant/components/vicare/diagnostics.py homeassistant/components/vicare/sensor.py homeassistant/components/vicare/water_heater.py homeassistant/components/vilfo/__init__.py - homeassistant/components/vilfo/const.py homeassistant/components/vilfo/sensor.py homeassistant/components/vivotek/camera.py homeassistant/components/vlc/media_player.py @@ -1481,9 +1380,7 @@ omit = homeassistant/components/volumio/media_player.py homeassistant/components/volvooncall/__init__.py homeassistant/components/volvooncall/binary_sensor.py - homeassistant/components/volvooncall/const.py homeassistant/components/volvooncall/device_tracker.py - homeassistant/components/volvooncall/errors.py homeassistant/components/volvooncall/lock.py homeassistant/components/volvooncall/sensor.py homeassistant/components/volvooncall/switch.py @@ -1503,7 +1400,6 @@ omit = homeassistant/components/wiffi/wiffi_strings.py homeassistant/components/wirelesstag/* homeassistant/components/wolflink/__init__.py - homeassistant/components/wolflink/const.py homeassistant/components/wolflink/sensor.py homeassistant/components/worldtidesinfo/sensor.py homeassistant/components/worxlandroid/sensor.py @@ -1514,7 +1410,6 @@ omit = homeassistant/components/xbox/binary_sensor.py homeassistant/components/xbox/browse_media.py homeassistant/components/xbox/media_player.py - homeassistant/components/xbox/media_source.py homeassistant/components/xbox/remote.py homeassistant/components/xbox/sensor.py homeassistant/components/xbox_live/sensor.py @@ -1522,7 +1417,6 @@ omit = homeassistant/components/xiaomi/camera.py homeassistant/components/xiaomi_aqara/__init__.py homeassistant/components/xiaomi_aqara/binary_sensor.py - homeassistant/components/xiaomi_aqara/const.py homeassistant/components/xiaomi_aqara/cover.py homeassistant/components/xiaomi_aqara/light.py homeassistant/components/xiaomi_aqara/lock.py @@ -1535,7 +1429,6 @@ omit = homeassistant/components/xiaomi_miio/button.py homeassistant/components/xiaomi_miio/device.py homeassistant/components/xiaomi_miio/device_tracker.py - homeassistant/components/xiaomi_miio/diagnostics.py homeassistant/components/xiaomi_miio/fan.py homeassistant/components/xiaomi_miio/gateway.py homeassistant/components/xiaomi_miio/humidifier.py @@ -1551,9 +1444,7 @@ omit = homeassistant/components/yale_smart_alarm/alarm_control_panel.py homeassistant/components/yale_smart_alarm/binary_sensor.py homeassistant/components/yale_smart_alarm/button.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/yalexs_ble/__init__.py @@ -1567,14 +1458,13 @@ omit = homeassistant/components/yamaha_musiccast/number.py homeassistant/components/yamaha_musiccast/select.py homeassistant/components/yamaha_musiccast/switch.py - homeassistant/components/yandex_transport/* + homeassistant/components/yandex_transport/sensor.py homeassistant/components/yeelightsunflower/light.py homeassistant/components/yi/camera.py homeassistant/components/yolink/__init__.py homeassistant/components/yolink/api.py homeassistant/components/yolink/binary_sensor.py homeassistant/components/yolink/climate.py - homeassistant/components/yolink/const.py homeassistant/components/yolink/coordinator.py homeassistant/components/yolink/cover.py homeassistant/components/yolink/entity.py @@ -1584,25 +1474,22 @@ omit = homeassistant/components/yolink/siren.py homeassistant/components/yolink/switch.py homeassistant/components/youless/__init__.py - homeassistant/components/youless/const.py homeassistant/components/youless/sensor.py homeassistant/components/zabbix/* homeassistant/components/zamg/coordinator.py - homeassistant/components/zamg/sensor.py - homeassistant/components/zamg/weather.py homeassistant/components/zengge/light.py - homeassistant/components/zeroconf/* - homeassistant/components/zerproc/__init__.py - homeassistant/components/zerproc/const.py + homeassistant/components/zeroconf/models.py + homeassistant/components/zeroconf/usage.py homeassistant/components/zestimate/sensor.py + homeassistant/components/zeversolar/__init__.py + homeassistant/components/zeversolar/coordinator.py + homeassistant/components/zeversolar/entity.py + homeassistant/components/zeversolar/sensor.py homeassistant/components/zha/api.py homeassistant/components/zha/core/channels/* - homeassistant/components/zha/core/const.py homeassistant/components/zha/core/device.py homeassistant/components/zha/core/gateway.py homeassistant/components/zha/core/helpers.py - homeassistant/components/zha/core/registries.py - homeassistant/components/zha/entity.py homeassistant/components/zha/light.py homeassistant/components/zhong_hong/climate.py homeassistant/components/ziggo_mediabox_xl/media_player.py diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 9aea4badcc7..c3a0a721710 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -10,7 +10,7 @@ on: env: BUILD_TYPE: core - DEFAULT_PYTHON: 3.9 + DEFAULT_PYTHON: "3.10" jobs: init: @@ -24,12 +24,12 @@ jobs: publish: ${{ steps.version.outputs.publish }} steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 with: fetch-depth: 0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -67,10 +67,10 @@ jobs: if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true' steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -100,7 +100,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Download nightly wheels of frontend if: needs.init.outputs.channel == 'dev' @@ -113,9 +113,20 @@ jobs: workflow_conclusion: success name: wheels + - name: Download nightly wheels of intents + if: needs.init.outputs.channel == 'dev' + uses: dawidd6/action-download-artifact@v2 + with: + github_token: ${{secrets.GITHUB_TOKEN}} + repo: home-assistant/intents + branch: main + workflow: nightly.yaml + workflow_conclusion: success + name: package + - name: Set up Python ${{ env.DEFAULT_PYTHON }} if: needs.init.outputs.channel == 'dev' - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -140,6 +151,24 @@ jobs: python -m script.gen_requirements_all fi + if [[ "$(ls home_assistant_intents*.whl)" =~ ^home_assistant_intents-(.*)-py3-none-any.whl$ ]]; then + echo "Found intents wheel, setting version to: ${BASH_REMATCH[1]}" + yq \ + --inplace e -o json \ + 'del(.requirements[] | select(contains("home-assistant-intents")))' \ + homeassistant/components/conversation/manifest.json + + intents_version="${BASH_REMATCH[1]}" yq \ + --inplace e -o json \ + '.requirements += ["home-assistant-intents=="+env(intents_version)]' \ + homeassistant/components/conversation/manifest.json + + sed -i "s|home-assistant-intents==.*|home-assistant-intents==${BASH_REMATCH[1]}|" \ + homeassistant/package_constraints.txt + + python -m script.gen_requirements_all + fi + - name: Write meta info file shell: bash run: | @@ -198,7 +227,7 @@ jobs: - yellow steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set build additional args run: | @@ -241,7 +270,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Initialize git uses: home-assistant/actions/helpers/git-init@master @@ -280,7 +309,7 @@ jobs: - "homeassistant" steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Login to DockerHub if: matrix.registry == 'homeassistant' diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b6a4ba5a793..ab83359267c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -18,13 +18,21 @@ on: description: "Skip pytest" default: false type: boolean + pylint-only: + description: "Only run pylint" + default: false + type: boolean + mypy-only: + description: "Only run mypy" + default: false + type: boolean env: CACHE_VERSION: 3 PIP_CACHE_VERSION: 3 - HA_SHORT_VERSION: 2023.1 - DEFAULT_PYTHON: 3.9 - ALL_PYTHON_VERSIONS: "['3.9', '3.10']" + HA_SHORT_VERSION: 2023.2 + DEFAULT_PYTHON: "3.10" + ALL_PYTHON_VERSIONS: "['3.10']" PRE_COMMIT_CACHE: ~/.cache/pre-commit PIP_CACHE: /tmp/pip-cache SQLALCHEMY_WARN_20: 1 @@ -56,7 +64,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Generate partial Python venv restore key id: generate_python_cache_key run: >- @@ -163,20 +171,23 @@ jobs: pre-commit: name: Prepare pre-commit base runs-on: ubuntu-20.04 + if: | + github.event.inputs.pylint-only != 'true' + && github.event.inputs.mypy-only != 'true' needs: - info steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.2.2 + uses: actions/cache@v3.2.3 with: path: venv key: >- @@ -191,7 +202,7 @@ jobs: pip install "$(cat requirements_test.txt | grep pre-commit)" - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache@v3.2.2 + uses: actions/cache@v3.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -211,16 +222,16 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -233,7 +244,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -265,16 +276,16 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -287,7 +298,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -313,25 +324,24 @@ jobs: . venv/bin/activate shopt -s globstar pre-commit run --hook-stage manual flake8 --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/{*,**/*} - - lint-isort: - name: Check isort - runs-on: ubuntu-20.04 + lint-ruff: + name: Check ruff + runs-on: ubuntu-latest needs: - info - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -344,7 +354,63 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 + with: + path: ${{ env.PRE_COMMIT_CACHE }} + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.info.outputs.pre-commit_cache_key }} + - name: Fail job if pre-commit cache restore failed + if: steps.cache-precommit.outputs.cache-hit != 'true' + run: | + echo "Failed to restore pre-commit environment from cache" + exit 1 + - name: Register ruff problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/ruff.json" + - name: Run ruff (fully) + if: needs.info.outputs.test_full_suite == 'true' + run: | + . venv/bin/activate + pre-commit run --hook-stage manual ruff --all-files + - name: Run ruff (partially) + if: needs.info.outputs.test_full_suite == 'false' + shell: bash + run: | + . venv/bin/activate + shopt -s globstar + pre-commit run --hook-stage manual ruff --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/{*,**/*} + lint-isort: + name: Check isort + runs-on: ubuntu-20.04 + needs: + - info + - pre-commit + steps: + - name: Check out code from GitHub + uses: actions/checkout@v3.3.0 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v4.5.0 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache/restore@v3.2.3 + with: + path: venv + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ + needs.info.outputs.pre-commit_cache_key }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache/restore@v3.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -368,16 +434,16 @@ jobs: - pre-commit steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -390,7 +456,7 @@ jobs: exit 1 - name: Restore pre-commit environment from cache id: cache-precommit - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: ${{ env.PRE_COMMIT_CACHE }} key: >- @@ -495,10 +561,10 @@ jobs: python-version: ${{ fromJSON(needs.info.outputs.python_versions) }} steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ matrix.python-version }} check-latest: true @@ -509,7 +575,7 @@ jobs: env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache@v3.2.2 + uses: actions/cache@v3.2.3 with: path: venv key: >- @@ -517,7 +583,7 @@ jobs: needs.info.outputs.python_cache_key }} - name: Restore pip wheel cache if: steps.cache-venv.outputs.cache-hit != 'true' - uses: actions/cache@v3.2.2 + uses: actions/cache@v3.2.3 with: path: ${{ env.PIP_CACHE }} key: >- @@ -554,21 +620,24 @@ jobs: hassfest: name: Check hassfest runs-on: ubuntu-20.04 + if: | + github.event.inputs.pylint-only != 'true' + && github.event.inputs.mypy-only != 'true' needs: - info - base steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -587,21 +656,24 @@ jobs: gen-requirements-all: name: Check all requirements runs-on: ubuntu-20.04 + if: | + github.event.inputs.pylint-only != 'true' + && github.event.inputs.mypy-only != 'true' needs: - info - base steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore base Python virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -621,21 +693,24 @@ jobs: name: Check pylint runs-on: ubuntu-20.04 timeout-minutes: 20 + if: | + github.event.inputs.mypy-only != 'true' + || github.event.inputs.pylint-only == 'true' needs: - info - base steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -666,21 +741,24 @@ jobs: mypy: name: Check mypy runs-on: ubuntu-20.04 + if: | + github.event.inputs.pylint-only != 'true' + || github.event.inputs.mypy-only == 'true' needs: - info - base steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} check-latest: true - name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -710,6 +788,9 @@ jobs: pip-check: runs-on: ubuntu-20.04 + if: | + github.event.inputs.pylint-only != 'true' + && github.event.inputs.mypy-only != 'true' needs: - info - base @@ -720,16 +801,16 @@ jobs: name: Run pip check ${{ matrix.python-version }} steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: >- @@ -750,6 +831,8 @@ jobs: if: | (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') && github.event.inputs.lint-only != 'true' + && github.event.inputs.pylint-only != 'true' + && github.event.inputs.mypy-only != 'true' && (needs.info.outputs.test_full_suite == 'true' || needs.info.outputs.tests_glob) needs: - info @@ -775,16 +858,16 @@ jobs: bluez \ ffmpeg - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -852,7 +935,7 @@ jobs: -p no:sugar \ tests/components/${{ matrix.group }} - name: Upload coverage artifact - uses: actions/upload-artifact@v3.1.1 + uses: actions/upload-artifact@v3.1.2 with: name: coverage-${{ matrix.python-version }}-${{ matrix.group }} path: coverage.xml @@ -873,6 +956,8 @@ jobs: if: | (github.event_name != 'push' || github.event.repository.full_name == 'home-assistant/core') && github.event.inputs.lint-only != 'true' + && github.event.inputs.pylint-only != 'true' + && github.event.inputs.mypy-only != 'true' && needs.info.outputs.test_full_suite == 'true' needs: - info @@ -898,16 +983,16 @@ jobs: ffmpeg \ libmariadb-dev-compat - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ matrix.python-version }} id: python - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ matrix.python-version }} check-latest: true - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv - uses: actions/cache/restore@v3.2.2 + uses: actions/cache/restore@v3.2.3 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ @@ -954,7 +1039,7 @@ jobs: --dburl=mysql://root:password@127.0.0.1/homeassistant-test \ tests/components/recorder - name: Upload coverage artifact - uses: actions/upload-artifact@v3.1.1 + uses: actions/upload-artifact@v3.1.2 with: name: coverage-${{ matrix.python-version }}-mariadb path: coverage.xml @@ -970,7 +1055,7 @@ jobs: - pytest steps: - name: Check out code from GitHub - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Download all coverage artifacts uses: actions/download-artifact@v3 - name: Upload coverage to Codecov (full coverage) diff --git a/.github/workflows/matchers/ruff.json b/.github/workflows/matchers/ruff.json new file mode 100644 index 00000000000..d189a3656a5 --- /dev/null +++ b/.github/workflows/matchers/ruff.json @@ -0,0 +1,30 @@ +{ + "problemMatcher": [ + { + "owner": "ruff-error", + "severity": "error", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s([EF]\\d{3}\\s.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + } + ] + }, + { + "owner": "ruff-warning", + "severity": "warning", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s([CDNW]\\d{3}\\s.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + } + ] + } + ] +} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 7ae6a3477d7..4bebe2d9043 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -11,34 +11,21 @@ jobs: if: github.repository_owner == 'home-assistant' runs-on: ubuntu-latest steps: - # The 90 day stale policy + # The 90 day stale policy for PRs # Used for: - # - Issues & PRs + # - PRs # - No PRs marked as no-stale - # - No issues marked as no-stale or help-wanted - - name: 90 days stale issues & PRs policy + # - No issues (-1) + - name: 90 days stale PRs policy uses: actions/stale@v7.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 90 days-before-close: 7 + days-before-issue-stale: -1 + days-before-issue-close: -1 operations-per-run: 150 remove-stale-when-updated: true - stale-issue-label: "stale" - exempt-issue-labels: "no-stale,help-wanted" - stale-issue-message: > - There hasn't been any activity on this issue recently. Due to the - high number of incoming GitHub notifications, we have to clean some - of the old issues, as many of them have already been resolved with - the latest updates. - - Please make sure to update to the latest Home Assistant version and - check if that solves the issue. Let us know if that works for you by - adding a comment 👍 - - This issue has now been marked as stale and will be closed if no - further activity occurs. Thank you for your contributions. - stale-pr-label: "stale" exempt-pr-labels: "no-stale" stale-pr-message: > @@ -48,30 +35,47 @@ jobs: Thank you for your contributions. - # The 30 day stale policy for PRS + # Generate a token for the GitHub App, we use this method to avoid + # hitting API limits for our GitHub actions + have a higher rate limit. + # This is only used for issues. + - name: Generate app token + id: token + # Pinned to a specific version of the action for security reasons + # v1.7.0 + uses: tibdex/github-app-token@021a2405c7f990db57f5eae5397423dcc554159c + with: + app_id: ${{ secrets.ISSUE_TRIAGE_APP_ID }} + private_key: ${{ secrets.ISSUE_TRIAGE_APP_PEM }} + + # The 90 day stale policy for issues # Used for: - # - PRs - # - No PRs marked as no-stale or new-integrations - # - No issues (-1) - - name: 30 days stale PRs policy + # - Issues + # - No issues marked as no-stale or help-wanted + # - No PRs (-1) + - name: 90 days stale issues uses: actions/stale@v7.0.0 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - days-before-stale: 30 + repo-token: ${{ steps.token.outputs.token }} + days-before-stale: 90 days-before-close: 7 - days-before-issue-close: -1 - operations-per-run: 50 + days-before-pr-stale: -1 + days-before-pr-close: -1 + operations-per-run: 250 remove-stale-when-updated: true - stale-pr-label: "stale" - # Exempt new integrations, these often take more time. - # They will automatically be handled by the 90 day version above. - exempt-pr-labels: "no-stale,new-integration" - stale-pr-message: > - There hasn't been any activity on this pull request recently. This - pull request has been automatically marked as stale because of that - and will be closed if no further activity occurs within 7 days. + stale-issue-label: "stale" + exempt-issue-labels: "no-stale,help-wanted,needs-more-information" + stale-issue-message: > + There hasn't been any activity on this issue recently. Due to the + high number of incoming GitHub notifications, we have to clean some + of the old issues, as many of them have already been resolved with + the latest updates. - Thank you for your contributions. + Please make sure to update to the latest Home Assistant version and + check if that solves the issue. Let us know if that works for you by + adding a comment 👍 + + This issue has now been marked as stale and will be closed if no + further activity occurs. Thank you for your contributions. # The 30 day stale policy for issues # Used for: @@ -81,12 +85,13 @@ jobs: - name: Needs more information stale issues policy uses: actions/stale@v7.0.0 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} + repo-token: ${{ steps.token.outputs.token }} only-labels: "needs-more-information" days-before-stale: 14 days-before-close: 7 + days-before-pr-stale: -1 days-before-pr-close: -1 - operations-per-run: 50 + operations-per-run: 250 remove-stale-when-updated: true stale-issue-label: "stale" exempt-issue-labels: "no-stale,help-wanted" diff --git a/.github/workflows/translations.yaml b/.github/workflows/translations.yaml index 1dcbcfabd3a..e96cb415d67 100644 --- a/.github/workflows/translations.yaml +++ b/.github/workflows/translations.yaml @@ -12,7 +12,7 @@ on: - "**strings.json" env: - DEFAULT_PYTHON: 3.9 + DEFAULT_PYTHON: "3.10" jobs: upload: @@ -21,10 +21,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -40,10 +40,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4.4.0 + uses: actions/setup-python@v4.5.0 with: python-version: ${{ env.DEFAULT_PYTHON }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 20b758d032f..59604ae0e4d 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -22,7 +22,7 @@ jobs: architectures: ${{ steps.info.outputs.architectures }} steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Get information id: info @@ -57,13 +57,13 @@ jobs: ) > .env_file - name: Upload env_file - uses: actions/upload-artifact@v3.1.1 + uses: actions/upload-artifact@v3.1.2 with: name: env_file path: ./.env_file - name: Upload requirements_diff - uses: actions/upload-artifact@v3.1.1 + uses: actions/upload-artifact@v3.1.2 with: name: requirements_diff path: ./requirements_diff.txt @@ -79,7 +79,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Download env_file uses: actions/download-artifact@v3 @@ -116,7 +116,7 @@ jobs: arch: ${{ fromJson(needs.init.outputs.architectures) }} steps: - name: Checkout the repository - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3.3.0 - name: Download env_file uses: actions/download-artifact@v3 @@ -144,6 +144,14 @@ jobs: sed -i "s|# opencv-python-headless|opencv-python-headless|g" ${requirement_file} done + - name: Split requirements all + run: | + # We split requirements all into two different files. + # This is to prevent the build from running out of memory when + # resolving packages on 32-bits systems (like armhf, armv7). + + split -l $(expr $(expr $(cat requirements_all.txt | wc -l) + 1) / 2) requirements_all.txt requirements_all.txt + - name: Adjust build env run: | if [ "${{ matrix.arch }}" = "i386" ]; then @@ -159,7 +167,7 @@ jobs: # Do not pin numpy in wheels building sed -i "/numpy/d" homeassistant/package_constraints.txt - - name: Build wheels + - name: Build wheels (part 1) uses: home-assistant/wheels@2022.10.1 with: abi: cp310 @@ -172,4 +180,19 @@ jobs: legacy: true constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" - requirements: "requirements_all.txt" + requirements: "requirements_all.txtaa" + + - name: Build wheels (part 2) + uses: home-assistant/wheels@2022.10.1 + with: + abi: cp310 + tag: musllinux_1_2 + arch: ${{ matrix.arch }} + wheels-key: ${{ secrets.WHEELS_KEY }} + env-file: true + apk: "libexecinfo-dev;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev" + skip-binary: aiohttp;grpcio + legacy: true + constraints: "homeassistant/package_constraints.txt" + requirements-diff: "requirements_diff.txt" + requirements: "requirements_all.txtab" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d68fde0ec25..f7684e4d4d7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,16 @@ repos: + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.0.231 + hooks: + - id: ruff + args: + - --fix - repo: https://github.com/asottile/pyupgrade rev: v3.3.1 hooks: - id: pyupgrade - args: [--py39-plus] + args: [--py310-plus] + stages: [manual] - repo: https://github.com/PyCQA/autoflake rev: v2.0.0 hooks: @@ -11,6 +18,7 @@ repos: args: - --in-place - --remove-all-unused-imports + stages: [manual] - repo: https://github.com/psf/black rev: 22.12.0 hooks: @@ -36,11 +44,12 @@ repos: - pycodestyle==2.10.0 - pyflakes==3.0.1 - flake8-docstrings==1.6.0 - - pydocstyle==6.1.1 + - pydocstyle==6.2.3 - flake8-comprehensions==3.10.1 - flake8-noqa==1.3.0 - mccabe==0.7.0 - files: ^(homeassistant|script|tests)/.+\.py$ + exclude: docs/source/conf.py + stages: [manual] - repo: https://github.com/PyCQA/bandit rev: 1.7.4 hooks: @@ -51,11 +60,11 @@ repos: - --configfile=tests/bandit.yaml files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/isort - rev: 5.11.4 + rev: 5.12.0 hooks: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + rev: v4.4.0 hooks: - id: check-executables-have-shebangs stages: [manual] @@ -84,7 +93,7 @@ repos: - id: python-typing-update stages: [manual] args: - - --py39-plus + - --py310-plus - --force - --keep-updates files: ^(homeassistant|tests|script)/.+\.py$ diff --git a/.strict-typing b/.strict-typing index 598f2bc1f6d..48ba71ff031 100644 --- a/.strict-typing +++ b/.strict-typing @@ -58,6 +58,7 @@ homeassistant.components.amcrest.* homeassistant.components.ampio.* homeassistant.components.analytics.* homeassistant.components.anthemav.* +homeassistant.components.apcupsd.* homeassistant.components.aqualogic.* homeassistant.components.aseko_pool_live.* homeassistant.components.asuswrt.* @@ -68,6 +69,7 @@ homeassistant.components.backup.* homeassistant.components.baf.* homeassistant.components.bayesian.* homeassistant.components.binary_sensor.* +homeassistant.components.bitcoin.* homeassistant.components.blockchain.* homeassistant.components.bluetooth.* homeassistant.components.bluetooth_tracker.* @@ -110,6 +112,7 @@ homeassistant.components.fastdotcom.* homeassistant.components.feedreader.* homeassistant.components.file_upload.* homeassistant.components.filesize.* +homeassistant.components.filter.* homeassistant.components.fitbit.* homeassistant.components.flux_led.* homeassistant.components.forecast_solar.* @@ -172,10 +175,12 @@ homeassistant.components.jewish_calendar.* homeassistant.components.kaleidescape.* homeassistant.components.knx.* homeassistant.components.kraken.* +homeassistant.components.lacrosse.* homeassistant.components.lacrosse_view.* homeassistant.components.lametric.* homeassistant.components.laundrify.* homeassistant.components.lcn.* +homeassistant.components.ld2410_ble.* homeassistant.components.lidarr.* homeassistant.components.lifx.* homeassistant.components.light.* @@ -198,6 +203,7 @@ homeassistant.components.mjpeg.* homeassistant.components.modbus.* homeassistant.components.modem_callerid.* homeassistant.components.moon.* +homeassistant.components.mopeka.* homeassistant.components.mqtt.* homeassistant.components.mysensors.* homeassistant.components.nam.* @@ -219,6 +225,7 @@ homeassistant.components.onewire.* homeassistant.components.open_meteo.* homeassistant.components.openexchangerates.* homeassistant.components.openuv.* +homeassistant.components.otbr.* homeassistant.components.overkiz.* homeassistant.components.peco.* homeassistant.components.persistent_notification.* @@ -245,17 +252,21 @@ homeassistant.components.ridwell.* homeassistant.components.rituals_perfume_genie.* homeassistant.components.roku.* homeassistant.components.rpi_power.* +homeassistant.components.rss_feed_template.* homeassistant.components.rtsp_to_webrtc.* +homeassistant.components.ruuvi_gateway.* homeassistant.components.ruuvitag_ble.* homeassistant.components.samsungtv.* homeassistant.components.scene.* homeassistant.components.schedule.* +homeassistant.components.scrape.* homeassistant.components.select.* homeassistant.components.senseme.* homeassistant.components.sensibo.* homeassistant.components.sensirion_ble.* homeassistant.components.sensor.* homeassistant.components.senz.* +homeassistant.components.sfr_box.* homeassistant.components.shelly.* homeassistant.components.simplepush.* homeassistant.components.simplisafe.* diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d71571d2594..4b62a16042d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -41,6 +41,20 @@ }, "problemMatcher": [] }, + { + "label": "Ruff", + "type": "shell", + "command": "pre-commit run ruff --all-files", + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, { "label": "Pylint", "type": "shell", diff --git a/CODEOWNERS b/CODEOWNERS index 8fe0ce4e831..6d87f133526 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -67,8 +67,6 @@ build.json @home-assistant/supervisor /tests/components/alert/ @home-assistant/core @frenck /homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh /tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh -/homeassistant/components/almond/ @gcampax @balloob -/tests/components/almond/ @gcampax @balloob /homeassistant/components/amberelectric/ @madpilot /tests/components/amberelectric/ @madpilot /homeassistant/components/ambiclimate/ @danielhiversen @@ -156,6 +154,8 @@ build.json @home-assistant/supervisor /homeassistant/components/bluesound/ @thrawnarn /homeassistant/components/bluetooth/ @bdraco /tests/components/bluetooth/ @bdraco +/homeassistant/components/bluetooth_adapters/ @bdraco +/tests/components/bluetooth_adapters/ @bdraco /homeassistant/components/bmw_connected_drive/ @gerard33 @rikroe /tests/components/bmw_connected_drive/ @gerard33 @rikroe /homeassistant/components/bond/ @bdraco @prystupa @joshs85 @marciogranzotto @@ -265,6 +265,8 @@ build.json @home-assistant/supervisor /tests/components/discord/ @tkdrob /homeassistant/components/discovery/ @home-assistant/core /tests/components/discovery/ @home-assistant/core +/homeassistant/components/dlink/ @tkdrob +/tests/components/dlink/ @tkdrob /homeassistant/components/dlna_dmr/ @StevenLooman @chishm /tests/components/dlna_dmr/ @StevenLooman @chishm /homeassistant/components/dlna_dms/ @chishm @@ -313,6 +315,8 @@ build.json @home-assistant/supervisor /tests/components/emulated_kasa/ @kbickar /homeassistant/components/energy/ @home-assistant/core /tests/components/energy/ @home-assistant/core +/homeassistant/components/energyzero/ @klaasnicolaas +/tests/components/energyzero/ @klaasnicolaas /homeassistant/components/enigma2/ @fbradyirl /homeassistant/components/enocean/ @bdurrer /tests/components/enocean/ @bdurrer @@ -331,6 +335,8 @@ build.json @home-assistant/supervisor /tests/components/escea/ @lazdavila /homeassistant/components/esphome/ @OttoWinter @jesserockz /tests/components/esphome/ @OttoWinter @jesserockz +/homeassistant/components/eufylife_ble/ @bdr99 +/tests/components/eufylife_ble/ @bdr99 /homeassistant/components/evil_genius_labs/ @balloob /tests/components/evil_genius_labs/ @balloob /homeassistant/components/evohome/ @zxdavb @@ -433,6 +439,8 @@ build.json @home-assistant/supervisor /homeassistant/components/google_assistant_sdk/ @tronikos /tests/components/google_assistant_sdk/ @tronikos /homeassistant/components/google_cloud/ @lufton +/homeassistant/components/google_mail/ @tkdrob +/tests/components/google_mail/ @tkdrob /homeassistant/components/google_sheets/ @tkdrob /tests/components/google_sheets/ @tkdrob /homeassistant/components/google_travel_time/ @eifinger @@ -499,8 +507,8 @@ build.json @home-assistant/supervisor /tests/components/homematic/ @pvizeli @danielperna84 /homeassistant/components/homewizard/ @DCSBL /tests/components/homewizard/ @DCSBL -/homeassistant/components/honeywell/ @rdfurman -/tests/components/honeywell/ @rdfurman +/homeassistant/components/honeywell/ @rdfurman @mkmer +/tests/components/honeywell/ @rdfurman @mkmer /homeassistant/components/http/ @home-assistant/core /tests/components/http/ @home-assistant/core /homeassistant/components/huawei_lte/ @scop @fphammerle @@ -533,6 +541,8 @@ build.json @home-assistant/supervisor /tests/components/image_processing/ @home-assistant/core /homeassistant/components/image_upload/ @home-assistant/core /tests/components/image_upload/ @home-assistant/core +/homeassistant/components/imap/ @engrbm87 +/tests/components/imap/ @engrbm87 /homeassistant/components/incomfort/ @zxdavb /homeassistant/components/influxdb/ @mdegat01 /tests/components/influxdb/ @mdegat01 @@ -599,6 +609,8 @@ build.json @home-assistant/supervisor /homeassistant/components/keyboard_remote/ @bendavid @lanrat /homeassistant/components/keymitt_ble/ @spycle /tests/components/keymitt_ble/ @spycle +/homeassistant/components/kitchen_sink/ @home-assistant/core +/tests/components/kitchen_sink/ @home-assistant/core /homeassistant/components/kmtronic/ @dgomes /tests/components/kmtronic/ @dgomes /homeassistant/components/knx/ @Julius2342 @farmio @marvin-w @@ -625,6 +637,8 @@ build.json @home-assistant/supervisor /tests/components/laundrify/ @xLarry /homeassistant/components/lcn/ @alengwenus /tests/components/lcn/ @alengwenus +/homeassistant/components/ld2410_ble/ @930913 +/tests/components/ld2410_ble/ @930913 /homeassistant/components/led_ble/ @bdraco /tests/components/led_ble/ @bdraco /homeassistant/components/lg_netcast/ @Drafteed @@ -669,7 +683,6 @@ build.json @home-assistant/supervisor /homeassistant/components/lyric/ @timmo001 /tests/components/lyric/ @timmo001 /homeassistant/components/mastodon/ @fabaff -/homeassistant/components/matrix/ @tinloaf /homeassistant/components/matter/ @home-assistant/matter /tests/components/matter/ @home-assistant/matter /homeassistant/components/mazda/ @bdr99 @@ -725,6 +738,8 @@ build.json @home-assistant/supervisor /tests/components/monoprice/ @etsinko @OnFreund /homeassistant/components/moon/ @fabaff @frenck /tests/components/moon/ @fabaff @frenck +/homeassistant/components/mopeka/ @bdraco +/tests/components/mopeka/ @bdraco /homeassistant/components/motion_blinds/ @starkillerOG /tests/components/motion_blinds/ @starkillerOG /homeassistant/components/motioneye/ @dermotduffy @@ -825,6 +840,8 @@ build.json @home-assistant/supervisor /tests/components/onvif/ @hunterjm /homeassistant/components/open_meteo/ @frenck /tests/components/open_meteo/ @frenck +/homeassistant/components/openai_conversation/ @balloob +/tests/components/openai_conversation/ @balloob /homeassistant/components/openerz/ @misialq /tests/components/openerz/ @misialq /homeassistant/components/openexchangerates/ @MartinHjelmare @@ -840,9 +857,11 @@ build.json @home-assistant/supervisor /tests/components/openweathermap/ @fabaff @freekode @nzapponi /homeassistant/components/opnsense/ @mtreinish /tests/components/opnsense/ @mtreinish -/homeassistant/components/oralb/ @bdraco -/tests/components/oralb/ @bdraco +/homeassistant/components/oralb/ @bdraco @Lash-L +/tests/components/oralb/ @bdraco @Lash-L /homeassistant/components/oru/ @bvlaicu +/homeassistant/components/otbr/ @home-assistant/core +/tests/components/otbr/ @home-assistant/core /homeassistant/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev /tests/components/overkiz/ @imicknl @vlebourl @tetienne @nyroDev /homeassistant/components/ovo_energy/ @timmo001 @@ -877,8 +896,8 @@ build.json @home-assistant/supervisor /tests/components/point/ @fredrike /homeassistant/components/poolsense/ @haemishkyd /tests/components/poolsense/ @haemishkyd -/homeassistant/components/powerwall/ @bdraco @jrester -/tests/components/powerwall/ @bdraco @jrester +/homeassistant/components/powerwall/ @bdraco @jrester @daniel-simpson +/tests/components/powerwall/ @bdraco @jrester @daniel-simpson /homeassistant/components/profiler/ @bdraco /tests/components/profiler/ @bdraco /homeassistant/components/progettihwsw/ @ardaseremet @@ -980,8 +999,12 @@ build.json @home-assistant/supervisor /tests/components/rtsp_to_webrtc/ @allenporter /homeassistant/components/ruckus_unleashed/ @gabe565 /tests/components/ruckus_unleashed/ @gabe565 +/homeassistant/components/ruuvi_gateway/ @akx +/tests/components/ruuvi_gateway/ @akx /homeassistant/components/ruuvitag_ble/ @akx /tests/components/ruuvitag_ble/ @akx +/homeassistant/components/rympro/ @OnFreund +/tests/components/rympro/ @OnFreund /homeassistant/components/sabnzbd/ @shaiu /tests/components/sabnzbd/ @shaiu /homeassistant/components/safe_mode/ @home-assistant/core @@ -1026,6 +1049,8 @@ build.json @home-assistant/supervisor /tests/components/senz/ @milanmeu /homeassistant/components/serial/ @fabaff /homeassistant/components/seven_segments/ @fabaff +/homeassistant/components/sfr_box/ @epenet +/tests/components/sfr_box/ @epenet /homeassistant/components/sharkiq/ @JeffResc @funkybunch @AritroSaha10 /tests/components/sharkiq/ @JeffResc @funkybunch @AritroSaha10 /homeassistant/components/shell_command/ @home-assistant/core @@ -1107,6 +1132,8 @@ build.json @home-assistant/supervisor /tests/components/srp_energy/ @briglx /homeassistant/components/starline/ @anonym-tsk /tests/components/starline/ @anonym-tsk +/homeassistant/components/starlink/ @boswelja +/tests/components/starlink/ @boswelja /homeassistant/components/statistics/ @fabaff @ThomDietrich /tests/components/statistics/ @fabaff @ThomDietrich /homeassistant/components/steam_online/ @tkdrob @@ -1116,6 +1143,8 @@ build.json @home-assistant/supervisor /homeassistant/components/stiebel_eltron/ @fucm /homeassistant/components/stookalert/ @fwestenberg @frenck /tests/components/stookalert/ @fwestenberg @frenck +/homeassistant/components/stookwijzer/ @fwestenberg +/tests/components/stookwijzer/ @fwestenberg /homeassistant/components/stream/ @hunterjm @uvjustin @allenporter /tests/components/stream/ @hunterjm @uvjustin @allenporter /homeassistant/components/stt/ @pvizeli @@ -1177,6 +1206,8 @@ build.json @home-assistant/supervisor /homeassistant/components/thermopro/ @bdraco /tests/components/thermopro/ @bdraco /homeassistant/components/thethingsnetwork/ @fabaff +/homeassistant/components/thread/ @home-assistant/core +/tests/components/thread/ @home-assistant/core /homeassistant/components/threshold/ @fabaff /tests/components/threshold/ @fabaff /homeassistant/components/tibber/ @danielhiversen @@ -1299,8 +1330,8 @@ build.json @home-assistant/supervisor /tests/components/websocket_api/ @home-assistant/core /homeassistant/components/wemo/ @esev /tests/components/wemo/ @esev -/homeassistant/components/whirlpool/ @abmantis -/tests/components/whirlpool/ @abmantis +/homeassistant/components/whirlpool/ @abmantis @mkmer +/tests/components/whirlpool/ @abmantis @mkmer /homeassistant/components/whois/ @frenck /tests/components/whois/ @frenck /homeassistant/components/wiffi/ @mampfes @@ -1356,6 +1387,8 @@ build.json @home-assistant/supervisor /tests/components/zeroconf/ @bdraco /homeassistant/components/zerproc/ @emlove /tests/components/zerproc/ @emlove +/homeassistant/components/zeversolar/ @kvanzuijlen +/tests/components/zeversolar/ @kvanzuijlen /homeassistant/components/zha/ @dmulcahey @adminiuga @puddly /tests/components/zha/ @dmulcahey @adminiuga @puddly /homeassistant/components/zodiac/ @JulienTant @@ -1365,8 +1398,8 @@ build.json @home-assistant/supervisor /homeassistant/components/zoneminder/ @rohankapoorcom /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 +/homeassistant/components/zwave_me/ @lawfulchaos @Z-Wave-Me @PoltoS +/tests/components/zwave_me/ @lawfulchaos @Z-Wave-Me @PoltoS # Individual files /homeassistant/components/demo/weather.py @fabaff diff --git a/Dockerfile b/Dockerfile index b80e86fb33c..fa8f5520f22 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,8 @@ FROM ${BUILD_FROM} ENV \ S6_SERVICES_GRACETIME=220000 +ARG QEMU_CPU + WORKDIR /usr/src ## Setup Home Assistant Core dependencies @@ -19,7 +21,7 @@ RUN \ --use-deprecated=legacy-resolver \ -r homeassistant/requirements.txt -COPY requirements_all.txt home_assistant_frontend-* homeassistant/ +COPY requirements_all.txt home_assistant_frontend-* home_assistant_intents-* homeassistant/ RUN \ if ls homeassistant/home_assistant_frontend*.whl 1> /dev/null 2>&1; then \ pip3 install \ @@ -27,6 +29,12 @@ RUN \ --no-index \ homeassistant/home_assistant_frontend-*.whl; \ fi \ + && if ls homeassistant/home_assistant_intents*.whl 1> /dev/null 2>&1; then \ + pip3 install \ + --no-cache-dir \ + --no-index \ + homeassistant/home_assistant_intents-*.whl; \ + fi \ && \ LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \ MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \ diff --git a/Dockerfile.dev b/Dockerfile.dev index fc9843461a0..863ac5690bc 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.9 +FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.10 SHELL ["/bin/bash", "-o", "pipefail", "-c"] diff --git a/codecov.yml b/codecov.yml index 21372758263..8c3c5b35ca5 100644 --- a/codecov.yml +++ b/codecov.yml @@ -6,19 +6,37 @@ coverage: default: target: 90 threshold: 0.09 - config-flows: + required: target: auto threshold: 1 paths: - homeassistant/components/*/config_flow.py + - homeassistant/components/*/device_action.py + - homeassistant/components/*/device_condition.py + - homeassistant/components/*/device_trigger.py + - homeassistant/components/*/diagnostics.py + - homeassistant/components/*/group.py + - homeassistant/components/*/intent.py + - homeassistant/components/*/logbook.py + - homeassistant/components/*/media_source.py + - homeassistant/components/*/scene.py patch: default: target: auto - config-flows: + required: target: 100 threshold: 0 paths: - homeassistant/components/*/config_flow.py + - homeassistant/components/*/device_action.py + - homeassistant/components/*/device_condition.py + - homeassistant/components/*/device_trigger.py + - homeassistant/components/*/diagnostics.py + - homeassistant/components/*/group.py + - homeassistant/components/*/intent.py + - homeassistant/components/*/logbook.py + - homeassistant/components/*/media_source.py + - homeassistant/components/*/scene.py comment: false # To make partial tests possible, diff --git a/docs/source/_ext/edit_on_github.py b/docs/source/_ext/edit_on_github.py index 1d40bfc33ab..420acbbdde5 100644 --- a/docs/source/_ext/edit_on_github.py +++ b/docs/source/_ext/edit_on_github.py @@ -1,6 +1,5 @@ """ -Sphinx extension to add ReadTheDocs-style "Edit on GitHub" links to the -sidebar. +Sphinx extension for ReadTheDocs-style "Edit on GitHub" links on the sidebar. Loosely based on https://github.com/astropy/astropy/pull/347 """ @@ -12,6 +11,7 @@ __licence__ = "BSD (3 clause)" def get_github_url(app, view, path): + """Build the GitHub URL.""" return ( f"https://github.com/{app.config.edit_on_github_project}/" f"{view}/{app.config.edit_on_github_branch}/" @@ -20,6 +20,7 @@ def get_github_url(app, view, path): def html_page_context(app, pagename, templatename, context, doctree): + """Build the HTML page.""" if templatename != "page.html": return @@ -38,6 +39,7 @@ def html_page_context(app, pagename, templatename, context, doctree): def setup(app): + """Set up the app.""" app.add_config_value("edit_on_github_project", "", True) app.add_config_value("edit_on_github_branch", "master", True) app.add_config_value("edit_on_github_src_path", "", True) # 'eg' "docs/" diff --git a/docs/source/conf.py b/docs/source/conf.py index ab09df87ae3..302a0655544 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,21 +1,20 @@ #!/usr/bin/env python3 -# -# Home-Assistant documentation build configuration file, created by -# sphinx-quickstart on Sun Aug 28 13:13:10 2016. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +"""Home Assistant documentation build configuration file. + +This file is execfile()d with the current directory set to its +containing dir. + +Note that not all possible configuration values are present in this +autogenerated file. + +All configuration values have a default; values that are commented out +serve to show the default. + +If extensions (or modules to document with autodoc) are in another directory, +add these directories to sys.path here. If the directory is relative to the +documentation root, use os.path.abspath to make it absolute, like shown here. +""" -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# import inspect import os import sys @@ -25,7 +24,7 @@ from homeassistant.const import __short_version__, __version__ PROJECT_NAME = "Home Assistant" PROJECT_PACKAGE_NAME = "homeassistant" PROJECT_AUTHOR = "The Home Assistant Authors" -PROJECT_COPYRIGHT = f" 2013-2020, {PROJECT_AUTHOR}" +PROJECT_COPYRIGHT = PROJECT_AUTHOR PROJECT_LONG_DESCRIPTION = ( "Home Assistant is an open-source " "home automation platform running on Python 3. " @@ -110,17 +109,17 @@ def linkcode_resolve(domain, info): for part in fullname.split("."): try: obj = getattr(obj, part) - except: + except Exception: # pylint: disable=broad-except return None try: fn = inspect.getsourcefile(obj) - except: + except Exception: # pylint: disable=broad-except fn = None if not fn: return None try: source, lineno = inspect.findsource(obj) - except: + except Exception: # pylint: disable=broad-except lineno = None if lineno: linespec = "#L%d" % (lineno + 1) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 9dfe4f4f9ed..f7ba18d3d75 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -15,7 +15,10 @@ FAULT_LOG_FILENAME = "home-assistant.log.fault" def validate_os() -> None: """Validate that Home Assistant is running in a supported operating system.""" if not sys.platform.startswith(("darwin", "linux")): - print("Home Assistant only supports Linux, OSX and Windows using WSL") + print( + "Home Assistant only supports Linux, OSX and Windows using WSL", + file=sys.stderr, + ) sys.exit(1) @@ -24,14 +27,15 @@ def validate_python() -> None: if sys.version_info[:3] < REQUIRED_PYTHON_VER: print( "Home Assistant requires at least Python " - f"{REQUIRED_PYTHON_VER[0]}.{REQUIRED_PYTHON_VER[1]}.{REQUIRED_PYTHON_VER[2]}" + f"{REQUIRED_PYTHON_VER[0]}.{REQUIRED_PYTHON_VER[1]}.{REQUIRED_PYTHON_VER[2]}", + file=sys.stderr, ) sys.exit(1) def ensure_config_path(config_dir: str) -> None: """Validate the configuration directory.""" - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from . import config as config_util lib_dir = os.path.join(config_dir, "deps") @@ -39,18 +43,23 @@ def ensure_config_path(config_dir: str) -> None: # Test if configuration directory exists if not os.path.isdir(config_dir): if config_dir != config_util.get_default_config_dir(): + if os.path.exists(config_dir): + reason = "is not a directory" + else: + reason = "does not exist" print( - f"Fatal Error: Specified configuration directory {config_dir} " - "does not exist" + f"Fatal Error: Specified configuration directory {config_dir} {reason}", + file=sys.stderr, ) sys.exit(1) try: os.mkdir(config_dir) - except OSError: + except OSError as ex: print( "Fatal Error: Unable to create default configuration " - f"directory {config_dir}" + f"directory {config_dir}: {ex}", + file=sys.stderr, ) sys.exit(1) @@ -58,14 +67,17 @@ def ensure_config_path(config_dir: str) -> None: if not os.path.isdir(lib_dir): try: os.mkdir(lib_dir) - except OSError: - print(f"Fatal Error: Unable to create library directory {lib_dir}") + except OSError as ex: + print( + f"Fatal Error: Unable to create library directory {lib_dir}: {ex}", + file=sys.stderr, + ) sys.exit(1) def get_arguments() -> argparse.Namespace: """Get parsed passed in arguments.""" - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from . import config as config_util parser = argparse.ArgumentParser( @@ -172,7 +184,7 @@ def main() -> int: validate_os() if args.script is not None: - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from . import scripts return scripts.run(args.script) @@ -180,7 +192,7 @@ def main() -> int: config_dir = os.path.abspath(os.path.join(os.getcwd(), args.config)) ensure_config_path(config_dir) - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from . import runner runtime_conf = runner.RuntimeConfig( diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 966536f446c..5c401570dee 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -5,7 +5,7 @@ import asyncio from collections import OrderedDict from collections.abc import Mapping from datetime import timedelta -from typing import Any, Optional, cast +from typing import Any, cast import jwt @@ -24,7 +24,7 @@ EVENT_USER_UPDATED = "user_updated" EVENT_USER_REMOVED = "user_removed" _MfaModuleDict = dict[str, MultiFactorAuthModule] -_ProviderKey = tuple[str, Optional[str]] +_ProviderKey = tuple[str, str | None] _ProviderDict = dict[_ProviderKey, AuthProvider] diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 5a9fb469e0d..50d5d630429 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -555,7 +555,9 @@ class AuthStore: "client_icon": refresh_token.client_icon, "token_type": refresh_token.token_type, "created_at": refresh_token.created_at.isoformat(), - "access_token_expiration": refresh_token.access_token_expiration.total_seconds(), + "access_token_expiration": ( + refresh_token.access_token_expiration.total_seconds() + ), "token": refresh_token.token, "jwt_key": refresh_token.jwt_key, "last_used_at": refresh_token.last_used_at.isoformat() diff --git a/homeassistant/auth/permissions/types.py b/homeassistant/auth/permissions/types.py index e335ba9f989..0aa8807211a 100644 --- a/homeassistant/auth/permissions/types.py +++ b/homeassistant/auth/permissions/types.py @@ -1,29 +1,28 @@ """Common code for permissions.""" from collections.abc import Mapping -from typing import Union # MyPy doesn't support recursion yet. So writing it out as far as we need. -ValueType = Union[ +ValueType = ( # Example: entities.all = { read: true, control: true } - Mapping[str, bool], - bool, - None, -] + Mapping[str, bool] + | bool + | None +) # Example: entities.domains = { light: … } SubCategoryDict = Mapping[str, ValueType] -SubCategoryType = Union[SubCategoryDict, bool, None] +SubCategoryType = SubCategoryDict | bool | None -CategoryType = Union[ +CategoryType = ( # Example: entities.domains - Mapping[str, SubCategoryType], + Mapping[str, SubCategoryType] # Example: entities.all - Mapping[str, ValueType], - bool, - None, -] + | Mapping[str, ValueType] + | bool + | None +) # Example: { entities: … } PolicyType = Mapping[str, CategoryType] diff --git a/homeassistant/auth/permissions/util.py b/homeassistant/auth/permissions/util.py index 60e68e8b7ca..7a1f102fdf3 100644 --- a/homeassistant/auth/permissions/util.py +++ b/homeassistant/auth/permissions/util.py @@ -3,13 +3,13 @@ from __future__ import annotations from collections.abc import Callable from functools import wraps -from typing import Optional, cast +from typing import cast from .const import SUBCAT_ALL from .models import PermissionLookup from .types import CategoryType, SubCategoryDict, ValueType -LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], Optional[ValueType]] +LookupFunc = Callable[[PermissionLookup, SubCategoryDict, str], ValueType | None] SubCatLookupType = dict[str, LookupFunc] diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index cf142fa1219..a6c4c19b02f 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -14,7 +14,7 @@ from ipaddress import ( ip_address, ip_network, ) -from typing import Any, Union, cast +from typing import Any, cast import voluptuous as vol @@ -27,8 +27,8 @@ from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from .. import InvalidAuthError from ..models import Credentials, RefreshToken, UserMeta -IPAddress = Union[IPv4Address, IPv6Address] -IPNetwork = Union[IPv4Network, IPv6Network] +IPAddress = IPv4Address | IPv6Address +IPNetwork = IPv4Network | IPv6Network CONF_TRUSTED_NETWORKS = "trusted_networks" CONF_TRUSTED_USERS = "trusted_users" diff --git a/homeassistant/block_async_io.py b/homeassistant/block_async_io.py index 29e31ae4a88..753fda5ae9b 100644 --- a/homeassistant/block_async_io.py +++ b/homeassistant/block_async_io.py @@ -8,7 +8,9 @@ from .util.async_ import protect_loop 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[assignment] + HTTPConnection.putrequest = protect_loop( # type: ignore[assignment] + HTTPConnection.putrequest + ) # 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 c8858293706..e821d0de10e 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -346,7 +346,7 @@ def async_enable_logging( if not log_no_color: try: - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from colorlog import ColoredFormatter # basicConfig must be called after importing colorlog in order to @@ -385,7 +385,11 @@ def async_enable_logging( ) threading.excepthook = lambda args: logging.getLogger(None).exception( "Uncaught thread exception", - exc_info=(args.exc_type, args.exc_value, args.exc_traceback), # type: ignore[arg-type] + exc_info=( # type: ignore[arg-type] + args.exc_type, + args.exc_value, + args.exc_traceback, + ), ) # Log errors to a file if we have write access to file or config dir @@ -403,7 +407,10 @@ def async_enable_logging( not err_path_exists and os.access(err_dir, os.W_OK) ): - err_handler: logging.handlers.RotatingFileHandler | logging.handlers.TimedRotatingFileHandler + err_handler: ( + logging.handlers.RotatingFileHandler + | logging.handlers.TimedRotatingFileHandler + ) if log_rotate_days: err_handler = logging.handlers.TimedRotatingFileHandler( err_log_path, when="midnight", backupCount=log_rotate_days @@ -462,7 +469,10 @@ def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]: async def _async_watch_pending_setups(hass: core.HomeAssistant) -> None: - """Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL.""" + """Periodic log of setups that are pending. + + Pending for longer than LOG_SLOW_STARTUP_INTERVAL. + """ loop_count = 0 setup_started: dict[str, datetime] = hass.data[DATA_SETUP_STARTED] previous_was_empty = True diff --git a/homeassistant/brands/amazon.json b/homeassistant/brands/amazon.json index e31bb410457..a7caea2b932 100644 --- a/homeassistant/brands/amazon.json +++ b/homeassistant/brands/amazon.json @@ -1,5 +1,5 @@ { "domain": "amazon", "name": "Amazon", - "integrations": ["alexa", "amazon_polly", "aws", "route53"] + "integrations": ["alexa", "amazon_polly", "aws", "fire_tv", "route53"] } diff --git a/homeassistant/brands/eufy.json b/homeassistant/brands/eufy.json new file mode 100644 index 00000000000..516c3d5c824 --- /dev/null +++ b/homeassistant/brands/eufy.json @@ -0,0 +1,6 @@ +{ + "domain": "eufy", + "name": "eufy", + "integrations": ["eufy", "eufylife_ble"], + "iot_standards": [] +} diff --git a/homeassistant/brands/google.json b/homeassistant/brands/google.json index cceda7505c6..0d396ca05ed 100644 --- a/homeassistant/brands/google.json +++ b/homeassistant/brands/google.json @@ -6,6 +6,7 @@ "google_assistant_sdk", "google_cloud", "google_domains", + "google_mail", "google_maps", "google_pubsub", "google_sheets", diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 092f9d36071..38e88944867 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -3,10 +3,14 @@ from __future__ import annotations from functools import partial -from abodepy import Abode, AbodeAutomation as AbodeAuto -from abodepy.devices import AbodeDevice as AbodeDev -from abodepy.exceptions import AbodeAuthenticationException, AbodeException -import abodepy.helpers.timeline as TIMELINE +from jaraco.abode.automation import Automation as AbodeAuto +from jaraco.abode.client import Client as Abode +from jaraco.abode.devices.base import Device as AbodeDev +from jaraco.abode.exceptions import ( + AuthenticationException as AbodeAuthenticationException, + Exception as AbodeException, +) +from jaraco.abode.helpers.timeline import Groups as GROUPS from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol @@ -26,7 +30,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, entity from homeassistant.helpers.dispatcher import dispatcher_send -from .const import ATTRIBUTION, CONF_POLLING, DEFAULT_CACHEDB, DOMAIN, LOGGER +from .const import ATTRIBUTION, CONF_POLLING, DOMAIN, LOGGER SERVICE_SETTINGS = "change_setting" SERVICE_CAPTURE_IMAGE = "capture_image" @@ -82,7 +86,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] polling = entry.data[CONF_POLLING] - cache = hass.config.path(DEFAULT_CACHEDB) # For previous config entries where unique_id is None if entry.unique_id is None: @@ -92,7 +95,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: abode = await hass.async_add_executor_job( - Abode, username, password, True, True, True, cache + Abode, username, password, True, True, True ) except AbodeAuthenticationException as ex: @@ -225,17 +228,17 @@ def setup_abode_events(hass: HomeAssistant) -> None: hass.bus.fire(event, data) events = [ - TIMELINE.ALARM_GROUP, - TIMELINE.ALARM_END_GROUP, - TIMELINE.PANEL_FAULT_GROUP, - TIMELINE.PANEL_RESTORE_GROUP, - TIMELINE.AUTOMATION_GROUP, - TIMELINE.DISARM_GROUP, - TIMELINE.ARM_GROUP, - TIMELINE.ARM_FAULT_GROUP, - TIMELINE.TEST_GROUP, - TIMELINE.CAPTURE_GROUP, - TIMELINE.DEVICE_GROUP, + GROUPS.ALARM, + GROUPS.ALARM_END, + GROUPS.PANEL_FAULT, + GROUPS.PANEL_RESTORE, + GROUPS.AUTOMATION, + GROUPS.DISARM, + GROUPS.ARM, + GROUPS.ARM_FAULT, + GROUPS.TEST, + GROUPS.CAPTURE, + GROUPS.DEVICE, ] for event in events: diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index c09a8ebd811..2546f762912 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -1,7 +1,7 @@ """Support for Abode Security System alarm control panels.""" from __future__ import annotations -from abodepy.devices.alarm import AbodeAlarm as AbodeAl +from jaraco.abode.devices.alarm import Alarm as AbodeAl import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import AlarmControlPanelEntityFeature diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index 08ed1925936..60a09e13bef 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -4,8 +4,8 @@ from __future__ import annotations from contextlib import suppress from typing import cast -from abodepy.devices.binary_sensor import AbodeBinarySensor as ABBinarySensor -import abodepy.helpers.constants as CONST +from jaraco.abode.devices.sensor import BinarySensor as ABBinarySensor +from jaraco.abode.helpers import constants as CONST from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index c4c2d0dc78d..17d7b820d45 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -4,9 +4,9 @@ from __future__ import annotations from datetime import timedelta from typing import Any, cast -from abodepy.devices import CONST, AbodeDevice as AbodeDev -from abodepy.devices.camera import AbodeCamera as AbodeCam -import abodepy.helpers.timeline as TIMELINE +from jaraco.abode.devices.base import Device as AbodeDev +from jaraco.abode.devices.camera import Camera as AbodeCam +from jaraco.abode.helpers import constants as CONST, timeline as TIMELINE import requests from requests.models import Response @@ -30,7 +30,7 @@ async def async_setup_entry( data: AbodeSystem = hass.data[DOMAIN] async_add_entities( - AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE) + AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE) # pylint: disable=no-member for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA) ) diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py index 4c3d44bebbe..56cd673bc1b 100644 --- a/homeassistant/components/abode/config_flow.py +++ b/homeassistant/components/abode/config_flow.py @@ -5,9 +5,12 @@ from collections.abc import Mapping from http import HTTPStatus from typing import Any, cast -from abodepy import Abode -from abodepy.exceptions import AbodeAuthenticationException, AbodeException -from abodepy.helpers.errors import MFA_CODE_REQUIRED +from jaraco.abode.client import Client as Abode +from jaraco.abode.exceptions import ( + AuthenticationException as AbodeAuthenticationException, + Exception as AbodeException, +) +from jaraco.abode.helpers.errors import MFA_CODE_REQUIRED from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol @@ -15,7 +18,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult -from .const import CONF_POLLING, DEFAULT_CACHEDB, DOMAIN, LOGGER +from .const import CONF_POLLING, DOMAIN, LOGGER CONF_MFA = "mfa_code" @@ -35,7 +38,6 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required(CONF_MFA): str, } - self._cache: str | None = None self._mfa_code: str | None = None self._password: str | None = None self._polling: bool = False @@ -43,12 +45,11 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _async_abode_login(self, step_id: str) -> FlowResult: """Handle login with Abode.""" - self._cache = self.hass.config.path(DEFAULT_CACHEDB) errors = {} try: await self.hass.async_add_executor_job( - Abode, self._username, self._password, True, False, False, self._cache + Abode, self._username, self._password, True, False, False ) except AbodeException as ex: @@ -77,12 +78,7 @@ class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle multi-factor authentication (MFA) login with Abode.""" try: # Create instance to access login method for passing MFA code - abode = Abode( - auto_login=False, - get_devices=False, - get_automations=False, - cache_path=self._cache, - ) + abode = Abode(auto_login=False, get_devices=False, get_automations=False) await self.hass.async_add_executor_job( abode.login, self._username, self._password, self._mfa_code ) diff --git a/homeassistant/components/abode/const.py b/homeassistant/components/abode/const.py index e6b048059a1..e24fe066823 100644 --- a/homeassistant/components/abode/const.py +++ b/homeassistant/components/abode/const.py @@ -6,5 +6,4 @@ LOGGER = logging.getLogger(__package__) DOMAIN = "abode" ATTRIBUTION = "Data provided by goabode.com" -DEFAULT_CACHEDB = "abodepy_cache.pickle" CONF_POLLING = "polling" diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index b48f00209ec..507b1284362 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -1,8 +1,8 @@ """Support for Abode Security System covers.""" from typing import Any -from abodepy.devices.cover import AbodeCover as AbodeCV -import abodepy.helpers.constants as CONST +from jaraco.abode.devices.cover import Cover as AbodeCV +from jaraco.abode.helpers import constants as CONST from homeassistant.components.cover import CoverEntity from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index b930c3d654b..be69897431f 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -4,8 +4,8 @@ from __future__ import annotations from math import ceil from typing import Any -from abodepy.devices.light import AbodeLight as AbodeLT -import abodepy.helpers.constants as CONST +from jaraco.abode.devices.light import Light as AbodeLT +from jaraco.abode.helpers import constants as CONST from homeassistant.components.light import ( ATTR_BRIGHTNESS, diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index 12258a45aaf..039b2423099 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -1,8 +1,8 @@ """Support for the Abode Security System locks.""" from typing import Any -from abodepy.devices.lock import AbodeLock as AbodeLK -import abodepy.helpers.constants as CONST +from jaraco.abode.devices.lock import Lock as AbodeLK +from jaraco.abode.helpers import constants as CONST from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index 07fcfe6cb74..6045f8797b4 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -3,11 +3,11 @@ "name": "Abode", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/abode", - "requirements": ["abodepy==1.2.0"], + "requirements": ["jaraco.abode==3.2.1"], "codeowners": ["@shred86"], "homekit": { "models": ["Abode", "Iota"] }, "iot_class": "cloud_push", - "loggers": ["abodepy", "lomond"] + "loggers": ["jaraco.abode", "lomond"] } diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index 7fd3a0280a1..87a9f8e9a27 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -3,7 +3,8 @@ from __future__ import annotations from typing import cast -from abodepy.devices.sensor import CONST, AbodeSensor as AbodeSense +from jaraco.abode.devices.sensor import Sensor as AbodeSense +from jaraco.abode.helpers import constants as CONST from homeassistant.components.sensor import ( SensorDeviceClass, diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index f472a5028c0..ab83e3a20c1 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -3,7 +3,8 @@ from __future__ import annotations from typing import Any, cast -from abodepy.devices.switch import CONST, AbodeSwitch as AbodeSW +from jaraco.abode.devices.switch import Switch as AbodeSW +from jaraco.abode.helpers import constants as CONST from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry diff --git a/homeassistant/components/abode/translations/lt.json b/homeassistant/components/abode/translations/lt.json new file mode 100644 index 00000000000..883b5c03e2c --- /dev/null +++ b/homeassistant/components/abode/translations/lt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis", + "username": "El. pa\u0161tas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/diagnostics.py b/homeassistant/components/accuweather/diagnostics.py index e4305eb747a..f307c6b5335 100644 --- a/homeassistant/components/accuweather/diagnostics.py +++ b/homeassistant/components/accuweather/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for AccuWeather.""" 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_API_KEY, CONF_LATITUDE, CONF_LONGITUDE @@ -14,7 +16,7 @@ TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE} async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: AccuWeatherDataUpdateCoordinator = hass.data[DOMAIN][ config_entry.entry_id diff --git a/homeassistant/components/accuweather/manifest.json b/homeassistant/components/accuweather/manifest.json index 5bda281ff3c..e9f3505feed 100644 --- a/homeassistant/components/accuweather/manifest.json +++ b/homeassistant/components/accuweather/manifest.json @@ -2,7 +2,7 @@ "domain": "accuweather", "name": "AccuWeather", "documentation": "https://www.home-assistant.io/integrations/accuweather/", - "requirements": ["accuweather==0.4.0"], + "requirements": ["accuweather==0.5.0"], "codeowners": ["@bieniu"], "config_flow": true, "quality_scale": "platinum", diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 9fd80362320..6991f5c872a 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -17,10 +17,10 @@ from homeassistant.const import ( PERCENTAGE, UV_INDEX, UnitOfLength, - UnitOfPrecipitationDepth, UnitOfSpeed, UnitOfTemperature, UnitOfTime, + UnitOfVolumetricFlux, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -290,11 +290,11 @@ SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = ( ), AccuWeatherSensorDescription( key="Precipitation", - device_class=SensorDeviceClass.PRECIPITATION, + device_class=SensorDeviceClass.PRECIPITATION_INTENSITY, name="Precipitation", state_class=SensorStateClass.MEASUREMENT, - metric_unit=UnitOfPrecipitationDepth.MILLIMETERS, - us_customary_unit=UnitOfPrecipitationDepth.INCHES, + metric_unit=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, + us_customary_unit=UnitOfVolumetricFlux.INCHES_PER_HOUR, value_fn=lambda data, unit: cast(float, data[unit][ATTR_VALUE]), attr_fn=lambda data: {"type": data["PrecipitationType"]}, ), @@ -452,7 +452,7 @@ def _get_sensor_data( return sensors[ATTR_FORECAST][forecast_day][kind] if kind == "Precipitation": - return sensors["PrecipitationSummary"][kind] + return sensors["PrecipitationSummary"]["PastHour"] return sensors[kind] diff --git a/homeassistant/components/accuweather/translations/tr.json b/homeassistant/components/accuweather/translations/tr.json index c7049160868..98285f089d3 100644 --- a/homeassistant/components/accuweather/translations/tr.json +++ b/homeassistant/components/accuweather/translations/tr.json @@ -22,6 +22,27 @@ } } }, + "entity": { + "sensor": { + "pressure_tendency": { + "state": { + "falling": "D\u00fc\u015f\u00fcyor", + "rising": "Y\u00fckseliyor", + "steady": "Sabit" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "forecast": "Hava Durumu tahmini" + }, + "description": "AccuWeather API anahtar\u0131n\u0131n \u00fccretsiz s\u00fcr\u00fcm\u00fcn\u00fcn s\u0131n\u0131rlamalar\u0131 nedeniyle, hava tahminini etkinle\u015ftirdi\u011finizde, veri g\u00fcncellemeleri her 40 dakikada bir yerine 80 dakikada bir ger\u00e7ekle\u015ftirilir." + } + } + }, "system_health": { "info": { "can_reach_server": "AccuWeather sunucusuna ula\u015f\u0131n", diff --git a/homeassistant/components/adax/translations/lv.json b/homeassistant/components/adax/translations/lv.json index 3ae3e819b7e..f0b80081a87 100644 --- a/homeassistant/components/adax/translations/lv.json +++ b/homeassistant/components/adax/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "step": { "cloud": { "data": { diff --git a/homeassistant/components/adax/translations/uk.json b/homeassistant/components/adax/translations/uk.json new file mode 100644 index 00000000000..78a6575f94c --- /dev/null +++ b/homeassistant/components/adax/translations/uk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + }, + "step": { + "cloud": { + "data": { + "account_id": "ID \u041e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/entity.py b/homeassistant/components/adguard/entity.py index 7d6bf099366..3a60ad4e8b1 100644 --- a/homeassistant/components/adguard/entity.py +++ b/homeassistant/components/adguard/entity.py @@ -58,7 +58,12 @@ class AdGuardHomeEntity(Entity): return DeviceInfo( entry_type=DeviceEntryType.SERVICE, identifiers={ - (DOMAIN, self.adguard.host, self.adguard.port, self.adguard.base_path) # type: ignore[arg-type] + ( # type: ignore[arg-type] + DOMAIN, + self.adguard.host, + self.adguard.port, + self.adguard.base_path, + ) }, manufacturer="AdGuard Team", name="AdGuard Home", diff --git a/homeassistant/components/adguard/translations/lt.json b/homeassistant/components/adguard/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/adguard/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/advantage_air/translations/lv.json b/homeassistant/components/advantage_air/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/advantage_air/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aemet/translations/el.json b/homeassistant/components/aemet/translations/el.json index a1aa6fa7ba2..1f11c98ec0b 100644 --- a/homeassistant/components/aemet/translations/el.json +++ b/homeassistant/components/aemet/translations/el.json @@ -14,7 +14,7 @@ "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" + "description": "\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://opendata.aemet.es/centrodedescargas/altaUsuario" } } }, diff --git a/homeassistant/components/agent_dvr/translations/lv.json b/homeassistant/components/agent_dvr/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index a3feab1e6f8..56ec8eae95b 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -150,7 +150,9 @@ class AirlyDataUpdateCoordinator(DataUpdateCoordinator): """Initialize.""" self.latitude = latitude self.longitude = longitude - self.airly = Airly(api_key, session) + # Currently, Airly only supports Polish and English + language = "pl" if hass.config.language == "pl" else "en" + self.airly = Airly(api_key, session, language=language) self.use_nearest = use_nearest super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index 10e4990ee05..5d41116eaa1 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -46,7 +46,7 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input["longitude"], ) if not location_point_valid: - await test_location( + location_nearest_valid = await test_location( websession, user_input["api_key"], user_input["latitude"], @@ -60,6 +60,8 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "wrong_location" else: if not location_point_valid: + if not location_nearest_valid: + return self.async_abort(reason="wrong_location") use_nearest = True return self.async_create_entry( title=user_input[CONF_NAME], diff --git a/homeassistant/components/airly/diagnostics.py b/homeassistant/components/airly/diagnostics.py index 2471ba90eea..bb270e6a664 100644 --- a/homeassistant/components/airly/diagnostics.py +++ b/homeassistant/components/airly/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Airly.""" 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 ( @@ -19,7 +21,7 @@ TO_REDACT = {CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_UNIQUE_ID} async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: AirlyDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json index 77f965b64c2..4f95f26afc0 100644 --- a/homeassistant/components/airly/strings.json +++ b/homeassistant/components/airly/strings.json @@ -16,7 +16,8 @@ "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_location%]", + "wrong_location": "No Airly measuring stations in this area." } }, "system_health": { diff --git a/homeassistant/components/airly/translations/ca.json b/homeassistant/components/airly/translations/ca.json index 36d7aaf8190..5ce526cdb7b 100644 --- a/homeassistant/components/airly/translations/ca.json +++ b/homeassistant/components/airly/translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada", + "wrong_location": "No hi ha estacions de mesura Airly en aquesta zona." }, "error": { "invalid_api_key": "Clau API inv\u00e0lida", diff --git a/homeassistant/components/airly/translations/de.json b/homeassistant/components/airly/translations/de.json index fb76155e76a..a8a47649631 100644 --- a/homeassistant/components/airly/translations/de.json +++ b/homeassistant/components/airly/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Standort ist bereits konfiguriert" + "already_configured": "Standort ist bereits konfiguriert", + "wrong_location": "Keine Airly Luftmessstation an diesem Ort" }, "error": { "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", diff --git a/homeassistant/components/airly/translations/el.json b/homeassistant/components/airly/translations/el.json index e8d61743523..8dfa7e3c5e1 100644 --- a/homeassistant/components/airly/translations/el.json +++ b/homeassistant/components/airly/translations/el.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0397 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af" + "already_configured": "\u0397 \u03c4\u03bf\u03c0\u03bf\u03b8\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "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\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03c0\u03b5\u03c1\u03b9\u03bf\u03c7\u03ae." }, "error": { "invalid_api_key": "\u0386\u03ba\u03c5\u03c1\u03bf API \u03ba\u03bb\u03b5\u03b9\u03b4\u03af", @@ -15,7 +16,7 @@ "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" + "description": "\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" } } }, diff --git a/homeassistant/components/airly/translations/en.json b/homeassistant/components/airly/translations/en.json index 1dee608b1da..d6cbcaa28ca 100644 --- a/homeassistant/components/airly/translations/en.json +++ b/homeassistant/components/airly/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Location is already configured" + "already_configured": "Location is already configured", + "wrong_location": "No Airly measuring stations in this area." }, "error": { "invalid_api_key": "Invalid API key", diff --git a/homeassistant/components/airly/translations/es.json b/homeassistant/components/airly/translations/es.json index efe0228e715..1f56042ce82 100644 --- a/homeassistant/components/airly/translations/es.json +++ b/homeassistant/components/airly/translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada" + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada", + "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta \u00e1rea." }, "error": { "invalid_api_key": "Clave API no v\u00e1lida", diff --git a/homeassistant/components/airly/translations/et.json b/homeassistant/components/airly/translations/et.json index ea17ec01ea6..ef94652e4ec 100644 --- a/homeassistant/components/airly/translations/et.json +++ b/homeassistant/components/airly/translations/et.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Asukoht on juba m\u00e4\u00e4ratud" + "already_configured": "Asukoht on juba m\u00e4\u00e4ratud", + "wrong_location": "Selles piirkonnas pole Airly m\u00f5\u00f5tejaamu." }, "error": { "invalid_api_key": "Vigane API v\u00f5ti", diff --git a/homeassistant/components/airly/translations/hu.json b/homeassistant/components/airly/translations/hu.json index 6955c2456cc..5894387a0b8 100644 --- a/homeassistant/components/airly/translations/hu.json +++ b/homeassistant/components/airly/translations/hu.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "A hely m\u00e1r konfigur\u00e1lva van" + "already_configured": "A hely m\u00e1r konfigur\u00e1lva van", + "wrong_location": "Ezen a ter\u00fcleten nincs Airly m\u00e9r\u0151\u00e1llom\u00e1s." }, "error": { "invalid_api_key": "\u00c9rv\u00e9nytelen API kulcs", diff --git a/homeassistant/components/airly/translations/id.json b/homeassistant/components/airly/translations/id.json index 7e2d7e3930e..f32e4f55360 100644 --- a/homeassistant/components/airly/translations/id.json +++ b/homeassistant/components/airly/translations/id.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Lokasi sudah dikonfigurasi" + "already_configured": "Lokasi sudah dikonfigurasi", + "wrong_location": "Tidak ada stasiun pengukur Airly di daerah ini." }, "error": { "invalid_api_key": "Kunci API tidak valid", diff --git a/homeassistant/components/airly/translations/it.json b/homeassistant/components/airly/translations/it.json index d57023faeca..8436cdd7504 100644 --- a/homeassistant/components/airly/translations/it.json +++ b/homeassistant/components/airly/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "La posizione \u00e8 gi\u00e0 configurata" + "already_configured": "La posizione \u00e8 gi\u00e0 configurata", + "wrong_location": "Nessuna stazione di misurazione Airly in quest'area." }, "error": { "invalid_api_key": "Chiave API non valida", diff --git a/homeassistant/components/airly/translations/nl.json b/homeassistant/components/airly/translations/nl.json index 9bfce7e7708..b888e60b224 100644 --- a/homeassistant/components/airly/translations/nl.json +++ b/homeassistant/components/airly/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Locatie is al geconfigureerd" + "already_configured": "Locatie is al geconfigureerd", + "wrong_location": "Geen Airly meetstations in deze ruimte." }, "error": { "invalid_api_key": "Ongeldige API-sleutel", diff --git a/homeassistant/components/airly/translations/no.json b/homeassistant/components/airly/translations/no.json index f4f87d6d887..cd7c9614352 100644 --- a/homeassistant/components/airly/translations/no.json +++ b/homeassistant/components/airly/translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Plasseringen er allerede konfigurert" + "already_configured": "Plasseringen er allerede konfigurert", + "wrong_location": "Ingen Airly m\u00e5lestasjoner i dette omr\u00e5det." }, "error": { "invalid_api_key": "Ugyldig API-n\u00f8kkel", diff --git a/homeassistant/components/airly/translations/pl.json b/homeassistant/components/airly/translations/pl.json index 9ae6243d2fb..6e7b012fbc3 100644 --- a/homeassistant/components/airly/translations/pl.json +++ b/homeassistant/components/airly/translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana", + "wrong_location": "Brak stacji pomiarowych Airly w tym rejonie" }, "error": { "invalid_api_key": "Nieprawid\u0142owy klucz API", diff --git a/homeassistant/components/airly/translations/pt-BR.json b/homeassistant/components/airly/translations/pt-BR.json index fe19bc8b3a7..c007d7723a3 100644 --- a/homeassistant/components/airly/translations/pt-BR.json +++ b/homeassistant/components/airly/translations/pt-BR.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + "already_configured": "Localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada", + "wrong_location": "Nenhuma esta\u00e7\u00e3o de medi\u00e7\u00e3o Airly nesta \u00e1rea." }, "error": { "invalid_api_key": "Chave de API inv\u00e1lida", diff --git a/homeassistant/components/airly/translations/ru.json b/homeassistant/components/airly/translations/ru.json index 4b159f1e50d..059374618f2 100644 --- a/homeassistant/components/airly/translations/ru.json +++ b/homeassistant/components/airly/translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "wrong_location": "\u0412 \u044d\u0442\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u043d\u0435\u0442 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0441\u0442\u0430\u043d\u0446\u0438\u0439 Airly." }, "error": { "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", diff --git a/homeassistant/components/airly/translations/sk.json b/homeassistant/components/airly/translations/sk.json index 0c8474a0725..a9b2482e495 100644 --- a/homeassistant/components/airly/translations/sk.json +++ b/homeassistant/components/airly/translations/sk.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9" + "already_configured": "Umiestnenie u\u017e je nakonfigurovan\u00e9", + "wrong_location": "V tejto oblasti nie s\u00fa \u017eiadne meracie stanice Airly." }, "error": { "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d", diff --git a/homeassistant/components/airly/translations/tr.json b/homeassistant/components/airly/translations/tr.json index 989179b73d8..a6aa6845700 100644 --- a/homeassistant/components/airly/translations/tr.json +++ b/homeassistant/components/airly/translations/tr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "wrong_location": "Bu b\u00f6lgede Airly \u00f6l\u00e7\u00fcm istasyonu yok." }, "error": { "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", diff --git a/homeassistant/components/airly/translations/zh-Hant.json b/homeassistant/components/airly/translations/zh-Hant.json index 9973269facd..dd5e88c0f7c 100644 --- a/homeassistant/components/airly/translations/zh-Hant.json +++ b/homeassistant/components/airly/translations/zh-Hant.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "wrong_location": "\u8a72\u5340\u57df\u6c92\u6709 Arily \u76e3\u6e2c\u7ad9\u3002" }, "error": { "invalid_api_key": "API \u91d1\u9470\u7121\u6548", diff --git a/homeassistant/components/airnow/translations/el.json b/homeassistant/components/airnow/translations/el.json index c8e97c5a925..f202bc98928 100644 --- a/homeassistant/components/airnow/translations/el.json +++ b/homeassistant/components/airnow/translations/el.json @@ -17,7 +17,7 @@ "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/" + "description": "\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://docs.airnowapi.org/account/request/" } } } diff --git a/homeassistant/components/airnow/translations/lv.json b/homeassistant/components/airnow/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/airnow/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/lv.json b/homeassistant/components/airq/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/airq/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/sv.json b/homeassistant/components/airq/translations/sv.json new file mode 100644 index 00000000000..b8e08724788 --- /dev/null +++ b/homeassistant/components/airq/translations/sv.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ip_address": "IP-adress", + "password": "L\u00f6senord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/tr.json b/homeassistant/components/airq/translations/tr.json new file mode 100644 index 00000000000..02624666136 --- /dev/null +++ b/homeassistant/components/airq/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_input": "Ge\u00e7ersiz ana bilgisayar ad\u0131 veya IP adresi" + }, + "step": { + "user": { + "data": { + "ip_address": "IP Adresi", + "password": "Parola" + }, + "description": "Cihaz\u0131n IP adresini veya mDNS'sini ve \u015fifresini sa\u011flay\u0131n", + "title": "Cihaz\u0131 tan\u0131mlay\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airq/translations/uk.json b/homeassistant/components/airq/translations/uk.json new file mode 100644 index 00000000000..5c722c2a338 --- /dev/null +++ b/homeassistant/components/airq/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/manifest.json b/homeassistant/components/airthings_ble/manifest.json index 422a51c7187..6321f8e961c 100644 --- a/homeassistant/components/airthings_ble/manifest.json +++ b/homeassistant/components/airthings_ble/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airthings_ble", "requirements": ["airthings-ble==0.5.3"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@vincegio"], "iot_class": "local_polling", "bluetooth": [ diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py index 57a8c3827d2..641b0b02f9f 100644 --- a/homeassistant/components/airthings_ble/sensor.py +++ b/homeassistant/components/airthings_ble/sensor.py @@ -154,7 +154,7 @@ class AirthingsSensor( def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[AirthingsDevice], airthings_device: AirthingsDevice, entity_description: SensorEntityDescription, ) -> None: diff --git a/homeassistant/components/airthings_ble/translations/cs.json b/homeassistant/components/airthings_ble/translations/cs.json new file mode 100644 index 00000000000..3b814303e69 --- /dev/null +++ b/homeassistant/components/airthings_ble/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/airthings_ble/translations/lv.json b/homeassistant/components/airthings_ble/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/airthings_ble/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airthings_ble/translations/tr.json b/homeassistant/components/airthings_ble/translations/tr.json index 9854002de33..76fe7b51700 100644 --- a/homeassistant/components/airthings_ble/translations/tr.json +++ b/homeassistant/components/airthings_ble/translations/tr.json @@ -10,13 +10,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/airtouch4/translations/el.json b/homeassistant/components/airtouch4/translations/el.json index 8790c6edab4..69645e39a65 100644 --- a/homeassistant/components/airtouch4/translations/el.json +++ b/homeassistant/components/airtouch4/translations/el.json @@ -12,7 +12,7 @@ "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}." + "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 AirTouch 4." } } } diff --git a/homeassistant/components/airtouch4/translations/lv.json b/homeassistant/components/airtouch4/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/airtouch4/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 16579be9a72..793b7879270 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -61,7 +61,6 @@ DOMAIN_AIRVISUAL_PRO = "airvisual_pro" PLATFORMS = [Platform.SENSOR] DEFAULT_ATTRIBUTION = "Data provided by AirVisual" -DEFAULT_NODE_PRO_UPDATE_INTERVAL = timedelta(minutes=1) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) @@ -267,8 +266,12 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, - context={"source": source}, - data={CONF_API_KEY: entry.data[CONF_API_KEY], **geography}, + context={"source": SOURCE_IMPORT}, + data={ + "import_source": source, + CONF_API_KEY: entry.data[CONF_API_KEY], + **geography, + }, ) ) diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 57d28ab0e87..27e79f2d40b 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -132,15 +132,14 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): LOGGER.error(err) errors["base"] = "unknown" + if errors: + return self.async_show_form( + step_id=error_step, data_schema=error_schema, errors=errors + ) + valid_keys.add(user_input[CONF_API_KEY]) - if errors: - return self.async_show_form( - step_id=error_step, data_schema=error_schema, errors=errors - ) - - existing_entry = await self.async_set_unique_id(self._geo_id) - if existing_entry: + if existing_entry := await self.async_set_unique_id(self._geo_id): self.hass.config_entries.async_update_entry(existing_entry, data=user_input) self.hass.async_create_task( self.hass.config_entries.async_reload(existing_entry.entry_id) @@ -172,6 +171,13 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Define the config flow to handle options.""" return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) + async def async_step_import(self, import_data: dict[str, str]) -> FlowResult: + """Handle import of config entry version 1 data.""" + import_source = import_data.pop("import_source") + if import_source == "geography_by_coords": + return await self.async_step_geography_by_coords(import_data) + return await self.async_step_geography_by_name(import_data) + async def async_step_geography_by_coords( self, user_input: dict[str, str] | None = None ) -> FlowResult: diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index cfb9b67ff38..1f0c5aa1baa 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -19,11 +19,8 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_SHOW_ON_MAP, CONF_STATE, - PERCENTAGE, - UnitOfTemperature, ) 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 @@ -37,17 +34,8 @@ ATTR_POLLUTANT_UNIT = "pollutant_unit" ATTR_REGION = "region" SENSOR_KIND_AQI = "air_quality_index" -SENSOR_KIND_BATTERY_LEVEL = "battery_level" -SENSOR_KIND_CO2 = "carbon_dioxide" -SENSOR_KIND_HUMIDITY = "humidity" SENSOR_KIND_LEVEL = "air_pollution_level" -SENSOR_KIND_PM_0_1 = "particulate_matter_0_1" -SENSOR_KIND_PM_1_0 = "particulate_matter_1_0" -SENSOR_KIND_PM_2_5 = "particulate_matter_2_5" SENSOR_KIND_POLLUTANT = "main_pollutant" -SENSOR_KIND_SENSOR_LIFE = "sensor_life" -SENSOR_KIND_TEMPERATURE = "temperature" -SENSOR_KIND_VOC = "voc" GEOGRAPHY_SENSOR_DESCRIPTIONS = ( SensorEntityDescription( @@ -82,70 +70,6 @@ GEOGRAPHY_SENSOR_DESCRIPTIONS = ( ) GEOGRAPHY_SENSOR_LOCALES = {"cn": "Chinese", "us": "U.S."} -NODE_PRO_SENSOR_DESCRIPTIONS = ( - SensorEntityDescription( - key=SENSOR_KIND_AQI, - name="Air quality index", - device_class=SensorDeviceClass.AQI, - native_unit_of_measurement="AQI", - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_KIND_BATTERY_LEVEL, - name="Battery", - device_class=SensorDeviceClass.BATTERY, - entity_category=EntityCategory.DIAGNOSTIC, - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key=SENSOR_KIND_CO2, - name="C02", - device_class=SensorDeviceClass.CO2, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_KIND_HUMIDITY, - name="Humidity", - device_class=SensorDeviceClass.HUMIDITY, - native_unit_of_measurement=PERCENTAGE, - ), - SensorEntityDescription( - key=SENSOR_KIND_PM_0_1, - name="PM 0.1", - device_class=SensorDeviceClass.PM1, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_KIND_PM_1_0, - name="PM 1.0", - device_class=SensorDeviceClass.PM10, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_KIND_PM_2_5, - name="PM 2.5", - device_class=SensorDeviceClass.PM25, - native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_KIND_TEMPERATURE, - name="Temperature", - device_class=SensorDeviceClass.TEMPERATURE, - native_unit_of_measurement=UnitOfTemperature.CELSIUS, - state_class=SensorStateClass.MEASUREMENT, - ), - SensorEntityDescription( - key=SENSOR_KIND_VOC, - name="VOC", - device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, - native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, - state_class=SensorStateClass.MEASUREMENT, - ), -) STATE_POLLUTANT_LABEL_CO = "co" STATE_POLLUTANT_LABEL_N2 = "n2" diff --git a/homeassistant/components/airvisual/translations/bg.json b/homeassistant/components/airvisual/translations/bg.json index b01f61640bd..af64d092f73 100644 --- a/homeassistant/components/airvisual/translations/bg.json +++ b/homeassistant/components/airvisual/translations/bg.json @@ -22,12 +22,6 @@ "country": "\u0421\u0442\u0440\u0430\u043d\u0430" } }, - "node_pro": { - "data": { - "ip_address": "\u0425\u043e\u0441\u0442", - "password": "\u041f\u0430\u0440\u043e\u043b\u0430" - } - }, "reauth_confirm": { "data": { "api_key": "API \u043a\u043b\u044e\u0447" diff --git a/homeassistant/components/airvisual/translations/ca.json b/homeassistant/components/airvisual/translations/ca.json index bd2fabdebe4..f4613a6f890 100644 --- a/homeassistant/components/airvisual/translations/ca.json +++ b/homeassistant/components/airvisual/translations/ca.json @@ -30,14 +30,6 @@ "description": "Utilitza l'API d'AirVisual per monitoritzar un/a ciutat/estat/pa\u00eds", "title": "Configura una ubicaci\u00f3 geogr\u00e0fica" }, - "node_pro": { - "data": { - "ip_address": "Amfitri\u00f3", - "password": "Contrasenya" - }, - "description": "Monitoritza una unitat personal d'AirVisual. Pots obtenir la contrasenya des de la interf\u00edcie d'usuari (UI) de la unitat.", - "title": "Configuraci\u00f3 d'AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "Clau API" diff --git a/homeassistant/components/airvisual/translations/cs.json b/homeassistant/components/airvisual/translations/cs.json index ba9a28bfc87..422983e9b8f 100644 --- a/homeassistant/components/airvisual/translations/cs.json +++ b/homeassistant/components/airvisual/translations/cs.json @@ -24,13 +24,6 @@ "country": "Zem\u011b" } }, - "node_pro": { - "data": { - "ip_address": "Hostitel", - "password": "Heslo" - }, - "title": "Nastaven\u00ed AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "Kl\u00ed\u010d API" diff --git a/homeassistant/components/airvisual/translations/de.json b/homeassistant/components/airvisual/translations/de.json index 8512e797cfd..f5a8abf3cf5 100644 --- a/homeassistant/components/airvisual/translations/de.json +++ b/homeassistant/components/airvisual/translations/de.json @@ -30,14 +30,6 @@ "description": "Verwende die AirVisual Cloud API, um ein(e) Stadt/Bundesland/Land zu \u00fcberwachen.", "title": "Konfiguriere einen Standort" }, - "node_pro": { - "data": { - "ip_address": "Host", - "password": "Passwort" - }, - "description": "\u00dcberwache eine pers\u00f6nliche AirVisual-Einheit. Das Passwort kann von der Benutzeroberfl\u00e4che des Ger\u00e4ts abgerufen werden.", - "title": "Konfiguriere einen AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "API-Schl\u00fcssel" diff --git a/homeassistant/components/airvisual/translations/el.json b/homeassistant/components/airvisual/translations/el.json index 8b3e8a4ef9a..9018ee5b844 100644 --- a/homeassistant/components/airvisual/translations/el.json +++ b/homeassistant/components/airvisual/translations/el.json @@ -1,7 +1,7 @@ { "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.", + "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": { @@ -30,14 +30,6 @@ "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" diff --git a/homeassistant/components/airvisual/translations/en.json b/homeassistant/components/airvisual/translations/en.json index 78ed599babb..f6dce1f87ab 100644 --- a/homeassistant/components/airvisual/translations/en.json +++ b/homeassistant/components/airvisual/translations/en.json @@ -30,14 +30,6 @@ "description": "Use the AirVisual cloud API to monitor a city/state/country.", "title": "Configure a Geography" }, - "node_pro": { - "data": { - "ip_address": "Host", - "password": "Password" - }, - "description": "Monitor a personal AirVisual unit. The password can be retrieved from the unit's UI.", - "title": "Configure an AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "API Key" diff --git a/homeassistant/components/airvisual/translations/es-419.json b/homeassistant/components/airvisual/translations/es-419.json index 6e26be959f9..2cfd07f31ca 100644 --- a/homeassistant/components/airvisual/translations/es-419.json +++ b/homeassistant/components/airvisual/translations/es-419.json @@ -22,14 +22,6 @@ "description": "Utilice la API en la nube de AirVisual para monitorear una ciudad/estado/pa\u00eds.", "title": "Configurar una geograf\u00eda" }, - "node_pro": { - "data": { - "ip_address": "Direcci\u00f3n IP/nombre de host de la unidad", - "password": "Contrase\u00f1a de la unidad" - }, - "description": "Monitoree una unidad AirVisual personal. La contrase\u00f1a se puede recuperar de la interfaz de usuario de la unidad.", - "title": "Configurar un AirVisual Node/Pro" - }, "reauth_confirm": { "title": "Vuelva a autenticar AirVisual" }, diff --git a/homeassistant/components/airvisual/translations/es.json b/homeassistant/components/airvisual/translations/es.json index 25c76c32565..c835e1bc29e 100644 --- a/homeassistant/components/airvisual/translations/es.json +++ b/homeassistant/components/airvisual/translations/es.json @@ -30,14 +30,6 @@ "description": "Usar la API de la nube de AirVisual para supervisar una ciudad/estado/pa\u00eds.", "title": "Configurar una geograf\u00eda" }, - "node_pro": { - "data": { - "ip_address": "Host", - "password": "Contrase\u00f1a" - }, - "description": "Supervisar una unidad AirVisual personal. La contrase\u00f1a se puede recuperar desde la IU de la unidad.", - "title": "Configurar un AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "Clave API" diff --git a/homeassistant/components/airvisual/translations/et.json b/homeassistant/components/airvisual/translations/et.json index c685eaf77ab..2000bbc65c1 100644 --- a/homeassistant/components/airvisual/translations/et.json +++ b/homeassistant/components/airvisual/translations/et.json @@ -30,14 +30,6 @@ "description": "Kasuta AirVisual pilve API-t linna/osariigi/riigi j\u00e4lgimiseks.", "title": "Seadista Geography sidumine" }, - "node_pro": { - "data": { - "ip_address": "\u00dcksuse IP-aadress / hostinimi", - "password": "Salas\u00f5na" - }, - "description": "J\u00e4lgige isiklikku AirVisual-seadet. Parooli saab hankida seadme kasutajaliidese kaudu.", - "title": "Seadistage AirVisual Node / Pro" - }, "reauth_confirm": { "data": { "api_key": "API v\u00f5ti" diff --git a/homeassistant/components/airvisual/translations/fi.json b/homeassistant/components/airvisual/translations/fi.json index 044d7688551..e962fea7180 100644 --- a/homeassistant/components/airvisual/translations/fi.json +++ b/homeassistant/components/airvisual/translations/fi.json @@ -2,13 +2,6 @@ "config": { "error": { "general_error": "Tapahtui tuntematon virhe." - }, - "step": { - "node_pro": { - "data": { - "password": "Salasana" - } - } } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/fr.json b/homeassistant/components/airvisual/translations/fr.json index 642fe40cc12..355b57cd4c7 100644 --- a/homeassistant/components/airvisual/translations/fr.json +++ b/homeassistant/components/airvisual/translations/fr.json @@ -30,14 +30,6 @@ "description": "Utilisez l'API cloud AirVisual pour surveiller une ville / un \u00e9tat / un pays.", "title": "Configurer un lieu g\u00e9ographique" }, - "node_pro": { - "data": { - "ip_address": "H\u00f4te", - "password": "Mot de passe" - }, - "description": "Surveillez une unit\u00e9 personnelle AirVisual. Le mot de passe peut \u00eatre r\u00e9cup\u00e9r\u00e9 dans l'interface utilisateur de l'unit\u00e9.", - "title": "Configurer un noeud AirVisual Pro" - }, "reauth_confirm": { "data": { "api_key": "Cl\u00e9 d'API" diff --git a/homeassistant/components/airvisual/translations/he.json b/homeassistant/components/airvisual/translations/he.json index 6d5684220aa..76360d2a2e8 100644 --- a/homeassistant/components/airvisual/translations/he.json +++ b/homeassistant/components/airvisual/translations/he.json @@ -22,13 +22,6 @@ "api_key": "\u05de\u05e4\u05ea\u05d7 API" } }, - "node_pro": { - "data": { - "ip_address": "\u05de\u05d0\u05e8\u05d7", - "password": "\u05e1\u05d9\u05e1\u05de\u05d4" - }, - "description": "\u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d9\u05d7\u05d9\u05d3\u05ea AirVisual \u05d0\u05d9\u05e9\u05d9\u05ea. \u05e0\u05d9\u05ea\u05df \u05dc\u05d0\u05d7\u05d6\u05e8 \u05d0\u05ea \u05d4\u05e1\u05d9\u05e1\u05de\u05d4 \u05de\u05de\u05e9\u05e7 \u05d4\u05de\u05e9\u05ea\u05de\u05e9 \u05e9\u05dc \u05d4\u05d9\u05d7\u05d9\u05d3\u05d4." - }, "reauth_confirm": { "data": { "api_key": "\u05de\u05e4\u05ea\u05d7 API" diff --git a/homeassistant/components/airvisual/translations/hi.json b/homeassistant/components/airvisual/translations/hi.json index ee03f27ccc0..0a59e1cd69c 100644 --- a/homeassistant/components/airvisual/translations/hi.json +++ b/homeassistant/components/airvisual/translations/hi.json @@ -2,16 +2,6 @@ "config": { "error": { "general_error": "\u0915\u094b\u0908 \u0905\u091c\u094d\u091e\u093e\u0924 \u0924\u094d\u0930\u0941\u091f\u093f \u0925\u0940\u0964" - }, - "step": { - "node_pro": { - "data": { - "ip_address": "\u0907\u0915\u093e\u0908 \u0915\u0947 \u0906\u0908\u092a\u0940 \u092a\u0924\u0947/\u0939\u094b\u0938\u094d\u091f\u0928\u093e\u092e", - "password": "\u0907\u0915\u093e\u0908 \u092a\u093e\u0938\u0935\u0930\u094d\u0921" - }, - "description": "\u090f\u0915 \u0935\u094d\u092f\u0915\u094d\u0924\u093f\u0917\u0924 \u090f\u092f\u0930\u0935\u093f\u091c\u0941\u0905\u0932 \u0907\u0915\u093e\u0908 \u0915\u0940 \u0928\u093f\u0917\u0930\u093e\u0928\u0940 \u0915\u0930\u0947\u0902\u0964 \u092a\u093e\u0938\u0935\u0930\u094d\u0921 \u092f\u0942\u0928\u093f\u091f \u0915\u0947 \u092f\u0942\u0906\u0908 \u0938\u0947 \u092a\u094d\u0930\u093e\u092a\u094d\u0924 \u0915\u093f\u092f\u093e \u091c\u093e \u0938\u0915\u0924\u093e \u0939\u0948\u0964", - "title": "\u090f\u092f\u0930\u0935\u093f\u091c\u0941\u0905\u0932 \u0928\u094b\u0921 \u092a\u094d\u0930\u094b" - } } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/hu.json b/homeassistant/components/airvisual/translations/hu.json index e8e80d1d9de..96a8f147256 100644 --- a/homeassistant/components/airvisual/translations/hu.json +++ b/homeassistant/components/airvisual/translations/hu.json @@ -30,14 +30,6 @@ "description": "Haszn\u00e1lja az AirVisual felh\u0151 API-t egy v\u00e1ros / \u00e1llam / orsz\u00e1g figyel\u00e9s\u00e9hez.", "title": "Konfigur\u00e1lja a geogr\u00e1fi\u00e1t" }, - "node_pro": { - "data": { - "ip_address": "C\u00edm", - "password": "Jelsz\u00f3" - }, - "description": "Szem\u00e9lyes AirVisual egys\u00e9g figyel\u00e9se. A jelsz\u00f3 lek\u00e9rhet\u0151 a k\u00e9sz\u00fcl\u00e9k felhaszn\u00e1l\u00f3i fel\u00fclet\u00e9r\u0151l.", - "title": "AirVisual Node/Pro konfigur\u00e1l\u00e1sa" - }, "reauth_confirm": { "data": { "api_key": "API kulcs" diff --git a/homeassistant/components/airvisual/translations/id.json b/homeassistant/components/airvisual/translations/id.json index bfd1d7eea05..b24009f6161 100644 --- a/homeassistant/components/airvisual/translations/id.json +++ b/homeassistant/components/airvisual/translations/id.json @@ -30,14 +30,6 @@ "description": "Gunakan API cloud AirVisual untuk memantau kota/negara bagian/negara.", "title": "Konfigurasikan Lokasi Geografi" }, - "node_pro": { - "data": { - "ip_address": "Host", - "password": "Kata Sandi" - }, - "description": "Pantau unit AirVisual pribadi. Kata sandi dapat diambil dari antarmuka unit.", - "title": "Konfigurasikan AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "Kunci API" diff --git a/homeassistant/components/airvisual/translations/it.json b/homeassistant/components/airvisual/translations/it.json index 4fd98e3fdbf..ecb919cdc39 100644 --- a/homeassistant/components/airvisual/translations/it.json +++ b/homeassistant/components/airvisual/translations/it.json @@ -30,14 +30,6 @@ "description": "Usa l'API cloud di AirVisual per monitorare una citt\u00e0/stato/paese.", "title": "Configura un'area geografica" }, - "node_pro": { - "data": { - "ip_address": "Host", - "password": "Password" - }, - "description": "Monitora un'unit\u00e0 AirVisual personale. La password pu\u00f2 essere recuperata dall'interfaccia utente dell'unit\u00e0.", - "title": "Configura un AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "Chiave API" diff --git a/homeassistant/components/airvisual/translations/ja.json b/homeassistant/components/airvisual/translations/ja.json index eafcdc7378d..28a3dae958a 100644 --- a/homeassistant/components/airvisual/translations/ja.json +++ b/homeassistant/components/airvisual/translations/ja.json @@ -30,14 +30,6 @@ "description": "AirVisual cloud API\u3092\u4f7f\u7528\u3057\u3066\u3001\u90fd\u5e02/\u5dde/\u56fd\u3092\u76e3\u8996\u3057\u307e\u3059\u3002", "title": "Geography\u306e\u8a2d\u5b9a" }, - "node_pro": { - "data": { - "ip_address": "\u30db\u30b9\u30c8", - "password": "\u30d1\u30b9\u30ef\u30fc\u30c9" - }, - "description": "\u500b\u4eba\u306eAirVisual\u30e6\u30cb\u30c3\u30c8\u3092\u76e3\u8996\u3057\u307e\u3059\u3002\u30d1\u30b9\u30ef\u30fc\u30c9\u306f\u3001\u672c\u4f53\u306eUI\u304b\u3089\u53d6\u5f97\u3067\u304d\u307e\u3059\u3002", - "title": "AirVisual Node/Pro\u306e\u8a2d\u5b9a" - }, "reauth_confirm": { "data": { "api_key": "API\u30ad\u30fc" diff --git a/homeassistant/components/airvisual/translations/ko.json b/homeassistant/components/airvisual/translations/ko.json index ddee51dcb3e..739cd19c172 100644 --- a/homeassistant/components/airvisual/translations/ko.json +++ b/homeassistant/components/airvisual/translations/ko.json @@ -30,14 +30,6 @@ "description": "AirVisual \ud074\ub77c\uc6b0\ub4dc API\ub97c \uc0ac\uc6a9\ud558\uc5ec \ub3c4\uc2dc/\uc8fc/\uad6d\uac00\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.", "title": "\uc9c0\ub9ac\uc801 \uc704\uce58 \uad6c\uc131\ud558\uae30" }, - "node_pro": { - "data": { - "ip_address": "\ud638\uc2a4\ud2b8", - "password": "\ube44\ubc00\ubc88\ud638" - }, - "description": "\uc0ac\uc6a9\uc790\uc758 AirVisual \uae30\uae30\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4. \uae30\uae30\uc758 UI \uc5d0\uc11c \ube44\ubc00\ubc88\ud638\ub97c \ucc3e\uc744 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "title": "AirVisual Node/Pro \uad6c\uc131\ud558\uae30" - }, "reauth_confirm": { "data": { "api_key": "API \ud0a4" diff --git a/homeassistant/components/airvisual/translations/lb.json b/homeassistant/components/airvisual/translations/lb.json index 12906b45277..609ac1b73c0 100644 --- a/homeassistant/components/airvisual/translations/lb.json +++ b/homeassistant/components/airvisual/translations/lb.json @@ -18,14 +18,6 @@ "state": "Kanton" } }, - "node_pro": { - "data": { - "ip_address": "Host", - "password": "Passwuert" - }, - "description": "Pers\u00e9inlech Airvisual Unit\u00e9it iwwerwaachen. Passwuert kann vum UI vum Apparat ausgelies ginn.", - "title": "Airvisual Node/Pro ariichten" - }, "reauth_confirm": { "data": { "api_key": "API Schl\u00ebssel" diff --git a/homeassistant/components/airvisual/translations/nl.json b/homeassistant/components/airvisual/translations/nl.json index c857a51ba61..95ec750cbca 100644 --- a/homeassistant/components/airvisual/translations/nl.json +++ b/homeassistant/components/airvisual/translations/nl.json @@ -30,14 +30,6 @@ "description": "Gebruik de AirVisual-cloud-API om een stad/staat/land te bewaken.", "title": "Configureer een geografie" }, - "node_pro": { - "data": { - "ip_address": "Host", - "password": "Wachtwoord" - }, - "description": "Monitor een persoonlijke AirVisual-eenheid. Het wachtwoord kan worden opgehaald uit de gebruikersinterface van het apparaat.", - "title": "Configureer een AirVisual Node / Pro" - }, "reauth_confirm": { "data": { "api_key": "API-sleutel" @@ -50,6 +42,17 @@ } } }, + "entity": { + "sensor": { + "pollutant_level": { + "state": { + "good": "Goed", + "hazardous": "Gevaarlijk", + "unhealthy": "Ongezond" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/airvisual/translations/no.json b/homeassistant/components/airvisual/translations/no.json index 92f0861a8d1..279a2e20078 100644 --- a/homeassistant/components/airvisual/translations/no.json +++ b/homeassistant/components/airvisual/translations/no.json @@ -30,14 +30,6 @@ "description": "Bruk AirVisual cloud API til \u00e5 overv\u00e5ke en by/stat/land.", "title": "Konfigurer en Geography" }, - "node_pro": { - "data": { - "ip_address": "Vert", - "password": "Passord" - }, - "description": "Overv\u00e5ke en personlig AirVisual-enhet. Passordet kan hentes fra enhetens brukergrensesnitt.", - "title": "Konfigurer en AirVisual Node / Pro" - }, "reauth_confirm": { "data": { "api_key": "API-n\u00f8kkel" diff --git a/homeassistant/components/airvisual/translations/pl.json b/homeassistant/components/airvisual/translations/pl.json index 6d69bc38981..abe8c0adc61 100644 --- a/homeassistant/components/airvisual/translations/pl.json +++ b/homeassistant/components/airvisual/translations/pl.json @@ -30,14 +30,6 @@ "description": "U\u017cyj API chmury AirVisual do monitorowania miasta/stanu/kraju.", "title": "Konfiguracja Geography" }, - "node_pro": { - "data": { - "ip_address": "Nazwa hosta lub adres IP", - "password": "Has\u0142o" - }, - "description": "Monitoruj jednostk\u0119 AirVisual. Has\u0142o mo\u017cna odzyska\u0107 z interfejsu u\u017cytkownika urz\u0105dzenia.", - "title": "Konfiguracja AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "Klucz API" diff --git a/homeassistant/components/airvisual/translations/pt-BR.json b/homeassistant/components/airvisual/translations/pt-BR.json index b1ed880bf71..986294e5cec 100644 --- a/homeassistant/components/airvisual/translations/pt-BR.json +++ b/homeassistant/components/airvisual/translations/pt-BR.json @@ -30,14 +30,6 @@ "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" diff --git a/homeassistant/components/airvisual/translations/pt.json b/homeassistant/components/airvisual/translations/pt.json index ab54ba867ea..28fe837af9e 100644 --- a/homeassistant/components/airvisual/translations/pt.json +++ b/homeassistant/components/airvisual/translations/pt.json @@ -15,12 +15,6 @@ "latitude": "Latitude" } }, - "node_pro": { - "data": { - "ip_address": "Endere\u00e7o", - "password": "Palavra-passe" - } - }, "reauth_confirm": { "data": { "api_key": "Chave da API" diff --git a/homeassistant/components/airvisual/translations/ru.json b/homeassistant/components/airvisual/translations/ru.json index 776f15301f9..b2086b8f760 100644 --- a/homeassistant/components/airvisual/translations/ru.json +++ b/homeassistant/components/airvisual/translations/ru.json @@ -30,14 +30,6 @@ "description": "\u0414\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0433\u043e\u0440\u043e\u0434\u0430/\u0448\u0442\u0430\u0442\u0430/\u0441\u0442\u0440\u0430\u043d\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043e\u0431\u043b\u0430\u0447\u043d\u044b\u0439 API AirVisual.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f" }, - "node_pro": { - "data": { - "ip_address": "\u0425\u043e\u0441\u0442", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c" - }, - "description": "\u041c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 AirVisual. \u041f\u0430\u0440\u043e\u043b\u044c \u043c\u043e\u0436\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 AirVisual Node / Pro" - }, "reauth_confirm": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API" diff --git a/homeassistant/components/airvisual/translations/sk.json b/homeassistant/components/airvisual/translations/sk.json index b93c5fad8f1..676cfecc6ec 100644 --- a/homeassistant/components/airvisual/translations/sk.json +++ b/homeassistant/components/airvisual/translations/sk.json @@ -30,14 +30,6 @@ "description": "Pou\u017eite cloudov\u00e9 API AirVisual na monitorovanie mesta/\u0161t\u00e1tu/krajiny.", "title": "Konfigur\u00e1cia geografie" }, - "node_pro": { - "data": { - "ip_address": "Hostite\u013e", - "password": "Heslo" - }, - "description": "Monitorujte osobn\u00fa jednotku AirVisual. Heslo je mo\u017en\u00e9 z\u00edska\u0165 z pou\u017e\u00edvate\u013esk\u00e9ho rozhrania jednotky.", - "title": "Nastavenie AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "API k\u013e\u00fa\u010d" diff --git a/homeassistant/components/airvisual/translations/sl.json b/homeassistant/components/airvisual/translations/sl.json index fc611a1589e..93ea854ff7e 100644 --- a/homeassistant/components/airvisual/translations/sl.json +++ b/homeassistant/components/airvisual/translations/sl.json @@ -8,14 +8,6 @@ "invalid_api_key": "Vpisan neveljaven API klju\u010d" }, "step": { - "node_pro": { - "data": { - "ip_address": "IP naslov/ime gostitelja enote", - "password": "Geslo enote" - }, - "description": "Spremljajte osebno napravo AirVisual. Geslo je mogo\u010de pridobiti iz uporabni\u0161kega vmesnika enote.", - "title": "Konfigurirajte AirVisual Node/Pro" - }, "user": { "description": "Spremljajte kakovost zraka na zemljepisni lokaciji.", "title": "Nastavite AirVisual" diff --git a/homeassistant/components/airvisual/translations/sv.json b/homeassistant/components/airvisual/translations/sv.json index 9e32b698eaf..b9f5d9aeb0b 100644 --- a/homeassistant/components/airvisual/translations/sv.json +++ b/homeassistant/components/airvisual/translations/sv.json @@ -30,14 +30,6 @@ "description": "Anv\u00e4nd AirVisuals moln-API f\u00f6r att \u00f6vervaka en stad/stat/land.", "title": "Konfigurera en geografi" }, - "node_pro": { - "data": { - "ip_address": "Enhets IP-adress / v\u00e4rdnamn", - "password": "Enhetsl\u00f6senord" - }, - "description": "\u00d6vervaka en personlig AirVisual-enhet. L\u00f6senordet kan h\u00e4mtas fr\u00e5n enhetens anv\u00e4ndargr\u00e4nssnitt.", - "title": "Konfigurera en AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "API-nyckel" diff --git a/homeassistant/components/airvisual/translations/tr.json b/homeassistant/components/airvisual/translations/tr.json index bcfe6825372..78e037c9844 100644 --- a/homeassistant/components/airvisual/translations/tr.json +++ b/homeassistant/components/airvisual/translations/tr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f veya Node/Pro Kimli\u011fi zaten kay\u0131tl\u0131.", + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { @@ -30,14 +30,6 @@ "description": "Bir \u015fehri/eyalet/\u00fclkeyi izlemek i\u00e7in AirVisual bulut API'sini kullan\u0131n.", "title": "Bir Co\u011frafyay\u0131 Yap\u0131land\u0131rma" }, - "node_pro": { - "data": { - "ip_address": "Sunucu", - "password": "Parola" - }, - "description": "Ki\u015fisel bir AirVisual \u00fcnitesini izleyin. Parola, \u00fcnitenin kullan\u0131c\u0131 aray\u00fcz\u00fcnden al\u0131nabilir.", - "title": "Bir AirVisual Node/Pro'yu yap\u0131land\u0131r\u0131n" - }, "reauth_confirm": { "data": { "api_key": "API Anahtar\u0131" @@ -50,6 +42,36 @@ } } }, + "entity": { + "sensor": { + "pollutant_label": { + "state": { + "co": "Karbonmonoksit", + "n2": "Nitrojen dioksit", + "o3": "Ozon", + "p1": "PM10", + "p2": "PM2.5", + "s2": "K\u00fck\u00fcrt dioksit" + } + }, + "pollutant_level": { + "state": { + "good": "\u0130yi", + "hazardous": "Tehlikeli", + "moderate": "Il\u0131ml\u0131", + "unhealthy": "Sa\u011fl\u0131ks\u0131z", + "unhealthy_sensitive": "Hassas gruplar i\u00e7in sa\u011fl\u0131ks\u0131z", + "very_unhealthy": "\u00c7ok sa\u011fl\u0131ks\u0131z" + } + } + } + }, + "issues": { + "airvisual_pro_migration": { + "description": "AirVisual Pro birimleri art\u0131k kendi Ev Asistan\u0131 entegrasyonudur (AirVisual bulut API'sini kullanan orijinal AirVisual entegrasyonuna dahil edilmek yerine). ` {ip_address} ` konumunda bulunan Pro cihaz\u0131 otomatik olarak ta\u015f\u0131nd\u0131. \n\n Bu ge\u00e7i\u015fin bir par\u00e7as\u0131 olarak, Uzman\u0131n \" {old_device_id}\" olan cihaz kimli\u011fi \" {old_device_id} {new_device_id} olarak de\u011fi\u015fti. L\u00fctfen bu otomasyonlar\u0131 yeni cihaz kimli\u011fini kullanacak \u015fekilde g\u00fcncelleyin: {device_automations_string} .", + "title": "{ip_address} art\u0131k AirVisual Pro entegrasyonunun bir par\u00e7as\u0131" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/airvisual/translations/uk.json b/homeassistant/components/airvisual/translations/uk.json index 4a4ea6c8b90..193a007fbd9 100644 --- a/homeassistant/components/airvisual/translations/uk.json +++ b/homeassistant/components/airvisual/translations/uk.json @@ -10,14 +10,6 @@ "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API" }, "step": { - "node_pro": { - "data": { - "ip_address": "\u0425\u043e\u0441\u0442", - "password": "\u041f\u0430\u0440\u043e\u043b\u044c" - }, - "description": "\u041c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e AirVisual. \u041f\u0430\u0440\u043e\u043b\u044c \u043c\u043e\u0436\u043d\u0430 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432.", - "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AirVisual Node / Pro" - }, "reauth_confirm": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API" diff --git a/homeassistant/components/airvisual/translations/zh-Hant.json b/homeassistant/components/airvisual/translations/zh-Hant.json index b3c521cf2e4..7a3ace4a5f5 100644 --- a/homeassistant/components/airvisual/translations/zh-Hant.json +++ b/homeassistant/components/airvisual/translations/zh-Hant.json @@ -30,14 +30,6 @@ "description": "\u4f7f\u7528 AirVisual \u96f2\u7aef API \u4ee5\u76e3\u63a7\u57ce\u5e02/\u5dde/\u570b\u5bb6\u3002", "title": "\u8a2d\u5b9a\u5730\u7406\u5ea7\u6a19" }, - "node_pro": { - "data": { - "ip_address": "\u4e3b\u6a5f\u7aef", - "password": "\u5bc6\u78bc" - }, - "description": "\u76e3\u63a7\u500b\u4eba AirVisual \u88dd\u7f6e\uff0c\u5bc6\u78bc\u53ef\u4ee5\u900f\u904e\u88dd\u7f6e UI \u7372\u5f97\u3002", - "title": "\u8a2d\u5b9a AirVisual Node/Pro" - }, "reauth_confirm": { "data": { "api_key": "API \u91d1\u9470" diff --git a/homeassistant/components/airvisual_pro/__init__.py b/homeassistant/components/airvisual_pro/__init__.py index b745dea1d94..b146651b6e6 100644 --- a/homeassistant/components/airvisual_pro/__init__.py +++ b/homeassistant/components/airvisual_pro/__init__.py @@ -21,7 +21,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, Platform, ) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import ( @@ -136,19 +136,3 @@ class AirVisualProEntity(CoordinatorEntity): hw_version=self.coordinator.data["status"]["system_version"], sw_version=self.coordinator.data["status"]["app_version"], ) - - @callback - def _async_update_from_latest_data(self) -> None: - """Update the entity's underlying data.""" - raise NotImplementedError - - @callback - def _handle_coordinator_update(self) -> None: - """Respond to a DataUpdateCoordinator update.""" - self._async_update_from_latest_data() - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" - await super().async_added_to_hass() - self._async_update_from_latest_data() diff --git a/homeassistant/components/airvisual_pro/sensor.py b/homeassistant/components/airvisual_pro/sensor.py index 9fb15c7ac74..11c315be45c 100644 --- a/homeassistant/components/airvisual_pro/sensor.py +++ b/homeassistant/components/airvisual_pro/sensor.py @@ -1,6 +1,8 @@ """Support for AirVisual Pro sensors.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from typing import Any from homeassistant.components.sensor import ( @@ -23,78 +25,93 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import AirVisualProData, AirVisualProEntity from .const import DOMAIN -SENSOR_KIND_AQI = "air_quality_index" -SENSOR_KIND_BATTERY_LEVEL = "battery_level" -SENSOR_KIND_CO2 = "carbon_dioxide" -SENSOR_KIND_HUMIDITY = "humidity" -SENSOR_KIND_PM_0_1 = "particulate_matter_0_1" -SENSOR_KIND_PM_1_0 = "particulate_matter_1_0" -SENSOR_KIND_PM_2_5 = "particulate_matter_2_5" -SENSOR_KIND_SENSOR_LIFE = "sensor_life" -SENSOR_KIND_TEMPERATURE = "temperature" -SENSOR_KIND_VOC = "voc" + +@dataclass +class AirVisualProMeasurementKeyMixin: + """Define an entity description mixin to include a measurement key.""" + + value_fn: Callable[[dict[str, Any], dict[str, Any], dict[str, Any]], float | int] + + +@dataclass +class AirVisualProMeasurementDescription( + SensorEntityDescription, AirVisualProMeasurementKeyMixin +): + """Describe an AirVisual Pro sensor.""" + SENSOR_DESCRIPTIONS = ( - SensorEntityDescription( - key=SENSOR_KIND_AQI, + AirVisualProMeasurementDescription( + key="air_quality_index", name="Air quality index", device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda settings, status, measurements: measurements[ + async_get_aqi_locale(settings) + ], ), - SensorEntityDescription( - key=SENSOR_KIND_BATTERY_LEVEL, + AirVisualProMeasurementDescription( + key="battery_level", name="Battery", device_class=SensorDeviceClass.BATTERY, entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=PERCENTAGE, + value_fn=lambda settings, status, measurements: status["battery"], ), - SensorEntityDescription( - key=SENSOR_KIND_CO2, + AirVisualProMeasurementDescription( + key="carbon_dioxide", name="C02", device_class=SensorDeviceClass.CO2, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda settings, status, measurements: measurements["co2"], ), - SensorEntityDescription( - key=SENSOR_KIND_HUMIDITY, + AirVisualProMeasurementDescription( + key="humidity", name="Humidity", device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=PERCENTAGE, + value_fn=lambda settings, status, measurements: measurements["humidity"], ), - SensorEntityDescription( - key=SENSOR_KIND_PM_0_1, + AirVisualProMeasurementDescription( + key="particulate_matter_0_1", name="PM 0.1", device_class=SensorDeviceClass.PM1, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda settings, status, measurements: measurements["pm0_1"], ), - SensorEntityDescription( - key=SENSOR_KIND_PM_1_0, + AirVisualProMeasurementDescription( + key="particulate_matter_1_0", name="PM 1.0", device_class=SensorDeviceClass.PM10, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda settings, status, measurements: measurements["pm1_0"], ), - SensorEntityDescription( - key=SENSOR_KIND_PM_2_5, + AirVisualProMeasurementDescription( + key="particulate_matter_2_5", name="PM 2.5", device_class=SensorDeviceClass.PM25, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda settings, status, measurements: measurements["pm2_5"], ), - SensorEntityDescription( - key=SENSOR_KIND_TEMPERATURE, + AirVisualProMeasurementDescription( + key="temperature", name="Temperature", device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda settings, status, measurements: measurements["temperature_C"], ), - SensorEntityDescription( - key=SENSOR_KIND_VOC, + AirVisualProMeasurementDescription( + key="voc", name="VOC", device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda settings, status, measurements: measurements["voc"], ), ) @@ -124,40 +141,13 @@ class AirVisualProSensor(AirVisualProEntity, SensorEntity): _attr_has_entity_name = True - MEASUREMENTS_KEY_TO_VALUE = { - SENSOR_KIND_CO2: "co2", - SENSOR_KIND_HUMIDITY: "humidity", - SENSOR_KIND_PM_0_1: "pm0_1", - SENSOR_KIND_PM_1_0: "pm1_0", - SENSOR_KIND_PM_2_5: "pm2_5", - SENSOR_KIND_TEMPERATURE: "temperature_C", - SENSOR_KIND_VOC: "voc", - } + entity_description: AirVisualProMeasurementDescription @property - def measurements(self) -> dict[str, Any]: - """Define measurements data.""" - return self.coordinator.data["measurements"] - - @property - def settings(self) -> dict[str, Any]: - """Define settings data.""" - return self.coordinator.data["settings"] - - @property - def status(self) -> dict[str, Any]: - """Define status data.""" - return self.coordinator.data["status"] - - @callback - def _async_update_from_latest_data(self) -> None: - """Update the entity from the latest data.""" - if self.entity_description.key == SENSOR_KIND_AQI: - locale = async_get_aqi_locale(self.settings) - self._attr_native_value = self.measurements[locale] - elif self.entity_description.key == SENSOR_KIND_BATTERY_LEVEL: - self._attr_native_value = self.status["battery"] - else: - self._attr_native_value = self.measurements[ - self.MEASUREMENTS_KEY_TO_VALUE[self.entity_description.key] - ] + def native_value(self) -> float | int: + """Return the sensor value.""" + return self.entity_description.value_fn( + self.coordinator.data["settings"], + self.coordinator.data["status"], + self.coordinator.data["measurements"], + ) diff --git a/homeassistant/components/airvisual_pro/translations/cs.json b/homeassistant/components/airvisual_pro/translations/cs.json new file mode 100644 index 00000000000..e1bf8e7f45f --- /dev/null +++ b/homeassistant/components/airvisual_pro/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/airvisual_pro/translations/lv.json b/homeassistant/components/airvisual_pro/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/airvisual_pro/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual_pro/translations/nl.json b/homeassistant/components/airvisual_pro/translations/nl.json new file mode 100644 index 00000000000..f3f17cb7c6f --- /dev/null +++ b/homeassistant/components/airvisual_pro/translations/nl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + } + }, + "user": { + "data": { + "ip_address": "Host", + "password": "Wachtwoord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual_pro/translations/pt-BR.json b/homeassistant/components/airvisual_pro/translations/pt-BR.json index 9e5b04ace73..a7efc58f520 100644 --- a/homeassistant/components/airvisual_pro/translations/pt-BR.json +++ b/homeassistant/components/airvisual_pro/translations/pt-BR.json @@ -1,11 +1,11 @@ { "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": "Falhou ao conectar", + "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, @@ -18,7 +18,7 @@ }, "user": { "data": { - "ip_address": "Host", + "ip_address": "Nome do host", "password": "Senha" }, "description": "A senha pode ser recuperada da IU do AirVisual Pro." diff --git a/homeassistant/components/airvisual_pro/translations/tr.json b/homeassistant/components/airvisual_pro/translations/tr.json new file mode 100644 index 00000000000..0270c3b471a --- /dev/null +++ b/homeassistant/components/airvisual_pro/translations/tr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "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", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "Parola, AirVisual Pro'nun kullan\u0131c\u0131 aray\u00fcz\u00fcnden al\u0131nabilir." + }, + "user": { + "data": { + "ip_address": "Sunucu", + "password": "Parola" + }, + "description": "Parola, AirVisual Pro'nun kullan\u0131c\u0131 aray\u00fcz\u00fcnden al\u0131nabilir." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual_pro/translations/uk.json b/homeassistant/components/airvisual_pro/translations/uk.json new file mode 100644 index 00000000000..7ca50b9aa7c --- /dev/null +++ b/homeassistant/components/airvisual_pro/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/__init__.py b/homeassistant/components/airzone/__init__.py index 65ce9193e07..de75bf03d45 100644 --- a/homeassistant/components/airzone/__init__.py +++ b/homeassistant/components/airzone/__init__.py @@ -19,7 +19,12 @@ from homeassistant.helpers import ( from .const import DOMAIN from .coordinator import AirzoneUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.SENSOR] +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.SELECT, + Platform.SENSOR, +] _LOGGER = logging.getLogger(__name__) @@ -40,7 +45,7 @@ async def _async_migrate_unique_ids( entity_unique_id = entity_entry.unique_id if entity_unique_id.startswith(entry_id): - new_unique_id = f"{unique_id}{entity_unique_id[len(entry_id):]}" + new_unique_id = f"{unique_id}{entity_unique_id.removeprefix(entry_id)}" _LOGGER.debug( "Migrating unique_id from [%s] to [%s]", entity_unique_id, diff --git a/homeassistant/components/airzone/climate.py b/homeassistant/components/airzone/climate.py index fa64efa355b..c344b1ff49c 100644 --- a/homeassistant/components/airzone/climate.py +++ b/homeassistant/components/airzone/climate.py @@ -1,7 +1,6 @@ """Support for the Airzone climate.""" from __future__ import annotations -import logging from typing import Any, Final from aioairzone.common import OperationMode @@ -9,8 +8,6 @@ from aioairzone.const import ( API_MODE, API_ON, API_SET_POINT, - API_SYSTEM_ID, - API_ZONE_ID, AZD_DEMAND, AZD_HUMIDITY, AZD_MASTER, @@ -25,7 +22,6 @@ from aioairzone.const import ( AZD_TEMP_UNIT, AZD_ZONES, ) -from aioairzone.exceptions import AirzoneError from homeassistant.components.climate import ( ClimateEntity, @@ -43,9 +39,6 @@ from .const import API_TEMPERATURE_STEP, DOMAIN, TEMP_UNIT_LIB_TO_HASS from .coordinator import AirzoneUpdateCoordinator from .entity import AirzoneZoneEntity -_LOGGER = logging.getLogger(__name__) - - HVAC_ACTION_LIB_TO_HASS: Final[dict[OperationMode, HVACAction]] = { OperationMode.STOP: HVACAction.OFF, OperationMode.COOLING: HVACAction.COOLING, @@ -114,23 +107,6 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity): ] self._async_update_attrs() - async def _async_update_hvac_params(self, params: dict[str, Any]) -> None: - """Send HVAC parameters to API.""" - _params = { - API_SYSTEM_ID: self.system_id, - API_ZONE_ID: self.zone_id, - **params, - } - _LOGGER.debug("update_hvac_params=%s", _params) - try: - await self.coordinator.airzone.set_hvac_parameters(_params) - except AirzoneError as error: - raise HomeAssistantError( - f"Failed to set zone {self.name}: {error}" - ) from error - else: - self.coordinator.async_set_updated_data(self.coordinator.airzone.data()) - async def async_turn_on(self) -> None: """Turn the entity on.""" params = { diff --git a/homeassistant/components/airzone/entity.py b/homeassistant/components/airzone/entity.py index f697a364bc8..a05b8cd6181 100644 --- a/homeassistant/components/airzone/entity.py +++ b/homeassistant/components/airzone/entity.py @@ -1,9 +1,12 @@ """Entity classes for the Airzone integration.""" from __future__ import annotations +import logging from typing import Any from aioairzone.const import ( + API_SYSTEM_ID, + API_ZONE_ID, AZD_FIRMWARE, AZD_FULL_NAME, AZD_ID, @@ -17,8 +20,10 @@ from aioairzone.const import ( AZD_WEBSERVER, AZD_ZONES, ) +from aioairzone.exceptions import AirzoneError from homeassistant.config_entries import ConfigEntry +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -26,6 +31,8 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN, MANUFACTURER from .coordinator import AirzoneUpdateCoordinator +_LOGGER = logging.getLogger(__name__) + class AirzoneEntity(CoordinatorEntity[AirzoneUpdateCoordinator]): """Define an Airzone entity.""" @@ -130,3 +137,20 @@ class AirzoneZoneEntity(AirzoneEntity): if key in zone: value = zone[key] return value + + async def _async_update_hvac_params(self, params: dict[str, Any]) -> None: + """Send HVAC parameters to API.""" + _params = { + API_SYSTEM_ID: self.system_id, + API_ZONE_ID: self.zone_id, + **params, + } + _LOGGER.debug("update_hvac_params=%s", _params) + try: + await self.coordinator.airzone.set_hvac_parameters(_params) + except AirzoneError as error: + raise HomeAssistantError( + f"Failed to set zone {self.name}: {error}" + ) from error + + self.coordinator.async_set_updated_data(self.coordinator.airzone.data()) diff --git a/homeassistant/components/airzone/select.py b/homeassistant/components/airzone/select.py new file mode 100644 index 00000000000..b67dab71c8d --- /dev/null +++ b/homeassistant/components/airzone/select.py @@ -0,0 +1,160 @@ +"""Support for the Airzone sensors.""" +from __future__ import annotations + +from dataclasses import dataclass, replace +from typing import Any, Final + +from aioairzone.common import GrilleAngle, SleepTimeout +from aioairzone.const import ( + API_COLD_ANGLE, + API_HEAT_ANGLE, + API_SLEEP, + AZD_COLD_ANGLE, + AZD_HEAT_ANGLE, + AZD_NAME, + AZD_SLEEP, + AZD_ZONES, +) + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +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 .const import DOMAIN +from .coordinator import AirzoneUpdateCoordinator +from .entity import AirzoneEntity, AirzoneZoneEntity + + +@dataclass +class AirzoneSelectDescriptionMixin: + """Define an entity description mixin for select entities.""" + + api_param: str + options_dict: dict[str, int] + + +@dataclass +class AirzoneSelectDescription(SelectEntityDescription, AirzoneSelectDescriptionMixin): + """Class to describe an Airzone select entity.""" + + +GRILLE_ANGLE_DICT: Final[dict[str, int]] = { + "90º": GrilleAngle.DEG_90, + "50º": GrilleAngle.DEG_50, + "45º": GrilleAngle.DEG_45, + "40º": GrilleAngle.DEG_40, +} + +SLEEP_DICT: Final[dict[str, int]] = { + "Off": SleepTimeout.SLEEP_OFF, + "30m": SleepTimeout.SLEEP_30, + "60m": SleepTimeout.SLEEP_60, + "90m": SleepTimeout.SLEEP_90, +} + + +ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = ( + AirzoneSelectDescription( + api_param=API_COLD_ANGLE, + entity_category=EntityCategory.CONFIG, + key=AZD_COLD_ANGLE, + name="Cold Angle", + options_dict=GRILLE_ANGLE_DICT, + ), + AirzoneSelectDescription( + api_param=API_HEAT_ANGLE, + entity_category=EntityCategory.CONFIG, + key=AZD_HEAT_ANGLE, + name="Heat Angle", + options_dict=GRILLE_ANGLE_DICT, + ), + AirzoneSelectDescription( + api_param=API_SLEEP, + entity_category=EntityCategory.CONFIG, + key=AZD_SLEEP, + name="Sleep", + options_dict=SLEEP_DICT, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Add Airzone sensors from a config_entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + entities: list[AirzoneBaseSelect] = [] + + for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items(): + for description in ZONE_SELECT_TYPES: + if description.key in zone_data: + _desc = replace( + description, + options=list(description.options_dict.keys()), + ) + entities.append( + AirzoneZoneSelect( + coordinator, + _desc, + entry, + system_zone_id, + zone_data, + ) + ) + + async_add_entities(entities) + + +class AirzoneBaseSelect(AirzoneEntity, SelectEntity): + """Define an Airzone select.""" + + entity_description: AirzoneSelectDescription + values_dict: dict[int, str] + + @callback + def _handle_coordinator_update(self) -> None: + """Update attributes when the coordinator updates.""" + self._async_update_attrs() + super()._handle_coordinator_update() + + def _get_current_option(self) -> str | None: + value = self.get_airzone_value(self.entity_description.key) + return self.values_dict.get(value) + + @callback + def _async_update_attrs(self) -> None: + """Update select attributes.""" + self._attr_current_option = self._get_current_option() + + +class AirzoneZoneSelect(AirzoneZoneEntity, AirzoneBaseSelect): + """Define an Airzone Zone select.""" + + def __init__( + self, + coordinator: AirzoneUpdateCoordinator, + description: AirzoneSelectDescription, + entry: ConfigEntry, + system_zone_id: str, + zone_data: dict[str, Any], + ) -> None: + """Initialize.""" + super().__init__(coordinator, entry, system_zone_id, zone_data) + + self._attr_name = f"{zone_data[AZD_NAME]} {description.name}" + self._attr_unique_id = ( + f"{self._attr_unique_id}_{system_zone_id}_{description.key}" + ) + self.entity_description = description + self.values_dict = {v: k for k, v in description.options_dict.items()} + + self._async_update_attrs() + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + param = self.entity_description.api_param + value = self.entity_description.options_dict[option] + await self._async_update_hvac_params({param: value}) diff --git a/homeassistant/components/airzone/translations/lv.json b/homeassistant/components/airzone/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/airzone/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airzone/translations/nl.json b/homeassistant/components/airzone/translations/nl.json index 1c8c936c2c9..c61f255873c 100644 --- a/homeassistant/components/airzone/translations/nl.json +++ b/homeassistant/components/airzone/translations/nl.json @@ -10,6 +10,7 @@ "step": { "discovered_connection": { "data": { + "host": "Host", "id": "Systeem ID", "port": "Poort" } diff --git a/homeassistant/components/airzone/translations/tr.json b/homeassistant/components/airzone/translations/tr.json index a5798334a23..3c3107aebcf 100644 --- a/homeassistant/components/airzone/translations/tr.json +++ b/homeassistant/components/airzone/translations/tr.json @@ -8,9 +8,17 @@ "invalid_system_id": "Ge\u00e7ersiz Airzone Sistem Kimli\u011fi" }, "step": { + "discovered_connection": { + "data": { + "host": "Sunucu", + "id": "Sistem ID", + "port": "Port" + } + }, "user": { "data": { "host": "Sunucu", + "id": "Sistem ID", "port": "Port" } } diff --git a/homeassistant/components/aladdin_connect/__init__.py b/homeassistant/components/aladdin_connect/__init__.py index e66efc1b0ab..3df3c0dbe0a 100644 --- a/homeassistant/components/aladdin_connect/__init__.py +++ b/homeassistant/components/aladdin_connect/__init__.py @@ -4,8 +4,8 @@ import logging from typing import Final from AIOAladdinConnect import AladdinConnectClient -from AIOAladdinConnect.session_manager import InvalidPasswordError -from aiohttp import ClientConnectionError +import AIOAladdinConnect.session_manager as Aladdin +from aiohttp import ClientError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform @@ -29,9 +29,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: await acc.login() - except (ClientConnectionError, asyncio.TimeoutError) as ex: + except (ClientError, asyncio.TimeoutError, Aladdin.ConnectionError) as ex: raise ConfigEntryNotReady("Can not connect to host") from ex - except InvalidPasswordError as ex: + except Aladdin.InvalidPasswordError as ex: raise ConfigEntryAuthFailed("Incorrect Password") from ex hass.data.setdefault(DOMAIN, {})[entry.entry_id] = acc diff --git a/homeassistant/components/aladdin_connect/config_flow.py b/homeassistant/components/aladdin_connect/config_flow.py index 1bfa9757907..eb201182b68 100644 --- a/homeassistant/components/aladdin_connect/config_flow.py +++ b/homeassistant/components/aladdin_connect/config_flow.py @@ -7,8 +7,8 @@ import logging from typing import Any from AIOAladdinConnect import AladdinConnectClient -from AIOAladdinConnect.session_manager import InvalidPasswordError -from aiohttp.client_exceptions import ClientConnectionError +import AIOAladdinConnect.session_manager as Aladdin +from aiohttp.client_exceptions import ClientError import voluptuous as vol from homeassistant import config_entries @@ -45,10 +45,10 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: ) try: await acc.login() - except (ClientConnectionError, asyncio.TimeoutError) as ex: + except (ClientError, asyncio.TimeoutError, Aladdin.ConnectionError) as ex: raise ex - except InvalidPasswordError as ex: + except Aladdin.InvalidPasswordError as ex: raise InvalidAuth from ex @@ -84,7 +84,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except InvalidAuth: errors["base"] = "invalid_auth" - except (ClientConnectionError, asyncio.TimeoutError): + except (ClientError, asyncio.TimeoutError, Aladdin.ConnectionError): errors["base"] = "cannot_connect" else: @@ -121,7 +121,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except InvalidAuth: errors["base"] = "invalid_auth" - except (ClientConnectionError, asyncio.TimeoutError): + except (ClientError, asyncio.TimeoutError, Aladdin.ConnectionError): errors["base"] = "cannot_connect" else: diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 532261339b0..8815ccdbb95 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -91,31 +91,27 @@ class AladdinDevice(CoverEntity): self._name = device["name"] self._serial = device["serial"] self._model = device["model"] - self._attr_unique_id = f"{self._device_id}-{self._number}" - self._attr_has_entity_name = True - @property - def device_info(self) -> DeviceInfo | None: - """Device information for Aladdin Connect cover.""" - return DeviceInfo( + self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"{self._device_id}-{self._number}")}, name=self._name, manufacturer="Overhead Door", model=self._model, ) + self._attr_has_entity_name = True + self._attr_unique_id = f"{self._device_id}-{self._number}" async def async_added_to_hass(self) -> None: """Connect Aladdin Connect to the cloud.""" - async def update_callback() -> None: - """Schedule a state update.""" - self.async_write_ha_state() - - self._acc.register_callback(update_callback, self._serial, self._number) + self._acc.register_callback( + self.async_write_ha_state, self._serial, self._number + ) await self._acc.get_doors(self._serial) async def async_will_remove_from_hass(self) -> None: """Close Aladdin Connect before removing.""" + self._acc.unregister_callback(self._serial, self._number) await self._acc.close() async def async_close_cover(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 71ad99a640d..3cfe7a14167 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "aladdin_connect", "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": ["AIOAladdinConnect==0.1.48"], + "requirements": ["AIOAladdinConnect==0.1.55"], "codeowners": ["@mkmer"], "iot_class": "cloud_polling", "loggers": ["aladdin_connect"], diff --git a/homeassistant/components/aladdin_connect/translations/lv.json b/homeassistant/components/aladdin_connect/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aladdin_connect/translations/uk.json b/homeassistant/components/aladdin_connect/translations/uk.json new file mode 100644 index 00000000000..eed9b44105e --- /dev/null +++ b/homeassistant/components/aladdin_connect/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/translations/lt.json b/homeassistant/components/alarm_control_panel/translations/lt.json index c8a44246004..7638dc86a47 100644 --- a/homeassistant/components/alarm_control_panel/translations/lt.json +++ b/homeassistant/components/alarm_control_panel/translations/lt.json @@ -1,13 +1,44 @@ { + "device_automation": { + "action_type": { + "arm_away": "U\u017erakinti {entity_name}", + "arm_home": "U\u017erakinti {entity_name} - Namie", + "arm_night": "U\u017erakinti {entity_name} - Naktin\u0117 apsauga", + "arm_vacation": "U\u017erakinti {entity_name} atostog\u0173 re\u017eime", + "disarm": "I\u0161jungti {entity_name}", + "trigger": "Suaktyvinti {entity_name}" + }, + "condition_type": { + "is_armed_away": "{entity_name} yra u\u017erakinta", + "is_armed_home": "{entity_name} yra u\u017erakinta nam\u0173 re\u017eime", + "is_armed_night": "{entity_name} u\u017erakinta naktin\u0117 apsauga", + "is_armed_vacation": "{entity_name} u\u017erakinta atostog\u0173 re\u017eime", + "is_disarmed": "{entity_name} i\u0161jungta", + "is_triggered": "{entity_name} suveik\u0117" + }, + "trigger_type": { + "armed_away": "{entity_name} u\u017erakinta", + "armed_home": "{entity_name} u\u017erakinta - Namie", + "armed_night": "{entity_name} u\u017erakinta - Naktin\u0117 apsauga", + "armed_vacation": "{entity_name} u\u017erakinta - atostog\u0173 re\u017eime", + "disarmed": "{entity_name} i\u0161jungta", + "triggered": "{entity_name} suveik\u0117" + } + }, "state": { "_": { "armed": "U\u017erakinta", - "armed_home": "Nam\u0173 apsauga \u012fjungta", - "arming": "Saugojimo re\u017eimo \u012fjungimas", + "armed_away": "U\u017erakinta", + "armed_custom_bypass": "U\u017erakinta su ap\u0117jimu", + "armed_home": "\u012ejungta - Namie", + "armed_night": "Naktin\u0117 apsauga", + "armed_vacation": "U\u017erakinta - atostog\u0173 re\u017eime", + "arming": "U\u017erakinama", "disarmed": "Atrakinta", "disarming": "Saugojimo re\u017eimo i\u0161jungimas", "pending": "Laukiama", "triggered": "Aktyvinta" } - } + }, + "title": "Signalizacijos valdymo pultas" } \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/lt.json b/homeassistant/components/alarmdecoder/translations/lt.json new file mode 100644 index 00000000000..c82e4b7b7e6 --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/lt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u012erenginys jau sukonfig\u016bruotas" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alert/translations/lt.json b/homeassistant/components/alert/translations/lt.json new file mode 100644 index 00000000000..9154d7bc0a0 --- /dev/null +++ b/homeassistant/components/alert/translations/lt.json @@ -0,0 +1,7 @@ +{ + "state": { + "_": { + "idle": "Laukiama" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alert/translations/tr.json b/homeassistant/components/alert/translations/tr.json new file mode 100644 index 00000000000..b623497bcdc --- /dev/null +++ b/homeassistant/components/alert/translations/tr.json @@ -0,0 +1,10 @@ +{ + "state": { + "_": { + "idle": "Bo\u015fta", + "off": "Onayland\u0131", + "on": "Etkin" + } + }, + "title": "Uyar\u0131" +} \ No newline at end of file diff --git a/homeassistant/components/alert/translations/uk.json b/homeassistant/components/alert/translations/uk.json new file mode 100644 index 00000000000..04a8f349959 --- /dev/null +++ b/homeassistant/components/alert/translations/uk.json @@ -0,0 +1,7 @@ +{ + "state": { + "_": { + "idle": "\u0411\u0435\u0437\u0434\u0456\u044f\u043b\u044c\u043d\u0456\u0441\u0442\u044c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index efa2ee3a48a..0feecbd6d24 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -14,6 +14,7 @@ from homeassistant.components import ( input_number, light, media_player, + number, timer, vacuum, ) @@ -26,6 +27,7 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, @@ -41,6 +43,10 @@ from homeassistant.const import ( STATE_UNKNOWN, STATE_UNLOCKED, STATE_UNLOCKING, + UnitOfLength, + UnitOfMass, + UnitOfTemperature, + UnitOfVolume, ) from homeassistant.core import State import homeassistant.util.color as color_util @@ -65,6 +71,34 @@ from .resources import ( _LOGGER = logging.getLogger(__name__) +UNIT_TO_CATALOG_TAG = { + UnitOfTemperature.CELSIUS: AlexaGlobalCatalog.UNIT_TEMPERATURE_CELSIUS, + UnitOfTemperature.FAHRENHEIT: AlexaGlobalCatalog.UNIT_TEMPERATURE_FAHRENHEIT, + UnitOfTemperature.KELVIN: AlexaGlobalCatalog.UNIT_TEMPERATURE_KELVIN, + UnitOfLength.METERS: AlexaGlobalCatalog.UNIT_DISTANCE_METERS, + UnitOfLength.KILOMETERS: AlexaGlobalCatalog.UNIT_DISTANCE_KILOMETERS, + UnitOfLength.INCHES: AlexaGlobalCatalog.UNIT_DISTANCE_INCHES, + UnitOfLength.FEET: AlexaGlobalCatalog.UNIT_DISTANCE_FEET, + UnitOfLength.YARDS: AlexaGlobalCatalog.UNIT_DISTANCE_YARDS, + UnitOfLength.MILES: AlexaGlobalCatalog.UNIT_DISTANCE_MILES, + UnitOfMass.GRAMS: AlexaGlobalCatalog.UNIT_MASS_GRAMS, + UnitOfMass.KILOGRAMS: AlexaGlobalCatalog.UNIT_MASS_KILOGRAMS, + UnitOfMass.POUNDS: AlexaGlobalCatalog.UNIT_WEIGHT_POUNDS, + UnitOfMass.OUNCES: AlexaGlobalCatalog.UNIT_WEIGHT_OUNCES, + UnitOfVolume.LITERS: AlexaGlobalCatalog.UNIT_VOLUME_LITERS, + UnitOfVolume.CUBIC_FEET: AlexaGlobalCatalog.UNIT_VOLUME_CUBIC_FEET, + UnitOfVolume.CUBIC_METERS: AlexaGlobalCatalog.UNIT_VOLUME_CUBIC_METERS, + UnitOfVolume.GALLONS: AlexaGlobalCatalog.UNIT_VOLUME_GALLONS, + PERCENTAGE: AlexaGlobalCatalog.UNIT_PERCENT, + "preset": AlexaGlobalCatalog.SETTING_PRESET, +} + + +def get_resource_by_unit_of_measurement(entity: State) -> str: + """Translate the unit of measurement to an Alexa Global Catalog keyword.""" + unit: str = entity.attributes.get("unit_of_measurement", "preset") + return UNIT_TO_CATALOG_TAG.get(unit, AlexaGlobalCatalog.SETTING_PRESET) + class AlexaCapability: """Base class for Alexa capability interfaces. @@ -78,10 +112,16 @@ class AlexaCapability: supported_locales = {"en-US"} - def __init__(self, entity: State, instance: str | None = None) -> None: + def __init__( + self, + entity: State, + instance: str | None = None, + non_controllable_properties: bool | None = None, + ) -> None: """Initialize an Alexa capability.""" self.entity = entity self.instance = instance + self._non_controllable_properties = non_controllable_properties def name(self) -> str: """Return the Alexa API name of this interface.""" @@ -101,7 +141,7 @@ class AlexaCapability: def properties_non_controllable(self) -> bool | None: """Return True if non controllable.""" - return None + return self._non_controllable_properties def get_property(self, name): """Read and return a property. @@ -135,16 +175,16 @@ class AlexaCapability: def configuration(self): """Return the configuration object. - Applicable to the ThermostatController, SecurityControlPanel, ModeController, RangeController, - and EventDetectionSensor. + Applicable to the ThermostatController, SecurityControlPanel, ModeController, + RangeController, and EventDetectionSensor. """ return [] def configurations(self): """Return the configurations object. - The plural configurations object is different that the singular configuration object. - Applicable to EqualizerController interface. + The plural configurations object is different that the singular configuration + object. Applicable to EqualizerController interface. """ return [] @@ -196,7 +236,8 @@ class AlexaCapability: if configuration := self.configuration(): result["configuration"] = configuration - # The plural configurations object is different than the singular configuration object above. + # The plural configurations object is different than the singular + # configuration object above. if configurations := self.configurations(): result["configurations"] = configurations @@ -757,7 +798,8 @@ class AlexaPlaybackController(AlexaCapability): def supported_operations(self): """Return the supportedOperations object. - Supported Operations: FastForward, Next, Pause, Play, Previous, Rewind, StartOver, Stop + Supported Operations: FastForward, Next, Pause, Play, Previous, Rewind, + StartOver, Stop """ supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -1117,7 +1159,9 @@ class AlexaThermostatController(AlexaCapability): def configuration(self): """Return configuration object. - Translates climate HVAC_MODES and PRESETS to supported Alexa ThermostatMode Values. + Translates climate HVAC_MODES and PRESETS to supported Alexa + ThermostatMode Values. + ThermostatMode Value must be AUTO, COOL, HEAT, ECO, OFF, or CUSTOM. """ supported_modes = [] @@ -1133,7 +1177,8 @@ class AlexaThermostatController(AlexaCapability): if thermostat_mode: supported_modes.append(thermostat_mode) - # Return False for supportsScheduling until supported with event listener in handler. + # Return False for supportsScheduling until supported with event + # listener in handler. configuration = {"supportsScheduling": False} if supported_modes: @@ -1270,12 +1315,15 @@ class AlexaSecurityPanelController(AlexaCapability): class AlexaModeController(AlexaCapability): """Implements Alexa.ModeController. - The instance property must be unique across ModeController, RangeController, ToggleController within the same device. - The instance property should be a concatenated string of device domain period and single word. - e.g. fan.speed & fan.direction. + The instance property must be unique across ModeController, RangeController, + ToggleController within the same device. - The instance property must not contain words from other instance property strings within the same device. - e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail. + The instance property should be a concatenated string of device domain period + and single word. e.g. fan.speed & fan.direction. + + The instance property must not contain words from other instance property + strings within the same device. e.g. Instance property cover.position & + cover.tilt_position will cause the Alexa.Discovery directive to fail. An instance property string value may be reused for different devices. @@ -1302,10 +1350,9 @@ class AlexaModeController(AlexaCapability): def __init__(self, entity, instance, non_controllable=False): """Initialize the entity.""" - super().__init__(entity, instance) + AlexaCapability.__init__(self, entity, instance, non_controllable) self._resource = None self._semantics = None - self.properties_non_controllable = lambda: non_controllable def name(self): """Return the Alexa API name of this interface.""" @@ -1408,8 +1455,8 @@ class AlexaModeController(AlexaCapability): modes = self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES, []) for mode in modes: self._resource.add_mode(f"{humidifier.ATTR_MODE}.{mode}", [mode]) - # Humidifiers or Fans with a single mode completely break Alexa discovery, add a - # fake preset (see issue #53832). + # Humidifiers or Fans with a single mode completely break Alexa discovery, + # add a fake preset (see issue #53832). if len(modes) == 1: self._resource.add_mode( f"{humidifier.ATTR_MODE}.{PRESET_MODE_NA}", [PRESET_MODE_NA] @@ -1479,12 +1526,15 @@ class AlexaModeController(AlexaCapability): class AlexaRangeController(AlexaCapability): """Implements Alexa.RangeController. - The instance property must be unique across ModeController, RangeController, ToggleController within the same device. - The instance property should be a concatenated string of device domain period and single word. - e.g. fan.speed & fan.direction. + The instance property must be unique across ModeController, RangeController, + ToggleController within the same device. - The instance property must not contain words from other instance property strings within the same device. - e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail. + The instance property should be a concatenated string of device domain period + and single word. e.g. fan.speed & fan.direction. + + The instance property must not contain words from other instance property + strings within the same device. e.g. Instance property cover.position & + cover.tilt_position will cause the Alexa.Discovery directive to fail. An instance property string value may be reused for different devices. @@ -1509,12 +1559,13 @@ class AlexaRangeController(AlexaCapability): "pt-BR", } - def __init__(self, entity, instance, non_controllable=False): + def __init__( + self, entity: State, instance: str | None, non_controllable: bool = False + ) -> None: """Initialize the entity.""" - super().__init__(entity, instance) + AlexaCapability.__init__(self, entity, instance, non_controllable) self._resource = None self._semantics = None - self.properties_non_controllable = lambda: non_controllable def name(self): """Return the Alexa API name of this interface.""" @@ -1538,7 +1589,8 @@ class AlexaRangeController(AlexaCapability): raise UnsupportedProperty(name) # Return None for unavailable and unknown states. - # Allows the Alexa.EndpointHealth Interface to handle the unavailable state in a stateReport. + # Allows the Alexa.EndpointHealth Interface to handle the unavailable + # state in a stateReport. if self.entity.state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): return None @@ -1567,6 +1619,10 @@ class AlexaRangeController(AlexaCapability): if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": return float(self.entity.state) + # Number Value + if self.instance == f"{number.DOMAIN}.{number.ATTR_VALUE}": + return float(self.entity.state) + # Vacuum Fan Speed if self.instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}": speed_list = self.entity.attributes.get(vacuum.ATTR_FAN_SPEED_LIST) @@ -1644,7 +1700,29 @@ class AlexaRangeController(AlexaCapability): unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._resource = AlexaPresetResource( - ["Value", AlexaGlobalCatalog.SETTING_PRESET], + ["Value", get_resource_by_unit_of_measurement(self.entity)], + min_value=min_value, + max_value=max_value, + precision=precision, + unit=unit, + ) + self._resource.add_preset( + value=min_value, labels=[AlexaGlobalCatalog.VALUE_MINIMUM] + ) + self._resource.add_preset( + value=max_value, labels=[AlexaGlobalCatalog.VALUE_MAXIMUM] + ) + return self._resource.serialize_capability_resources() + + # Number Value + if self.instance == f"{number.DOMAIN}.{number.ATTR_VALUE}": + min_value = float(self.entity.attributes[number.ATTR_MIN]) + max_value = float(self.entity.attributes[number.ATTR_MAX]) + precision = float(self.entity.attributes.get(number.ATTR_STEP, 1)) + unit = self.entity.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + self._resource = AlexaPresetResource( + ["Value", get_resource_by_unit_of_measurement(self.entity)], min_value=min_value, max_value=max_value, precision=precision, @@ -1760,12 +1838,15 @@ class AlexaRangeController(AlexaCapability): class AlexaToggleController(AlexaCapability): """Implements Alexa.ToggleController. - The instance property must be unique across ModeController, RangeController, ToggleController within the same device. - The instance property should be a concatenated string of device domain period and single word. - e.g. fan.speed & fan.direction. + The instance property must be unique across ModeController, RangeController, + ToggleController within the same device. - The instance property must not contain words from other instance property strings within the same device. - e.g. Instance property cover.position & cover.tilt_position will cause the Alexa.Discovery directive to fail. + The instance property should be a concatenated string of device domain period + and single word. e.g. fan.speed & fan.direction. + + The instance property must not contain words from other instance property + strings within the same device. e.g. Instance property cover.position + & cover.tilt_position will cause the Alexa.Discovery directive to fail. An instance property string value may be reused for different devices. @@ -1792,10 +1873,9 @@ class AlexaToggleController(AlexaCapability): def __init__(self, entity, instance, non_controllable=False): """Initialize the entity.""" - super().__init__(entity, instance) + AlexaCapability.__init__(self, entity, instance, non_controllable) self._resource = None self._semantics = None - self.properties_non_controllable = lambda: non_controllable def name(self): """Return the Alexa API name of this interface.""" @@ -2021,7 +2101,8 @@ class AlexaEventDetectionSensor(AlexaCapability): state = self.entity.state # Return None for unavailable and unknown states. - # Allows the Alexa.EndpointHealth Interface to handle the unavailable state in a stateReport. + # Allows the Alexa.EndpointHealth Interface to handle the unavailable + # state in a stateReport. if state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): return None @@ -2089,7 +2170,8 @@ class AlexaEqualizerController(AlexaCapability): def properties_supported(self): """Return what properties this entity supports. - Either bands, mode or both can be specified. Only mode is supported at this time. + Either bands, mode or both can be specified. Only mode is supported + at this time. """ return [{"name": "mode"}] diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index 9f51d92a229..cdbea2ca346 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -1,8 +1,9 @@ """Config helpers for Alexa.""" from abc import ABC, abstractmethod +import asyncio import logging -from homeassistant.core import callback +from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.storage import Store from .const import DOMAIN @@ -16,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) class AbstractConfig(ABC): """Hold the configuration for Alexa.""" - _unsub_proactive_report = None + _unsub_proactive_report: asyncio.Task[CALLBACK_TYPE] | None = None def __init__(self, hass): """Initialize abstract config.""" diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 77d35a1582c..ab0cafe1156 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -23,6 +23,7 @@ from homeassistant.components import ( light, lock, media_player, + number, scene, script, sensor, @@ -103,7 +104,8 @@ class DisplayCategory: # Indicates a device that cools the air in interior spaces. AIR_CONDITIONER = "AIR_CONDITIONER" - # Indicates a device that emits pleasant odors and masks unpleasant odors in interior spaces. + # Indicates a device that emits pleasant odors and masks unpleasant + # odors in interior spaces. AIR_FRESHENER = "AIR_FRESHENER" # Indicates a device that improves the quality of air in interior spaces. @@ -143,7 +145,8 @@ class DisplayCategory: GAME_CONSOLE = "GAME_CONSOLE" # Indicates a garage door. - # Garage doors must implement the ModeController interface to open and close the door. + # Garage doors must implement the ModeController interface to + # open and close the door. GARAGE_DOOR = "GARAGE_DOOR" # Indicates a wearable device that transmits audio directly into the ear. @@ -206,8 +209,8 @@ class DisplayCategory: # Indicates a security system. SECURITY_SYSTEM = "SECURITY_SYSTEM" - # Indicates an electric cooking device that sits on a countertop, cooks at low temperatures, - # and is often shaped like a cooking pot. + # Indicates an electric cooking device that sits on a countertop, + # cooks at low temperatures, and is often shaped like a cooking pot. SLOW_COOKER = "SLOW_COOKER" # Indicates an endpoint that locks. @@ -243,7 +246,8 @@ class DisplayCategory: # Indicates a vacuum cleaner. VACUUM_CLEANER = "VACUUM_CLEANER" - # Indicates a network-connected wearable device, such as an Apple Watch, Fitbit, or Samsung Gear. + # Indicates a network-connected wearable device, such as an Apple Watch, + # Fitbit, or Samsung Gear. WEARABLE = "WEARABLE" @@ -574,9 +578,10 @@ class FanCapabilities(AlexaEntity): force_range_controller = False # AlexaRangeController controls the Fan Speed Percentage. - # For fans which only support on/off, no controller is added. This makes the - # fan impossible to turn on or off through Alexa, most likely due to a bug in Alexa. - # As a workaround, we add a range controller which can only be set to 0% or 100%. + # For fans which only support on/off, no controller is added. This makes + # the fan impossible to turn on or off through Alexa, most likely due + # to a bug in Alexa. As a workaround, we add a range controller which + # can only be set to 0% or 100%. if force_range_controller or supported & fan.FanEntityFeature.SET_SPEED: yield AlexaRangeController( self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PERCENTAGE}" @@ -849,8 +854,9 @@ class ImageProcessingCapabilities(AlexaEntity): @ENTITY_ADAPTERS.register(input_number.DOMAIN) +@ENTITY_ADAPTERS.register(number.DOMAIN) class InputNumberCapabilities(AlexaEntity): - """Class to represent input_number capabilities.""" + """Class to represent number and input_number capabilities.""" def default_display_categories(self): """Return the display categories for this entity.""" @@ -858,10 +864,8 @@ class InputNumberCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" - - yield AlexaRangeController( - self.entity, instance=f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}" - ) + domain = self.entity.domain + yield AlexaRangeController(self.entity, instance=f"{domain}.value") yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 24ab3ec10e3..eb23b09627e 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -19,6 +19,7 @@ from homeassistant.components import ( input_number, light, media_player, + number, timer, vacuum, ) @@ -613,9 +614,10 @@ async def async_api_adjust_volume_step( """Process an adjust volume step request.""" # media_player volume up/down service does not support specifying steps # each component handles it differently e.g. via config. - # This workaround will simply call the volume up/Volume down the amount of steps asked for - # When no steps are called in the request, Alexa sends a default of 10 steps which for most - # purposes is too high. The default is set 1 in this case. + # This workaround will simply call the volume up/Volume down the amount of + # steps asked for. When no steps are called in the request, Alexa sends + # a default of 10 steps which for most purposes is too high. The default + # is set 1 in this case. entity = directive.entity volume_int = int(directive.payload["volumeSteps"]) is_default = bool(directive.payload["volumeStepsDefault"]) @@ -1020,8 +1022,9 @@ async def async_api_disarm( data = {ATTR_ENTITY_ID: entity.entity_id} response = directive.response() - # Per Alexa Documentation: If you receive a Disarm directive, and the system is already disarmed, - # respond with a success response, not an error response. + # Per Alexa Documentation: If you receive a Disarm directive, and the + # system is already disarmed, respond with a success response, + # not an error response. if entity.state == STATE_ALARM_DISARMED: return response @@ -1136,7 +1139,8 @@ async def async_api_adjust_mode( Only supportedModes with ordered=True support the adjustMode directive. """ - # Currently no supportedModes are configured with ordered=True to support this request. + # Currently no supportedModes are configured with ordered=True + # to support this request. raise AlexaInvalidDirectiveError(DIRECTIVE_NOT_SUPPORTED) @@ -1282,6 +1286,14 @@ async def async_api_set_range( max_value = float(entity.attributes[input_number.ATTR_MAX]) data[input_number.ATTR_VALUE] = min(max_value, max(min_value, range_value)) + # Input Number Value + elif instance == f"{number.DOMAIN}.{number.ATTR_VALUE}": + range_value = float(range_value) + service = number.SERVICE_SET_VALUE + min_value = float(entity.attributes[number.ATTR_MIN]) + max_value = float(entity.attributes[number.ATTR_MAX]) + data[number.ATTR_VALUE] = min(max_value, max(min_value, range_value)) + # Vacuum Fan Speed elif instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}": service = vacuum.SERVICE_SET_FAN_SPEED @@ -1413,6 +1425,17 @@ async def async_api_adjust_range( max_value, max(min_value, range_delta + current) ) + # Number Value + elif instance == f"{number.DOMAIN}.{number.ATTR_VALUE}": + range_delta = float(range_delta) + service = number.SERVICE_SET_VALUE + min_value = float(entity.attributes[number.ATTR_MIN]) + max_value = float(entity.attributes[number.ATTR_MAX]) + current = float(entity.state) + data[number.ATTR_VALUE] = response_value = min( + max_value, max(min_value, range_delta + current) + ) + # Vacuum Fan Speed elif instance == f"{vacuum.DOMAIN}.{vacuum.ATTR_FAN_SPEED}": range_delta = int(range_delta) @@ -1483,7 +1506,9 @@ async def async_api_changechannel( data = { ATTR_ENTITY_ID: entity.entity_id, media_player.const.ATTR_MEDIA_CONTENT_ID: channel, - media_player.const.ATTR_MEDIA_CONTENT_TYPE: media_player.const.MEDIA_TYPE_CHANNEL, + media_player.const.ATTR_MEDIA_CONTENT_TYPE: ( + media_player.const.MEDIA_TYPE_CHANNEL + ), } await hass.services.async_call( diff --git a/homeassistant/components/alexa/resources.py b/homeassistant/components/alexa/resources.py index fb207f17ff4..e171cf0ebdc 100644 --- a/homeassistant/components/alexa/resources.py +++ b/homeassistant/components/alexa/resources.py @@ -6,12 +6,15 @@ class AlexaGlobalCatalog: https://developer.amazon.com/docs/device-apis/resources-and-assets.html#global-alexa-catalog - You can use the global Alexa catalog for pre-defined names of devices, settings, values, and units. - This catalog is localized into all the languages that Alexa supports. + You can use the global Alexa catalog for pre-defined names of devices, settings, + values, and units. + This catalog is localized into all the languages that Alexa supports. You can reference the following catalog of pre-defined friendly names. - Each item in the following list is an asset identifier followed by its supported friendly names. - The first friendly name for each identifier is the one displayed in the Alexa mobile app. + + Each item in the following list is an asset identifier followed by its + supported friendly names. The first friendly name for each identifier is + the one displayed in the Alexa mobile app. """ # Air Purifier, Air Cleaner,Clean Air Machine @@ -23,7 +26,8 @@ class AlexaGlobalCatalog: # Router, Internet Router, Network Router, Wifi Router, Net Router DEVICE_NAME_ROUTER = "Alexa.DeviceName.Router" - # Shade, Blind, Curtain, Roller, Shutter, Drape, Awning, Window shade, Interior blind + # Shade, Blind, Curtain, Roller, Shutter, Drape, Awning, + # Window shade, Interior blind DEVICE_NAME_SHADE = "Alexa.DeviceName.Shade" # Shower @@ -190,10 +194,13 @@ class AlexaGlobalCatalog: class AlexaCapabilityResource: - """Base class for Alexa capabilityResources, modeResources, and presetResources objects. + """Base class for Alexa capabilityResources, modeResources, and presetResources. + + Resources objects labels must be unique across all modeResources and + presetResources within the same device. To provide support for all + supported locales, include one label from the AlexaGlobalCatalog in the + labels array. - Resources objects labels must be unique across all modeResources and presetResources within the same device. - To provide support for all supported locales, include one label from the AlexaGlobalCatalog in the labels array. You cannot use any words from the following list as friendly names: https://developer.amazon.com/docs/alexa/device-apis/resources-and-assets.html#names-you-cannot-use @@ -211,11 +218,17 @@ class AlexaCapabilityResource: return self.serialize_labels(self._resource_labels) def serialize_configuration(self): - """Return ModeResources, PresetResources friendlyNames serialized for an API response.""" + """Return serialized configuration for an API response. + + Return ModeResources, PresetResources friendlyNames serialized. + """ return [] def serialize_labels(self, resources): - """Return resource label objects for friendlyNames serialized for an API response.""" + """Return serialized labels for an API response. + + Returns resource label objects for friendlyNames serialized. + """ labels = [] for label in resources: if label in AlexaGlobalCatalog.__dict__.values(): @@ -245,7 +258,10 @@ class AlexaModeResource(AlexaCapabilityResource): self._supported_modes.append({"value": value, "labels": labels}) def serialize_configuration(self): - """Return configuration for ModeResources friendlyNames serialized for an API response.""" + """Return serialized configuration for an API response. + + Returns configuration for ModeResources friendlyNames serialized. + """ mode_resources = [] for mode in self._supported_modes: result = { @@ -260,7 +276,8 @@ class AlexaModeResource(AlexaCapabilityResource): class AlexaPresetResource(AlexaCapabilityResource): """Implements Alexa PresetResources. - Use presetResources with RangeController to provide a set of friendlyNames for each RangeController preset. + Use presetResources with RangeController to provide a set of + friendlyNamesfor each RangeController preset. https://developer.amazon.com/docs/device-apis/resources-and-assets.html#presetresources """ @@ -281,7 +298,10 @@ class AlexaPresetResource(AlexaCapabilityResource): self._presets.append({"value": value, "labels": labels}) def serialize_configuration(self): - """Return configuration for PresetResources friendlyNames serialized for an API response.""" + """Return serialized configuration for an API response. + + Returns configuration for PresetResources friendlyNames serialized. + """ configuration = { "supportedRange": { "minimumValue": self._minimum_value, @@ -309,18 +329,23 @@ class AlexaPresetResource(AlexaCapabilityResource): class AlexaSemantics: """Class for Alexa Semantics Object. - You can optionally enable additional utterances by using semantics. When you use semantics, - you manually map the phrases "open", "close", "raise", and "lower" to directives. + You can optionally enable additional utterances by using semantics. When + you use semantics, you manually map the phrases "open", "close", "raise", + and "lower" to directives. - Semantics is supported for the following interfaces only: ModeController, RangeController, and ToggleController. + Semantics is supported for the following interfaces only: ModeController, + RangeController, and ToggleController. - Semantics stateMappings are only supported for one interface of the same type on the same device. If a device has - multiple RangeControllers only one interface may use stateMappings otherwise discovery will fail. + Semantics stateMappings are only supported for one interface of the same + type on the same device. If a device has multiple RangeControllers only + one interface may use stateMappings otherwise discovery will fail. - You can support semantics actionMappings on different controllers for the same device, however each controller must - support different phrases. For example, you can support "raise" on a RangeController, and "open" on a ModeController, - but you can't support "open" on both RangeController and ModeController. Semantics stateMappings are only supported - for one interface on the same device. + You can support semantics actionMappings on different controllers for the + same device, however each controller must support different phrases. + For example, you can support "raise" on a RangeController, and "open" + on a ModeController, but you can't support "open" on both RangeController + and ModeController. Semantics stateMappings are only supported for one + interface on the same device. https://developer.amazon.com/docs/device-apis/alexa-discovery.html#semantics-object """ diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py deleted file mode 100644 index 07aea4f792e..00000000000 --- a/homeassistant/components/almond/__init__.py +++ /dev/null @@ -1,325 +0,0 @@ -"""Support for Almond.""" -from __future__ import annotations - -import asyncio -from datetime import timedelta -import logging -import time -from typing import Any - -from aiohttp import ClientError, ClientSession -import async_timeout -from pyalmond import AbstractAlmondWebAuth, AlmondLocalAuth, WebAlmondAPI -import voluptuous as vol - -from homeassistant.auth.const import GROUP_ID_ADMIN -from homeassistant.components import conversation -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - CONF_HOST, - CONF_TYPE, - EVENT_HOMEASSISTANT_START, -) -from homeassistant.core import Context, CoreState, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import ( - aiohttp_client, - config_entry_oauth2_flow, - config_validation as cv, - event, - intent, - network, - storage, -) -from homeassistant.helpers.typing import ConfigType - -from . import config_flow -from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2 - -STORAGE_VERSION = 1 -STORAGE_KEY = DOMAIN - -ALMOND_SETUP_DELAY = 30 - -DEFAULT_OAUTH2_HOST = "https://almond.stanford.edu" -DEFAULT_LOCAL_HOST = "http://localhost:3000" - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Any( - vol.Schema( - { - vol.Required(CONF_TYPE): TYPE_OAUTH2, - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, - vol.Optional(CONF_HOST, default=DEFAULT_OAUTH2_HOST): cv.url, - } - ), - vol.Schema( - {vol.Required(CONF_TYPE): TYPE_LOCAL, vol.Required(CONF_HOST): cv.url} - ), - ) - }, - extra=vol.ALLOW_EXTRA, -) -_LOGGER = logging.getLogger(__name__) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Almond component.""" - hass.data[DOMAIN] = {} - - if DOMAIN not in config: - return True - - conf = config[DOMAIN] - - host = conf[CONF_HOST] - - if conf[CONF_TYPE] == TYPE_OAUTH2: - config_flow.AlmondFlowHandler.async_register_implementation( - hass, - config_entry_oauth2_flow.LocalOAuth2Implementation( - hass, - DOMAIN, - conf[CONF_CLIENT_ID], - conf[CONF_CLIENT_SECRET], - f"{host}/me/api/oauth2/authorize", - f"{host}/me/api/oauth2/token", - ), - ) - return True - - if not hass.config_entries.async_entries(DOMAIN): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={"type": TYPE_LOCAL, "host": conf[CONF_HOST]}, - ) - ) - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Almond config entry.""" - websession = aiohttp_client.async_get_clientsession(hass) - - if entry.data["type"] == TYPE_LOCAL: - auth = AlmondLocalAuth(entry.data["host"], websession) - else: - # OAuth2 - implementation = ( - await config_entry_oauth2_flow.async_get_config_entry_implementation( - hass, entry - ) - ) - oauth_session = config_entry_oauth2_flow.OAuth2Session( - hass, entry, implementation - ) - auth = AlmondOAuth(entry.data["host"], websession, oauth_session) - - api = WebAlmondAPI(auth) - agent = AlmondAgent(hass, api, entry) - - # Hass.io does its own configuration. - if not entry.data.get("is_hassio"): - # If we're not starting or local, set up Almond right away - if hass.state != CoreState.not_running or entry.data["type"] == TYPE_LOCAL: - await _configure_almond_for_ha(hass, entry, api) - - else: - # OAuth2 implementations can potentially rely on the HA Cloud url. - # This url is not be available until 30 seconds after boot. - - async def configure_almond(_now): - try: - await _configure_almond_for_ha(hass, entry, api) - except ConfigEntryNotReady: - _LOGGER.warning( - "Unable to configure Almond to connect to Home Assistant" - ) - - async def almond_hass_start(_event): - event.async_call_later(hass, ALMOND_SETUP_DELAY, configure_almond) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, almond_hass_start) - - conversation.async_set_agent(hass, agent) - return True - - -async def _configure_almond_for_ha( - hass: HomeAssistant, entry: ConfigEntry, api: WebAlmondAPI -): - """Configure Almond to connect to HA.""" - try: - if entry.data["type"] == TYPE_OAUTH2: - # If we're connecting over OAuth2, we will only set up connection - # with Home Assistant if we're remotely accessible. - hass_url = network.get_url(hass, allow_internal=False, prefer_cloud=True) - else: - hass_url = network.get_url(hass) - except network.NoURLAvailableError: - # If no URL is available, we're not going to configure Almond to connect to HA. - return - - _LOGGER.debug("Configuring Almond to connect to Home Assistant at %s", hass_url) - store = storage.Store[dict[str, Any]](hass, STORAGE_VERSION, STORAGE_KEY) - data = await store.async_load() - - if data is None: - data = {} - - user = None - if "almond_user" in data: - user = await hass.auth.async_get_user(data["almond_user"]) - - if user is None: - user = await hass.auth.async_create_system_user( - "Almond", group_ids=[GROUP_ID_ADMIN] - ) - data["almond_user"] = user.id - await store.async_save(data) - - refresh_token = await hass.auth.async_create_refresh_token( - user, - # Almond will be fine as long as we restart once every 5 years - access_token_expiration=timedelta(days=365 * 5), - ) - - # Create long lived access token - access_token = hass.auth.async_create_access_token(refresh_token) - - # Store token in Almond - try: - async with async_timeout.timeout(30): - await api.async_create_device( - { - "kind": "io.home-assistant", - "hassUrl": hass_url, - "accessToken": access_token, - "refreshToken": "", - # 5 years from now in ms. - "accessTokenExpires": (time.time() + 60 * 60 * 24 * 365 * 5) * 1000, - } - ) - except (asyncio.TimeoutError, ClientError) as err: - if isinstance(err, asyncio.TimeoutError): - msg: str | ClientError = "Request timeout" - else: - msg = err - _LOGGER.warning("Unable to configure Almond: %s", msg) - await hass.auth.async_remove_refresh_token(refresh_token) - raise ConfigEntryNotReady from err - - # Clear all other refresh tokens - for token in list(user.refresh_tokens.values()): - if token.id != refresh_token.id: - await hass.auth.async_remove_refresh_token(token) - - -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload Almond.""" - conversation.async_set_agent(hass, None) - return True - - -class AlmondOAuth(AbstractAlmondWebAuth): - """Almond Authentication using OAuth2.""" - - def __init__( - self, - host: str, - websession: ClientSession, - oauth_session: config_entry_oauth2_flow.OAuth2Session, - ) -> None: - """Initialize Almond auth.""" - super().__init__(host, websession) - self._oauth_session = oauth_session - - async def async_get_access_token(self): - """Return a valid access token.""" - if not self._oauth_session.valid_token: - await self._oauth_session.async_ensure_token_valid() - - return self._oauth_session.token["access_token"] - - -class AlmondAgent(conversation.AbstractConversationAgent): - """Almond conversation agent.""" - - def __init__( - self, hass: HomeAssistant, api: WebAlmondAPI, entry: ConfigEntry - ) -> None: - """Initialize the agent.""" - self.hass = hass - self.api = api - self.entry = entry - - @property - def attribution(self): - """Return the attribution.""" - return {"name": "Powered by Almond", "url": "https://almond.stanford.edu/"} - - async def async_get_onboarding(self): - """Get onboard url if not onboarded.""" - if self.entry.data.get("onboarded"): - return None - - host = self.entry.data["host"] - if self.entry.data.get("is_hassio"): - host = "/core_almond" - return { - "text": ( - "Would you like to opt-in to share your anonymized commands with" - " Stanford to improve Almond's responses?" - ), - "url": f"{host}/conversation", - } - - async def async_set_onboarding(self, shown): - """Set onboarding status.""" - self.hass.config_entries.async_update_entry( - self.entry, data={**self.entry.data, "onboarded": shown} - ) - - return True - - async def async_process( - self, - text: str, - context: Context, - conversation_id: str | None = None, - language: str | None = None, - ) -> conversation.ConversationResult | None: - """Process a sentence.""" - response = await self.api.async_converse_text(text, conversation_id) - language = language or self.hass.config.language - - first_choice = True - buffer = "" - for message in response["messages"]: - if message["type"] == "text": - buffer += f"\n{message['text']}" - elif message["type"] == "picture": - buffer += f"\n Picture: {message['url']}" - elif message["type"] == "rdl": - buffer += ( - f"\n Link: {message['rdl']['displayTitle']} " - f"{message['rdl']['webCallback']}" - ) - elif message["type"] == "choice": - if first_choice: - first_choice = False - else: - buffer += "," - buffer += f" {message['title']}" - - intent_response = intent.IntentResponse(language=language) - intent_response.async_set_speech(buffer.strip()) - return conversation.ConversationResult( - response=intent_response, conversation_id=conversation_id - ) diff --git a/homeassistant/components/almond/config_flow.py b/homeassistant/components/almond/config_flow.py deleted file mode 100644 index 11c883f4e0a..00000000000 --- a/homeassistant/components/almond/config_flow.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Config flow to connect with Home Assistant.""" -from __future__ import annotations - -import asyncio -import logging -from typing import Any - -from aiohttp import ClientError -import async_timeout -from pyalmond import AlmondLocalAuth, WebAlmondAPI -from yarl import URL - -from homeassistant import core, data_entry_flow -from homeassistant.components.hassio import HassioServiceInfo -from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow - -from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2 - - -async def async_verify_local_connection(hass: core.HomeAssistant, host: str): - """Verify that a local connection works.""" - websession = aiohttp_client.async_get_clientsession(hass) - api = WebAlmondAPI(AlmondLocalAuth(host, websession)) - - try: - async with async_timeout.timeout(10): - await api.async_list_apps() - - return True - except (asyncio.TimeoutError, ClientError): - return False - - -class AlmondFlowHandler( - config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN -): - """Implementation of the Almond OAuth2 config flow.""" - - DOMAIN = DOMAIN - - host = None - hassio_discovery = None - - @property - def logger(self) -> logging.Logger: - """Return logger.""" - return logging.getLogger(__name__) - - @property - def extra_authorize_data(self) -> dict: - """Extra data that needs to be appended to the authorize url.""" - return {"scope": "profile user-read user-read-results user-exec-command"} - - async def async_step_user(self, user_input=None): - """Handle a flow start.""" - # Only allow 1 instance. - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - return await super().async_step_user(user_input) - - async def async_step_auth(self, user_input=None): - """Handle authorize step.""" - result = await super().async_step_auth(user_input) - - if result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP: - self.host = str(URL(result["url"]).with_path("me")) - - return result - - async def async_oauth_create_entry(self, data: dict) -> FlowResult: - """Create an entry for the flow. - - Ok to override if you want to fetch extra info or even add another step. - """ - data["type"] = TYPE_OAUTH2 - data["host"] = self.host - return self.async_create_entry(title=self.flow_impl.name, data=data) - - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Import data.""" - # Only allow 1 instance. - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - if not await async_verify_local_connection(self.hass, user_input["host"]): - self.logger.warning( - "Aborting import of Almond because we're unable to connect" - ) - return self.async_abort(reason="cannot_connect") - - return self.async_create_entry( - title="Configuration.yaml", - data={"type": TYPE_LOCAL, "host": user_input["host"]}, - ) - - async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: - """Receive a Hass.io discovery.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - self.hassio_discovery = discovery_info.config - - return await self.async_step_hassio_confirm() - - async def async_step_hassio_confirm(self, user_input=None): - """Confirm a Hass.io discovery.""" - data = self.hassio_discovery - - if user_input is not None: - return self.async_create_entry( - title=data["addon"], - data={ - "is_hassio": True, - "type": TYPE_LOCAL, - "host": f"http://{data['host']}:{data['port']}", - }, - ) - - return self.async_show_form( - step_id="hassio_confirm", - description_placeholders={"addon": data["addon"]}, - ) diff --git a/homeassistant/components/almond/const.py b/homeassistant/components/almond/const.py deleted file mode 100644 index 34dca28e957..00000000000 --- a/homeassistant/components/almond/const.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Constants for the Almond integration.""" -DOMAIN = "almond" -TYPE_OAUTH2 = "oauth2" -TYPE_LOCAL = "local" diff --git a/homeassistant/components/almond/manifest.json b/homeassistant/components/almond/manifest.json deleted file mode 100644 index 012450180ca..00000000000 --- a/homeassistant/components/almond/manifest.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "domain": "almond", - "name": "Almond", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/almond", - "dependencies": ["auth", "conversation"], - "codeowners": ["@gcampax", "@balloob"], - "requirements": ["pyalmond==0.0.2"], - "iot_class": "local_polling", - "loggers": ["pyalmond"] -} diff --git a/homeassistant/components/almond/strings.json b/homeassistant/components/almond/strings.json deleted file mode 100644 index 548471a664c..00000000000 --- a/homeassistant/components/almond/strings.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "step": { - "pick_implementation": { - "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" - }, - "hassio_confirm": { - "title": "Almond via Home Assistant add-on", - "description": "Do you want to configure Home Assistant to connect to Almond provided by the add-on: {addon}?" - } - }, - "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", - "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" - } - } -} diff --git a/homeassistant/components/almond/translations/bg.json b/homeassistant/components/almond/translations/bg.json deleted file mode 100644 index 81e1094b1ab..00000000000 --- a/homeassistant/components/almond/translations/bg.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 Almond \u0441\u044a\u0440\u0432\u044a\u0440\u0430.", - "missing_configuration": "\u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430 \u043a\u0430\u043a \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 Almond.", - "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": { - "pick_implementation": { - "title": "\u0418\u0437\u0431\u043e\u0440 \u043d\u0430 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/ca.json b/homeassistant/components/almond/translations/ca.json deleted file mode 100644 index c4dcc2e38e2..00000000000 --- a/homeassistant/components/almond/translations/ca.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Ha fallat la connexi\u00f3", - "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", - "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", - "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." - }, - "step": { - "hassio_confirm": { - "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb Almond proporcionat pel complement: {addon}?", - "title": "Almond via complement de Home Assistant" - }, - "pick_implementation": { - "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/cs.json b/homeassistant/components/almond/translations/cs.json deleted file mode 100644 index dc981403ad2..00000000000 --- a/homeassistant/components/almond/translations/cs.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "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})", - "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." - }, - "step": { - "hassio_confirm": { - "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k Almond pomoc\u00ed Supervisor {addon}?", - "title": "Almond prost\u0159ednictv\u00edm dopl\u0148ku Supervisor" - }, - "pick_implementation": { - "title": "Vyberte metodu ov\u011b\u0159en\u00ed" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/da.json b/homeassistant/components/almond/translations/da.json deleted file mode 100644 index 0e7a804acc6..00000000000 --- a/homeassistant/components/almond/translations/da.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Kan ikke oprette forbindelse til Almond-serveren.", - "missing_configuration": "Tjek venligst dokumentationen om, hvordan man indstiller Almond." - }, - "step": { - "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til Almond leveret af Supervisor-tilf\u00f8jelsen: {addon}?", - "title": "Almond via Supervisor-tilf\u00f8jelse" - }, - "pick_implementation": { - "title": "V\u00e6lg godkendelsesmetode" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/de.json b/homeassistant/components/almond/translations/de.json deleted file mode 100644 index 1f69b1c09e4..00000000000 --- a/homeassistant/components/almond/translations/de.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Verbindung fehlgeschlagen", - "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", - "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", - "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." - }, - "step": { - "hassio_confirm": { - "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit Almond als Supervisor-Add-On hergestellt wird: {addon}?", - "title": "Almond \u00fcber das Supervisor Add-on" - }, - "pick_implementation": { - "title": "W\u00e4hle die Authentifizierungsmethode" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/el.json b/homeassistant/components/almond/translations/el.json deleted file mode 100644 index ac3a8efd757..00000000000 --- a/homeassistant/components/almond/translations/el.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "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" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/en.json b/homeassistant/components/almond/translations/en.json deleted file mode 100644 index fb7d4127352..00000000000 --- a/homeassistant/components/almond/translations/en.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Failed to connect", - "missing_configuration": "The component is not configured. Please follow the documentation.", - "no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", - "single_instance_allowed": "Already configured. Only a single configuration possible." - }, - "step": { - "hassio_confirm": { - "description": "Do you want to configure Home Assistant to connect to Almond provided by the add-on: {addon}?", - "title": "Almond via Home Assistant add-on" - }, - "pick_implementation": { - "title": "Pick Authentication Method" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/es-419.json b/homeassistant/components/almond/translations/es-419.json deleted file mode 100644 index ce1d655d69e..00000000000 --- a/homeassistant/components/almond/translations/es-419.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "No se puede conectar con el servidor Almond.", - "missing_configuration": "Por favor, consulte la documentaci\u00f3n sobre c\u00f3mo configurar Almond." - }, - "step": { - "hassio_confirm": { - "description": "\u00bfDesea configurar Home Assistant para conectarse a Almond proporcionado por el complemento Supervisor: {addon}?", - "title": "Almond a trav\u00e9s del complemento Supervisor" - }, - "pick_implementation": { - "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/es.json b/homeassistant/components/almond/translations/es.json deleted file mode 100644 index 7c768deecdd..00000000000 --- a/homeassistant/components/almond/translations/es.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "No se pudo conectar", - "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", - "no_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [revisa la secci\u00f3n de ayuda]({docs_url})", - "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." - }, - "step": { - "hassio_confirm": { - "description": "\u00bfQuieres configurar Home Assistant para conectarse a Almond proporcionado por el complemento: {addon}?", - "title": "Almond a trav\u00e9s del complemento Home Assistant" - }, - "pick_implementation": { - "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/et.json b/homeassistant/components/almond/translations/et.json deleted file mode 100644 index 5b15d9328cc..00000000000 --- a/homeassistant/components/almond/translations/et.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u00dchendamine nurjus", - "missing_configuration": "Osis on seadistamata. Vaata dokumentatsiooni.", - "no_url_available": "URL pole saadaval. Rohkem teavet [spikrijaotis]({docs_url})", - "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." - }, - "step": { - "hassio_confirm": { - "description": "Kas soovid seadistada Home Assistant-i \u00fchendust Almondiga mida pakub lisandmoodul: {addon} ?", - "title": "Almond Home Assistanti lisandmooduli abil" - }, - "pick_implementation": { - "title": "Vali tuvastusmeetod" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/fi.json b/homeassistant/components/almond/translations/fi.json deleted file mode 100644 index 33427bf8451..00000000000 --- a/homeassistant/components/almond/translations/fi.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Yhteyden muodostaminen Almond-palvelimeen ei onnistu." - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/fr.json b/homeassistant/components/almond/translations/fr.json deleted file mode 100644 index 6c38df7dec1..00000000000 --- a/homeassistant/components/almond/translations/fr.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u00c9chec de connexion", - "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide]({docs_url})", - "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." - }, - "step": { - "hassio_confirm": { - "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte \u00e0 Almond fourni par le module compl\u00e9mentaire Hass.io: {addon} ?", - "title": "Amande via le module compl\u00e9mentaire Hass.io" - }, - "pick_implementation": { - "title": "S\u00e9lectionner une m\u00e9thode d'authentification" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/he.json b/homeassistant/components/almond/translations/he.json deleted file mode 100644 index 6aa9dd1d75f..00000000000 --- a/homeassistant/components/almond/translations/he.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", - "missing_configuration": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05e8\u05db\u05d9\u05d1 \u05dc\u05d0 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e0\u05d0 \u05e2\u05e7\u05d5\u05d1 \u05d0\u05d7\u05e8 \u05d4\u05ea\u05d9\u05e2\u05d5\u05d3.", - "no_url_available": "\u05d0\u05d9\u05df \u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8 \u05d6\u05de\u05d9\u05e0\u05d4. \u05e7\u05d1\u05dc\u05ea \u05de\u05d9\u05d3\u05e2 \u05e2\u05dc \u05e9\u05d2\u05d9\u05d0\u05d4 \u05d6\u05d5, [\u05e2\u05d9\u05d9\u05df \u05d1\u05e1\u05e2\u05d9\u05e3 \u05d4\u05e2\u05d6\u05e8\u05d4] ({docs_url})", - "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." - }, - "step": { - "pick_implementation": { - "title": "\u05d1\u05d7\u05e8 \u05e9\u05d9\u05d8\u05ea \u05d0\u05d9\u05de\u05d5\u05ea" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/hu.json b/homeassistant/components/almond/translations/hu.json deleted file mode 100644 index b424675faaa..00000000000 --- a/homeassistant/components/almond/translations/hu.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", - "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", - "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." - }, - "step": { - "hassio_confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani Home Assistantot Almondhoz val\u00f3 csatlakoz\u00e1shoz, {addon} b\u0151v\u00edtm\u00e9ny \u00e1ltal?", - "title": "Almond - Home Assistant b\u0151v\u00edtm\u00e9nnyel" - }, - "pick_implementation": { - "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/id.json b/homeassistant/components/almond/translations/id.json deleted file mode 100644 index 8e4302220b5..00000000000 --- a/homeassistant/components/almond/translations/id.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Gagal terhubung", - "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", - "no_url_available": "Tidak ada URL yang tersedia. Untuk informasi tentang kesalahan ini, [lihat bagian bantuan]({docs_url})", - "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." - }, - "step": { - "hassio_confirm": { - "description": "Ingin mengonfigurasi Home Assistant untuk terhubung ke Almond yang disediakan oleh add-on: {addon}?", - "title": "Almond melalui add-on Home Assistant" - }, - "pick_implementation": { - "title": "Pilih Metode Autentikasi" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/it.json b/homeassistant/components/almond/translations/it.json deleted file mode 100644 index 1d028e73111..00000000000 --- a/homeassistant/components/almond/translations/it.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Impossibile connettersi", - "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", - "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", - "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." - }, - "step": { - "hassio_confirm": { - "description": "Vuoi configurare Home Assistant per connettersi a Almond fornito dal componente aggiuntivo: {addon}?", - "title": "Almond tramite il componente aggiuntivo di Home Assistant" - }, - "pick_implementation": { - "title": "Scegli il metodo di autenticazione" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/ja.json b/homeassistant/components/almond/translations/ja.json deleted file mode 100644 index 898bc4bc1c0..00000000000 --- a/homeassistant/components/almond/translations/ja.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "missing_configuration": "\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u306b\u5f93\u3063\u3066\u304f\u3060\u3055\u3044\u3002", - "no_url_available": "\u4f7f\u7528\u53ef\u80fd\u306aURL\u304c\u3042\u308a\u307e\u305b\u3093\u3002\u3053\u306e\u30a8\u30e9\u30fc\u306e\u8a73\u7d30\u306b\u3064\u3044\u3066\u306f\u3001[\u30d8\u30eb\u30d7\u30bb\u30af\u30b7\u30e7\u30f3\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044]({docs_url})", - "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u8a2d\u5b9a\u3067\u304d\u308b\u306e\u306f1\u3064\u3060\u3051\u3067\u3059\u3002" - }, - "step": { - "hassio_confirm": { - "description": "\u30a2\u30c9\u30aa\u30f3: {addon} \u306b\u3088\u3063\u3066\u63d0\u4f9b\u3055\u308c\u308b\u3001Almond\u306b\u63a5\u7d9a\u3059\u308b\u3088\u3046\u306bHome Assistant\u306e\u8a2d\u5b9a\u3092\u3057\u307e\u3059\u304b\uff1f", - "title": "Home Assistant\u30a2\u30c9\u30aa\u30f3\u7d4c\u7531\u306eAlmond" - }, - "pick_implementation": { - "title": "\u8a8d\u8a3c\u65b9\u6cd5\u306e\u9078\u629e" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/ko.json b/homeassistant/components/almond/translations/ko.json deleted file mode 100644 index d18f5c914cc..00000000000 --- a/homeassistant/components/almond/translations/ko.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", - "no_url_available": "\uc0ac\uc6a9 \uac00\ub2a5\ud55c URL\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 [\ub3c4\uc6c0\ub9d0 \uc139\uc158]({docs_url}) \uc744(\ub97c) \ucc38\uc870\ud574\uc8fc\uc138\uc694.", - "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." - }, - "step": { - "hassio_confirm": { - "description": "{addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ud558\ub294 Almond\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant\ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Home Assistant \uc560\ub4dc\uc628\uc758 Almond" - }, - "pick_implementation": { - "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/lb.json b/homeassistant/components/almond/translations/lb.json deleted file mode 100644 index 0e29d69bbed..00000000000 --- a/homeassistant/components/almond/translations/lb.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Feeler beim verbannen", - "missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.", - "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})", - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." - }, - "step": { - "hassio_confirm": { - "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mam Almond ze verbannen dee vun der Supervisor Erweiderung {addon} bereet gestallt g\u00ebtt?", - "title": "Almond via Supervisor Erweiderung" - }, - "pick_implementation": { - "title": "Wiel Authentifikatiouns Method aus" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/nl.json b/homeassistant/components/almond/translations/nl.json deleted file mode 100644 index e548206e23e..00000000000 --- a/homeassistant/components/almond/translations/nl.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Kan geen verbinding maken", - "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", - "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})", - "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." - }, - "step": { - "hassio_confirm": { - "description": "Wilt u Home Assistant configureren om verbinding te maken met Almond die wordt aangeboden door de add-on {addon} ?", - "title": "Almond via Home Assistant add-on" - }, - "pick_implementation": { - "title": "Kies een authenticatie methode" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/no.json b/homeassistant/components/almond/translations/no.json deleted file mode 100644 index 098184ff7af..00000000000 --- a/homeassistant/components/almond/translations/no.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Tilkobling mislyktes", - "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", - "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", - "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." - }, - "step": { - "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant for \u00e5 koble til Almond levert av tillegget: {addon} ?", - "title": "Almond via Home Assistant-tillegg" - }, - "pick_implementation": { - "title": "Velg godkjenningsmetode" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/pl.json b/homeassistant/components/almond/translations/pl.json deleted file mode 100644 index 88fd6cda01c..00000000000 --- a/homeassistant/components/almond/translations/pl.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", - "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", - "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." - }, - "step": { - "hassio_confirm": { - "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby \u0142\u0105czy\u0142 si\u0119 z Almond dostarczonym przez dodatek {addon}?", - "title": "Almond poprzez dodatek Home Assistant" - }, - "pick_implementation": { - "title": "Wybierz metod\u0119 uwierzytelniania" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/pt-BR.json b/homeassistant/components/almond/translations/pt-BR.json deleted file mode 100644 index d012b1695f3..00000000000 --- a/homeassistant/components/almond/translations/pt-BR.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "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" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/pt.json b/homeassistant/components/almond/translations/pt.json deleted file mode 100644 index 86d4823a272..00000000000 --- a/homeassistant/components/almond/translations/pt.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "A liga\u00e7\u00e3o falhou", - "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", - "no_url_available": "Nenhum URL est\u00e1 dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [consulte a sec\u00e7\u00e3o de ajuda]({docs_url})", - "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." - }, - "step": { - "pick_implementation": { - "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/ru.json b/homeassistant/components/almond/translations/ru.json deleted file mode 100644 index b77e1cfca2c..00000000000 --- a/homeassistant/components/almond/translations/ru.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", - "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435.", - "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": { - "hassio_confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \"{addon}\")?", - "title": "Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" - }, - "pick_implementation": { - "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/sk.json b/homeassistant/components/almond/translations/sk.json deleted file mode 100644 index 0189ab5be44..00000000000 --- a/homeassistant/components/almond/translations/sk.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Nepodarilo sa pripoji\u0165", - "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", - "no_url_available": "Nie je k dispoz\u00edcii \u017eiadna adresa URL. Inform\u00e1cie o tejto chybe n\u00e1jdete [pozrite si sekciu pomocn\u00edka]({docs_url})", - "single_instance_allowed": "U\u017e je nakonfigurovan\u00fd. Mo\u017en\u00e1 len jedna konfigur\u00e1cia." - }, - "step": { - "hassio_confirm": { - "description": "Chcete nakonfigurova\u0165 dom\u00e1ceho asistenta na pripojenie k Almond poskytovan\u00e9mu doplnkom: {addon}?", - "title": "Doplnok Almond cez Home Assistant" - }, - "pick_implementation": { - "title": "Vyberte met\u00f3du overenia" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/sl.json b/homeassistant/components/almond/translations/sl.json deleted file mode 100644 index cb20393201f..00000000000 --- a/homeassistant/components/almond/translations/sl.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Ni mogo\u010de vzpostaviti povezave s stre\u017enikom Almond.", - "missing_configuration": "Prosimo, preverite dokumentacijo o tem, kako nastaviti Almond." - }, - "step": { - "hassio_confirm": { - "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo z Almondom, ki ga ponuja dodatek Supervisor: {addon} ?", - "title": "Almond prek dodatka Supervisor" - }, - "pick_implementation": { - "title": "Izberite na\u010din preverjanja pristnosti" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/sv.json b/homeassistant/components/almond/translations/sv.json deleted file mode 100644 index c11af204012..00000000000 --- a/homeassistant/components/almond/translations/sv.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Det g\u00e5r inte att ansluta till Almond-servern.", - "missing_configuration": "Kontrollera dokumentationen f\u00f6r hur du st\u00e4ller in Almond.", - "no_url_available": "Ingen webbadress tillg\u00e4nglig. F\u00f6r information om detta fel, [kolla hj\u00e4lpavsnittet]({docs_url})", - "single_instance_allowed": "Redan konfigurerad. Endast en konfiguration m\u00f6jlig." - }, - "step": { - "hassio_confirm": { - "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till Almond som tillhandah\u00e5lls av Supervisor-till\u00e4gget: {addon} ?", - "title": "Almond via Supervisor-till\u00e4gget" - }, - "pick_implementation": { - "title": "V\u00e4lj autentiseringsmetod" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/tr.json b/homeassistant/components/almond/translations/tr.json deleted file mode 100644 index a0808fde8ef..00000000000 --- a/homeassistant/components/almond/translations/tr.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "Ba\u011flanma hatas\u0131", - "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", - "no_url_available": "Kullan\u0131labilir URL yok. Bu hata hakk\u0131nda bilgi i\u00e7in [yard\u0131m b\u00f6l\u00fcm\u00fcne bak\u0131n]({docs_url})", - "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." - }, - "step": { - "hassio_confirm": { - "description": "{addon} taraf\u0131ndan sa\u011flanan Almond'a ba\u011flanacak \u015fekilde yap\u0131land\u0131rmak istiyor musunuz?", - "title": "Home Assistant eklentisi arac\u0131l\u0131\u011f\u0131yla Almond" - }, - "pick_implementation": { - "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/uk.json b/homeassistant/components/almond/translations/uk.json deleted file mode 100644 index d1e0d1e1cb6..00000000000 --- a/homeassistant/components/almond/translations/uk.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", - "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", - "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", - "single_instance_allowed": "\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": { - "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Almond (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor \"{addon}\")?", - "title": "Almond (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Supervisor)" - }, - "pick_implementation": { - "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/zh-Hant.json b/homeassistant/components/almond/translations/zh-Hant.json deleted file mode 100644 index d5139fcb8b8..00000000000 --- a/homeassistant/components/almond/translations/zh-Hant.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "config": { - "abort": { - "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": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" - }, - "step": { - "hassio_confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Almond\u3002\u9644\u52a0\u5143\u4ef6\u70ba\uff1a{addon} \uff1f", - "title": "\u4f7f\u7528 Home Assistant \u9644\u52a0\u5143\u4ef6 Almond" - }, - "pick_implementation": { - "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/translations/lt.json b/homeassistant/components/ambiclimate/translations/lt.json new file mode 100644 index 00000000000..701f066e84c --- /dev/null +++ b/homeassistant/components/ambiclimate/translations/lt.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "auth": { + "description": "Pra\u0161ome sekti \u0161ia [nuoroda]({authorization_url}) ir **leisti** prieig\u0105 prie savo \"Ambiclimate\" paskyros, tada gr\u012f\u017ekite ir paspauskite **Pateikti** toliau.\n(\u012esitikinkite, kad nurodytas gr\u012f\u017etamojo ry\u0161io URL yra {cb_url})" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 9aced7a2a45..5dd8f0fb2fd 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -179,7 +179,11 @@ class AmbientStation: # attempt forward setup of the config entry (because it will have # already been done): if not self._entry_setup_complete: - self._hass.config_entries.async_setup_platforms(self._entry, PLATFORMS) + self._hass.async_create_task( + self._hass.config_entries.async_forward_entry_setups( + self._entry, PLATFORMS + ) + ) self._entry_setup_complete = True self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index eb01fd379b2..0fc6e7643db 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -128,7 +128,6 @@ SENSOR_DESCRIPTIONS = ( key=TYPE_AQI_PM25_24H, name="AQI PM2.5 24h avg", device_class=SensorDeviceClass.AQI, - state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_AQI_PM25_IN, @@ -140,7 +139,6 @@ SENSOR_DESCRIPTIONS = ( key=TYPE_AQI_PM25_IN_24H, name="AQI PM2.5 indoor 24h avg", device_class=SensorDeviceClass.AQI, - state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_BAROMABSIN, @@ -182,7 +180,7 @@ SENSOR_DESCRIPTIONS = ( name="Event rain", native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_FEELSLIKE, @@ -287,7 +285,6 @@ SENSOR_DESCRIPTIONS = ( name="Last rain", icon="mdi:water", device_class=SensorDeviceClass.TIMESTAMP, - state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TYPE_LIGHTNING_PER_DAY, @@ -315,7 +312,7 @@ SENSOR_DESCRIPTIONS = ( name="Monthly rain", native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_PM25_24H, @@ -586,7 +583,7 @@ SENSOR_DESCRIPTIONS = ( name="Lifetime rain", native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL_INCREASING, ), SensorEntityDescription( key=TYPE_UV, @@ -599,7 +596,7 @@ SENSOR_DESCRIPTIONS = ( name="Weekly rain", native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), SensorEntityDescription( key=TYPE_WINDDIR, diff --git a/homeassistant/components/android_ip_webcam/translations/lv.json b/homeassistant/components/android_ip_webcam/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/android_ip_webcam/translations/uk.json b/homeassistant/components/android_ip_webcam/translations/uk.json new file mode 100644 index 00000000000..2aed6be91ba --- /dev/null +++ b/homeassistant/components/android_ip_webcam/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/androidtv/__init__.py b/homeassistant/components/androidtv/__init__.py index c2d83ab05e8..d10b1161da6 100644 --- a/homeassistant/components/androidtv/__init__.py +++ b/homeassistant/components/androidtv/__init__.py @@ -6,6 +6,13 @@ import os from typing import Any from adb_shell.auth.keygen import keygen +from adb_shell.exceptions import ( + AdbTimeoutError, + InvalidChecksumError, + InvalidCommandError, + InvalidResponseError, + TcpTimeoutException, +) from androidtv.adb_manager.adb_manager_sync import ADBPythonSync, PythonRSASigner from androidtv.setup_async import ( AndroidTVAsync, @@ -43,6 +50,18 @@ from .const import ( SIGNAL_CONFIG_ENTITY, ) +ADB_PYTHON_EXCEPTIONS: tuple = ( + AdbTimeoutError, + BrokenPipeError, + ConnectionResetError, + ValueError, + InvalidChecksumError, + InvalidCommandError, + InvalidResponseError, + TcpTimeoutException, +) +ADB_TCP_EXCEPTIONS: tuple = (ConnectionResetError, RuntimeError) + PLATFORMS = [Platform.MEDIA_PLAYER] RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES] @@ -132,9 +151,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Android TV platform.""" state_det_rules = entry.options.get(CONF_STATE_DETECTION_RULES) - aftv, error_message = await async_connect_androidtv( - hass, entry.data, state_detection_rules=state_det_rules - ) + if CONF_ADB_SERVER_IP not in entry.data: + exceptions = ADB_PYTHON_EXCEPTIONS + else: + exceptions = ADB_TCP_EXCEPTIONS + + try: + aftv, error_message = await async_connect_androidtv( + hass, entry.data, state_detection_rules=state_det_rules + ) + except exceptions as exc: + raise ConfigEntryNotReady(exc) from exc + if not aftv: raise ConfigEntryNotReady(error_message) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 572aa426105..f4be6d6eea5 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -5,18 +5,10 @@ from collections.abc import Awaitable, Callable, Coroutine from datetime import datetime import functools import logging -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar -from adb_shell.exceptions import ( - AdbTimeoutError, - InvalidChecksumError, - InvalidCommandError, - InvalidResponseError, - TcpTimeoutException, -) from androidtv.constants import APPS, KEYS from androidtv.exceptions import LockNotAcquiredException -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.components import persistent_notification @@ -43,7 +35,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import get_androidtv_mac +from . import ADB_PYTHON_EXCEPTIONS, ADB_TCP_EXCEPTIONS, get_androidtv_mac from .const import ( ANDROID_DEV, ANDROID_DEV_OPT, @@ -170,7 +162,6 @@ def adb_decorator( self: _ADBDeviceT, *args: _P.args, **kwargs: _P.kwargs ) -> _R | None: """Call an ADB-related method and catch exceptions.""" - # pylint: disable=protected-access if not self.available and not override_available: return None @@ -192,12 +183,14 @@ def adb_decorator( err, ) await self.aftv.adb_close() + # pylint: disable-next=protected-access self._attr_available = False return None except Exception: # An unforeseen exception occurred. Close the ADB connection so that # it doesn't happen over and over again, then raise the exception. await self.aftv.adb_close() + # pylint: disable-next=protected-access self._attr_available = False raise @@ -252,19 +245,10 @@ class ADBDevice(MediaPlayerEntity): # ADB exceptions to catch if not aftv.adb_server_ip: # Using "adb_shell" (Python ADB implementation) - self.exceptions = ( - AdbTimeoutError, - BrokenPipeError, - ConnectionResetError, - ValueError, - InvalidChecksumError, - InvalidCommandError, - InvalidResponseError, - TcpTimeoutException, - ) + self.exceptions = ADB_PYTHON_EXCEPTIONS else: # Using "pure-python-adb" (communicate with ADB server) - self.exceptions = (ConnectionResetError, RuntimeError) + self.exceptions = ADB_TCP_EXCEPTIONS # Property attributes self._attr_extra_state_attributes = { diff --git a/homeassistant/components/androidtv/translations/lv.json b/homeassistant/components/androidtv/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/androidtv/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anthemav/__init__.py b/homeassistant/components/anthemav/__init__.py index 24a83d0aff0..fe7fe072785 100644 --- a/homeassistant/components/anthemav/__init__.py +++ b/homeassistant/components/anthemav/__init__.py @@ -42,7 +42,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = avr - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @callback def close_avr(event: Event) -> None: diff --git a/homeassistant/components/anthemav/translations/lv.json b/homeassistant/components/anthemav/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/anthemav/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/anwb_energie/__init__.py b/homeassistant/components/anwb_energie/__init__.py new file mode 100644 index 00000000000..0857e2526e6 --- /dev/null +++ b/homeassistant/components/anwb_energie/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: ANWB Energie.""" diff --git a/homeassistant/components/anwb_energie/manifest.json b/homeassistant/components/anwb_energie/manifest.json new file mode 100644 index 00000000000..aec78b7b46d --- /dev/null +++ b/homeassistant/components/anwb_energie/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "anwb_energie", + "name": "ANWB Energie", + "integration_type": "virtual", + "supported_by": "energyzero" +} diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py index e7088a09101..3fb8bf00b8a 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -62,7 +62,7 @@ class APCUPSdData: """Initialize the data object.""" self._host = host self._port = port - self.status: dict[str, Any] = {} + self.status: dict[str, str] = {} @property def name(self) -> str | None: @@ -100,7 +100,7 @@ class APCUPSdData: return self.status.get("STATFLAG") @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self, **kwargs): + def update(self, **kwargs: Any) -> None: """Fetch the latest status from APCUPSd. Note that the result dict uses upper case for each resource, where our diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index 7438e3236d4..d45ad561d8d 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -65,8 +65,15 @@ class OnlineStatus(BinarySensorEntity): def update(self) -> None: """Get the status report from APCUPSd and set this entity's state.""" - self._data_service.update() + try: + self._data_service.update() + except OSError as ex: + if self._attr_available: + self._attr_available = False + _LOGGER.exception("Got exception while fetching state: %s", ex) + return + self._attr_available = True key = self.entity_description.key.upper() if key not in self._data_service.status: self._attr_is_on = None diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 08464285853..4e0e46f6392 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, + SensorStateClass, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -39,6 +40,9 @@ SENSORS: dict[str, SensorEntityDescription] = { key="ambtemp", name="UPS Ambient Temperature", icon="mdi:thermometer", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), "apc": SensorEntityDescription( key="apc", @@ -72,12 +76,15 @@ SENSORS: dict[str, SensorEntityDescription] = { name="UPS Battery Voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), "bcharge": SensorEntityDescription( key="bcharge", name="UPS Battery", native_unit_of_measurement=PERCENTAGE, icon="mdi:battery", + device_class=SensorDeviceClass.BATTERY, + state_class=SensorStateClass.MEASUREMENT, ), "cable": SensorEntityDescription( key="cable", @@ -89,6 +96,7 @@ SENSORS: dict[str, SensorEntityDescription] = { key="cumonbatt", name="UPS Total Time on Battery", icon="mdi:timer-outline", + state_class=SensorStateClass.TOTAL_INCREASING, ), "date": SensorEntityDescription( key="date", @@ -155,13 +163,16 @@ SENSORS: dict[str, SensorEntityDescription] = { key="humidity", name="UPS Ambient Humidity", native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, icon="mdi:water-percent", + state_class=SensorStateClass.MEASUREMENT, ), "itemp": SensorEntityDescription( key="itemp", name="UPS Internal Temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, ), "laststest": SensorEntityDescription( key="laststest", @@ -184,18 +195,21 @@ SENSORS: dict[str, SensorEntityDescription] = { name="UPS Line Frequency", native_unit_of_measurement=UnitOfFrequency.HERTZ, device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, ), "linev": SensorEntityDescription( key="linev", name="UPS Input Voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), "loadpct": SensorEntityDescription( key="loadpct", name="UPS Load", native_unit_of_measurement=PERCENTAGE, icon="mdi:gauge", + state_class=SensorStateClass.MEASUREMENT, ), "loadapnt": SensorEntityDescription( key="loadapnt", @@ -288,18 +302,21 @@ SENSORS: dict[str, SensorEntityDescription] = { key="numxfers", name="UPS Transfer Count", icon="mdi:counter", + state_class=SensorStateClass.TOTAL_INCREASING, ), "outcurnt": SensorEntityDescription( key="outcurnt", name="UPS Output Current", native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, ), "outputv": SensorEntityDescription( key="outputv", name="UPS Output Voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, ), "reg1": SensorEntityDescription( key="reg1", @@ -362,16 +379,19 @@ SENSORS: dict[str, SensorEntityDescription] = { key="stesti", name="UPS Self Test Interval", icon="mdi:information-outline", + state_class=SensorStateClass.TOTAL_INCREASING, ), "timeleft": SensorEntityDescription( key="timeleft", name="UPS Time Left", icon="mdi:clock-alert", + state_class=SensorStateClass.MEASUREMENT, ), "tonbatt": SensorEntityDescription( key="tonbatt", name="UPS Time on Battery", icon="mdi:timer-outline", + state_class=SensorStateClass.TOTAL_INCREASING, ), "upsmode": SensorEntityDescription( key="upsmode", @@ -445,7 +465,7 @@ async def async_setup_entry( async_add_entities(entities, update_before_add=True) -def infer_unit(value): +def infer_unit(value: str) -> tuple[str, str | None]: """If the value ends with any of the units from ALL_UNITS. Split the unit off the end of the value and return the value, unit tuple @@ -454,7 +474,7 @@ def infer_unit(value): for unit in ALL_UNITS: if value.endswith(unit): - return value[: -len(unit)], INFERRED_UNITS.get(unit, unit.strip()) + return value.removesuffix(unit), INFERRED_UNITS.get(unit, unit.strip()) return value, None @@ -483,8 +503,15 @@ class APCUPSdSensor(SensorEntity): def update(self) -> None: """Get the latest status and use it to update our sensor state.""" - self._data_service.update() + try: + self._data_service.update() + except OSError as ex: + if self._attr_available: + self._attr_available = False + _LOGGER.exception("Got exception while fetching state: %s", ex) + return + self._attr_available = True key = self.entity_description.key.upper() if key not in self._data_service.status: self._attr_native_value = None diff --git a/homeassistant/components/apcupsd/translations/lv.json b/homeassistant/components/apcupsd/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/apcupsd/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apcupsd/translations/nl.json b/homeassistant/components/apcupsd/translations/nl.json index 622bb5180f9..52de357db9f 100644 --- a/homeassistant/components/apcupsd/translations/nl.json +++ b/homeassistant/components/apcupsd/translations/nl.json @@ -15,5 +15,10 @@ } } } + }, + "issues": { + "deprecated_yaml": { + "title": "De APC UPS Daemon YAML-configuratie wordt verwijderd." + } } } \ No newline at end of file diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 2c9ad84ac42..f2e341d4ab4 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -344,12 +344,7 @@ class AppleTVManager: ATTR_MANUFACTURER: "Apple", ATTR_NAME: self.config_entry.data[CONF_NAME], } - - area = attrs[ATTR_NAME] - name_trailer = f" {DEFAULT_NAME}" - if area.endswith(name_trailer): - area = area[: -len(name_trailer)] - attrs[ATTR_SUGGESTED_AREA] = area + attrs[ATTR_SUGGESTED_AREA] = attrs[ATTR_NAME].removesuffix(f" {DEFAULT_NAME}") if self.atv: dev_info = self.atv.device_info diff --git a/homeassistant/components/apple_tv/translations/lv.json b/homeassistant/components/apple_tv/translations/lv.json new file mode 100644 index 00000000000..862ef1ca431 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/lv.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/manifest.json b/homeassistant/components/aranet/manifest.json index 6dc5cbe903c..fe450f68755 100644 --- a/homeassistant/components/aranet/manifest.json +++ b/homeassistant/components/aranet/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/aranet", "requirements": ["aranet4==2.1.3"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@aschmitz"], "iot_class": "local_push", "integration_type": "device", diff --git a/homeassistant/components/aranet/sensor.py b/homeassistant/components/aranet/sensor.py index 512748fef8d..6ac27b1652b 100644 --- a/homeassistant/components/aranet/sensor.py +++ b/homeassistant/components/aranet/sensor.py @@ -1,8 +1,6 @@ """Support for Aranet sensors.""" from __future__ import annotations -from typing import Optional, Union - from aranet4.client import Aranet4Advertisement from bleak.backends.device import BLEDevice @@ -145,9 +143,7 @@ async def async_setup_entry( class Aranet4BluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of an Aranet sensor.""" diff --git a/homeassistant/components/aranet/translations/hu.json b/homeassistant/components/aranet/translations/hu.json index 2773a11e3dc..a508aa92e75 100644 --- a/homeassistant/components/aranet/translations/hu.json +++ b/homeassistant/components/aranet/translations/hu.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", - "integrations_diabled": "Ezen az eszk\u00f6z\u00f6n nincs enged\u00e9lyezve az integr\u00e1ci\u00f3. K\u00e9rj\u00fck, enged\u00e9lyezze az okosotthon-integr\u00e1ci\u00f3kat az alkalmaz\u00e1s seg\u00edts\u00e9g\u00e9vel, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", + "integrations_diabled": "Ezen az eszk\u00f6z\u00f6n nincs enged\u00e9lyezve az integr\u00e1ci\u00f3. K\u00e9rem, enged\u00e9lyezze az okosotthon-integr\u00e1ci\u00f3kat az alkalmaz\u00e1s seg\u00edts\u00e9g\u00e9vel, \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", "no_devices_found": "Nem tal\u00e1lhat\u00f3 konfigur\u00e1latlan Aranet eszk\u00f6z.", - "outdated_version": "Ez az eszk\u00f6z elavult firmware-t haszn\u00e1l. K\u00e9rj\u00fck, friss\u00edtse legal\u00e1bb 1.2.0-s verzi\u00f3ra, \u00e9s pr\u00f3b\u00e1lja \u00fajra." + "outdated_version": "Ez az eszk\u00f6z elavult firmware-t haszn\u00e1l. K\u00e9rem, friss\u00edtse legal\u00e1bb 1.2.0-s verzi\u00f3ra, \u00e9s pr\u00f3b\u00e1lja \u00fajra." }, "error": { "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" diff --git a/homeassistant/components/aranet/translations/lv.json b/homeassistant/components/aranet/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/aranet/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aranet/translations/nl.json b/homeassistant/components/aranet/translations/nl.json index 64f3286888c..a0719f96188 100644 --- a/homeassistant/components/aranet/translations/nl.json +++ b/homeassistant/components/aranet/translations/nl.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Apparaat is al geconfigureerd" + "already_configured": "Apparaat is al geconfigureerd", + "no_devices_found": "Geen niet-geconfigureerde Aranet apparaten gevonden.", + "outdated_version": "Dit apparaat gebruikt verouderde firmware. Werk het bij naar minstens v1.2.0 en probeer het opnieuw." }, "error": { "unknown": "Onverwachte fout" diff --git a/homeassistant/components/aranet/translations/tr.json b/homeassistant/components/aranet/translations/tr.json new file mode 100644 index 00000000000..a234195ed39 --- /dev/null +++ b/homeassistant/components/aranet/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "integrations_diabled": "Bu cihazda entegrasyon etkinle\u015ftirilmemi\u015f. L\u00fctfen uygulamay\u0131 kullanarak ak\u0131ll\u0131 ev entegrasyonlar\u0131n\u0131 etkinle\u015ftirin ve tekrar deneyin.", + "no_devices_found": "Yap\u0131land\u0131r\u0131lmam\u0131\u015f Aranet cihaz\u0131 bulunamad\u0131.", + "outdated_version": "Bu cihaz eski \u00fcretici yaz\u0131l\u0131m\u0131 kullan\u0131yor. L\u00fctfen en az v1.2.0 s\u00fcr\u00fcm\u00fcne g\u00fcncelleyin ve tekrar deneyin." + }, + "error": { + "unknown": "Beklenmeyen hata" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} 'i kurmak istiyor musunuz?" + }, + "user": { + "data": { + "address": "Cihaz" + }, + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/lv.json b/homeassistant/components/arcam_fmj/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/arcam_fmj/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/asuswrt/translations/el.json b/homeassistant/components/asuswrt/translations/el.json index 6a380b23152..cd30521bcbe 100644 --- a/homeassistant/components/asuswrt/translations/el.json +++ b/homeassistant/components/asuswrt/translations/el.json @@ -19,7 +19,7 @@ "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", + "port": "\u0398\u03cd\u03c1\u03b1 (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03bf\u03c5)", "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" diff --git a/homeassistant/components/asuswrt/translations/uk.json b/homeassistant/components/asuswrt/translations/uk.json new file mode 100644 index 00000000000..2aed6be91ba --- /dev/null +++ b/homeassistant/components/asuswrt/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/lv.json b/homeassistant/components/atag/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/atag/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index 32b23e1329c..a3cc18ab9c0 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -22,7 +22,10 @@ async def async_setup_entry( ) -> None: """Set up August cameras.""" data: AugustData = hass.data[DOMAIN][config_entry.entry_id] - session = aiohttp_client.async_get_clientsession(hass) + # Create an aiohttp session instead of using the default one since the + # default one is likely to trigger august's WAF if another integration + # is also using Cloudflare + session = aiohttp_client.async_create_clientsession(hass) async_add_entities( AugustCamera(data, doorbell, session, DEFAULT_TIMEOUT) for doorbell in data.doorbells diff --git a/homeassistant/components/august/entity.py b/homeassistant/components/august/entity.py index 209747da0be..8dab470376b 100644 --- a/homeassistant/components/august/entity.py +++ b/homeassistant/components/august/entity.py @@ -70,7 +70,5 @@ def _remove_device_types(name, device_types): """ lower_name = name.lower() for device_type in device_types: - device_type_with_space = f" {device_type}" - if lower_name.endswith(device_type_with_space): - lower_name = lower_name[: -len(device_type_with_space)] + lower_name = lower_name.removesuffix(f" {device_type}") return name[: len(lower_name)] diff --git a/homeassistant/components/august/gateway.py b/homeassistant/components/august/gateway.py index ac7b81a7117..32004158f7f 100644 --- a/homeassistant/components/august/gateway.py +++ b/homeassistant/components/august/gateway.py @@ -30,7 +30,10 @@ class AugustGateway: def __init__(self, hass): """Init the connection.""" - self._aiohttp_session = aiohttp_client.async_get_clientsession(hass) + # Create an aiohttp session instead of using the default one since the + # default one is likely to trigger august's WAF if another integration + # is also using Cloudflare + self._aiohttp_session = aiohttp_client.async_create_clientsession(hass) self._token_refresh_lock = asyncio.Lock() self._access_token_cache_file = None self._hass = hass diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 773a341954c..cf00616b65f 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["yalexs==1.2.6", "yalexs_ble==1.12.5"], + "requirements": ["yalexs==1.2.6", "yalexs_ble==1.12.8"], "codeowners": ["@bdraco"], "dhcp": [ { diff --git a/homeassistant/components/august/translations/sk.json b/homeassistant/components/august/translations/sk.json index e293efb90a9..424909fb2fd 100644 --- a/homeassistant/components/august/translations/sk.json +++ b/homeassistant/components/august/translations/sk.json @@ -23,7 +23,7 @@ "password": "Heslo", "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" }, - "description": "Ak je sp\u00f4sob prihl\u00e1senia \u201ee-mail\u201c, pou\u017e\u00edvate\u013esk\u00e9 meno je e-mailov\u00e1 adresa. Ak je met\u00f3da prihl\u00e1senia \u201etelef\u00f3n\u201c, pou\u017e\u00edvate\u013esk\u00e9 meno je telef\u00f3nne \u010d\u00edslo vo form\u00e1te \u201e+NNNNNNNNNN\u201c.", + "description": "Ak je sp\u00f4sob prihl\u00e1senia 'e-mail', pou\u017e\u00edvate\u013esk\u00e9 meno je e-mailov\u00e1 adresa. Ak je met\u00f3da prihl\u00e1senia 'telef\u00f3n', pou\u017e\u00edvate\u013esk\u00e9 meno je telef\u00f3nne \u010d\u00edslo vo form\u00e1te '+NNNNNNNNNN'.", "title": "Nastavenie \u00fa\u010dtu August" }, "validation": { diff --git a/homeassistant/components/august/translations/tr.json b/homeassistant/components/august/translations/tr.json index 93c21154faa..44080662e6a 100644 --- a/homeassistant/components/august/translations/tr.json +++ b/homeassistant/components/august/translations/tr.json @@ -24,7 +24,7 @@ "username": "Kullan\u0131c\u0131 Ad\u0131" }, "description": "Giri\u015f Y\u00f6ntemi 'e-posta' ise, Kullan\u0131c\u0131 Ad\u0131 e-posta adresidir. Giri\u015f Y\u00f6ntemi 'telefon' ise, Kullan\u0131c\u0131 Ad\u0131 '+ NNNNNNNNN' bi\u00e7imindeki telefon numaras\u0131d\u0131r.", - "title": "Bir August hesab\u0131 olu\u015fturun" + "title": "Bir A\u011fustos hesab\u0131 olu\u015fturun" }, "validation": { "data": { diff --git a/homeassistant/components/august/translations/uk.json b/homeassistant/components/august/translations/uk.json index 5f4729d02b2..e0aecc19b45 100644 --- a/homeassistant/components/august/translations/uk.json +++ b/homeassistant/components/august/translations/uk.json @@ -10,6 +10,15 @@ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { + "reauth_validate": { + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}." + }, + "user_validate": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u042f\u043a\u0449\u043e \u041c\u0435\u0442\u043e\u0434\u043e\u043c \u0432\u0445\u043e\u0434\u0443 \u0454 \"\u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430 \u043f\u043e\u0448\u0442\u0430\", \u0456\u043c'\u044f\u043c \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0454 \u0430\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438. \u042f\u043a\u0449\u043e \u043c\u0435\u0442\u043e\u0434 \u0432\u0445\u043e\u0434\u0443 \u2014 \u00ab\u0442\u0435\u043b\u0435\u0444\u043e\u043d\u00bb, \u0456\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u2014 \u0446\u0435 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0443 \u0443 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 \u00ab+NNNNNNNNN\u00bb." + }, "validation": { "data": { "code": "\u041a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f" diff --git a/homeassistant/components/aurora/strings.json b/homeassistant/components/aurora/strings.json index 92b8422d524..9beb9c7906d 100644 --- a/homeassistant/components/aurora/strings.json +++ b/homeassistant/components/aurora/strings.json @@ -10,6 +10,9 @@ } } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } diff --git a/homeassistant/components/aurora/translations/bg.json b/homeassistant/components/aurora/translations/bg.json index fea56662ef3..74c8ecb18f8 100644 --- a/homeassistant/components/aurora/translations/bg.json +++ b/homeassistant/components/aurora/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, diff --git a/homeassistant/components/aurora/translations/ca.json b/homeassistant/components/aurora/translations/ca.json index 99db9855e74..7c57bf40903 100644 --- a/homeassistant/components/aurora/translations/ca.json +++ b/homeassistant/components/aurora/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, diff --git a/homeassistant/components/aurora/translations/de.json b/homeassistant/components/aurora/translations/de.json index 838673e8d60..61e82c4fb7a 100644 --- a/homeassistant/components/aurora/translations/de.json +++ b/homeassistant/components/aurora/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, diff --git a/homeassistant/components/aurora/translations/en.json b/homeassistant/components/aurora/translations/en.json index e3e36574608..dd418ef0daf 100644 --- a/homeassistant/components/aurora/translations/en.json +++ b/homeassistant/components/aurora/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Service is already configured" + }, "error": { "cannot_connect": "Failed to connect" }, diff --git a/homeassistant/components/aurora/translations/et.json b/homeassistant/components/aurora/translations/et.json index 80fb6b21736..07b13e3185c 100644 --- a/homeassistant/components/aurora/translations/et.json +++ b/homeassistant/components/aurora/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud" + }, "error": { "cannot_connect": "\u00dchendus nurjus" }, diff --git a/homeassistant/components/aurora/translations/no.json b/homeassistant/components/aurora/translations/no.json index 1d22d6cd08b..ec0bcbfa969 100644 --- a/homeassistant/components/aurora/translations/no.json +++ b/homeassistant/components/aurora/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, "error": { "cannot_connect": "Tilkobling mislyktes" }, diff --git a/homeassistant/components/aurora/translations/ru.json b/homeassistant/components/aurora/translations/ru.json index 20e8f4a184b..b103f2a1e51 100644 --- a/homeassistant/components/aurora/translations/ru.json +++ b/homeassistant/components/aurora/translations/ru.json @@ -1,5 +1,8 @@ { "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." }, diff --git a/homeassistant/components/aurora/translations/zh-Hant.json b/homeassistant/components/aurora/translations/zh-Hant.json index d12e8332373..981bc55cf6d 100644 --- a/homeassistant/components/aurora/translations/zh-Hant.json +++ b/homeassistant/components/aurora/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, diff --git a/homeassistant/components/aurora_abb_powerone/translations/lv.json b/homeassistant/components/aurora_abb_powerone/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/aurora_abb_powerone/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aussie_broadband/translations/uk.json b/homeassistant/components/aussie_broadband/translations/uk.json new file mode 100644 index 00000000000..2881e205e50 --- /dev/null +++ b/homeassistant/components/aussie_broadband/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041e\u043d\u043e\u0432\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index ac5035de645..01ce39f5fcc 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -127,7 +127,7 @@ from __future__ import annotations from collections.abc import Callable from datetime import datetime, timedelta from http import HTTPStatus -from typing import Any, Optional, cast +from typing import Any, cast import uuid from aiohttp import web @@ -159,7 +159,7 @@ from . import indieauth, login_flow, mfa_setup_flow DOMAIN = "auth" StoreResultType = Callable[[str, Credentials], str] -RetrieveResultType = Callable[[str, str], Optional[Credentials]] +RetrieveResultType = Callable[[str, str], Credentials | None] @bind_hass diff --git a/homeassistant/components/auth/translations/hu.json b/homeassistant/components/auth/translations/hu.json index 99504c1b7a7..3f1fbd29156 100644 --- a/homeassistant/components/auth/translations/hu.json +++ b/homeassistant/components/auth/translations/hu.json @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "Ahhoz, hogy haszn\u00e1lhassa a k\u00e9tfaktoros hiteles\u00edt\u00e9st id\u0151alap\u00fa egyszeri jelszavakkal, szkennelje be a QR k\u00f3dot a hiteles\u00edt\u00e9si applik\u00e1ci\u00f3j\u00e1val. Ha m\u00e9g nincs ilyenje, akkor aj\u00e1nljuk figyelm\u00e9be a [Google Hiteles\u00edt\u0151](https://support.google.com/accounts/answer/1066447)t vagy az [Authy](https://authy.com/)-t.\n\n{qr_code}\n\nA k\u00f3d beolvas\u00e1sa ut\u00e1n adja meg a hat sz\u00e1mjegy\u0171 k\u00f3dot az applik\u00e1ci\u00f3b\u00f3l a telep\u00edt\u00e9s ellen\u0151rz\u00e9s\u00e9hez. Ha probl\u00e9m\u00e1ba \u00fctk\u00f6zne a QR k\u00f3d beolvas\u00e1s\u00e1n\u00e1l, akkor ind\u00edtson egy k\u00e9zi be\u00e1ll\u00edt\u00e1st a **`{code}`** k\u00f3ddal.", + "description": "Ahhoz, hogy haszn\u00e1lhassa a k\u00e9tfaktoros hiteles\u00edt\u00e9st id\u0151alap\u00fa egyszeri jelszavakkal, szkennelje be a QR k\u00f3dot a hiteles\u00edt\u00e9si applik\u00e1ci\u00f3j\u00e1val. Ha m\u00e9g nincs ilyenje, akkor aj\u00e1nljuk figyelm\u00e9be a [Google Hiteles\u00edt\u0151t](https://support.google.com/accounts/answer/1066447) vagy az [Authy](https://authy.com/)-t.\n\n{qr_code}\n\nA k\u00f3d beolvas\u00e1sa ut\u00e1n adja meg a hat sz\u00e1mjegy\u0171 k\u00f3dot az applik\u00e1ci\u00f3b\u00f3l a telep\u00edt\u00e9s ellen\u0151rz\u00e9s\u00e9hez. Ha probl\u00e9m\u00e1ba \u00fctk\u00f6zne a QR k\u00f3d beolvas\u00e1s\u00e1n\u00e1l, akkor ind\u00edtson egy k\u00e9zi be\u00e1ll\u00edt\u00e1st a **`{code}`** k\u00f3ddal.", "title": "K\u00e9tfaktoros hiteles\u00edt\u00e9s be\u00e1ll\u00edt\u00e1sa TOTP haszn\u00e1lat\u00e1val" } }, diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index c0bbf51138a..f005c1dfce2 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -870,7 +870,7 @@ async def _async_process_if( for index, check in enumerate(checks): try: with trace_path(["condition", str(index)]): - if not check(hass, variables): + if check(hass, variables) is False: return False except ConditionError as ex: errors.append( diff --git a/homeassistant/components/automation/translations/hu.json b/homeassistant/components/automation/translations/hu.json index 5ebb2c43351..b47a2f390d2 100644 --- a/homeassistant/components/automation/translations/hu.json +++ b/homeassistant/components/automation/translations/hu.json @@ -4,7 +4,7 @@ "fix_flow": { "step": { "confirm": { - "description": "A \"{name}\" (`{entity_id}`) automatizmusnak van egy m\u0171velete, amely egy ismeretlen szolg\u00e1ltat\u00e1st h\u00edv meg: `{service}`.\n\nEz a hiba megakad\u00e1lyozza az automatizmus megfelel\u0151 m\u0171k\u00f6d\u00e9s\u00e9t. Lehet, hogy ez a szolg\u00e1ltat\u00e1s m\u00e1r nem \u00e9rhet\u0151 el, vagy tal\u00e1n egy el\u00edr\u00e1s okozta.\n\nA hiba kijav\u00edt\u00e1s\u00e1hoz [szerkessze az automatizmust]({edit}), \u00e9s t\u00e1vol\u00edtsa el a szolg\u00e1ltat\u00e1st h\u00edv\u00f3 m\u0171veletet.\n\nKattintson az al\u00e1bbi MEHET gombra annak meger\u0151s\u00edt\u00e9s\u00e9hez, hogy jav\u00edtotta-e ezt az automatiz\u00e1l\u00e1st.", + "description": "A \"{name}\" (`{entity_id}`) automatizmusnak van egy m\u0171velete, amely egy ismeretlen szolg\u00e1ltat\u00e1st h\u00edv meg: `{service}`.\n\nEz a hiba megakad\u00e1lyozza az automatizmus megfelel\u0151 m\u0171k\u00f6d\u00e9s\u00e9t. Lehet, hogy ez a szolg\u00e1ltat\u00e1s m\u00e1r nem \u00e9rhet\u0151 el, vagy tal\u00e1n egy el\u00edr\u00e1s okozta.\n\nA hiba kijav\u00edt\u00e1s\u00e1hoz [szerkessze az automatizmust]({edit}), \u00e9s t\u00e1vol\u00edtsa el vagy \u00edrja \u00e1t a szolg\u00e1ltat\u00e1st h\u00edv\u00f3 m\u0171veletet.\n\nKattintson az al\u00e1bbi MEHET gombra annak meger\u0151s\u00edt\u00e9s\u00e9hez, hogy jav\u00edtotta-e ezt a hib\u00e1t.", "title": "{name} egy ismeretlen szolg\u00e1ltat\u00e1st haszn\u00e1l" } } diff --git a/homeassistant/components/automation/translations/nl.json b/homeassistant/components/automation/translations/nl.json index 4e36f46b980..4991a8bdd64 100644 --- a/homeassistant/components/automation/translations/nl.json +++ b/homeassistant/components/automation/translations/nl.json @@ -1,6 +1,13 @@ { "issues": { "service_not_found": { + "fix_flow": { + "step": { + "confirm": { + "title": "{name} gebruikt een onbekende service" + } + } + }, "title": "{name} gebruikt een onbekende service" } }, diff --git a/homeassistant/components/awair/translations/el.json b/homeassistant/components/awair/translations/el.json index 849dc24270f..18b93446312 100644 --- a/homeassistant/components/awair/translations/el.json +++ b/homeassistant/components/awair/translations/el.json @@ -25,7 +25,7 @@ "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {model} ({device_id});" }, "local": { - "description": "\u03a4\u03bf Awair Local API \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ce\u03bd\u03c4\u03b1\u03c2 \u03b1\u03c5\u03c4\u03ac \u03c4\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1: {url}" + "description": "\u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 [\u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2]( {url} ) \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Awair Local API. \n\n \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae \u03cc\u03c4\u03b1\u03bd \u03c4\u03b5\u03bb\u03b5\u03b9\u03ce\u03c3\u03b5\u03c4\u03b5." }, "local_pick": { "data": { @@ -41,7 +41,7 @@ "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": { - "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", + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf\u03c0\u03b9\u03ba\u03cc \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03ba\u03b1\u03bb\u03cd\u03c4\u03b5\u03c1\u03b7 \u03b5\u03bc\u03c0\u03b5\u03b9\u03c1\u03af\u03b1. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf cloud \u03bc\u03cc\u03bd\u03bf \u03b5\u03ac\u03bd \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b4\u03b5\u03bc\u03ad\u03bd\u03b7 \u03c3\u03c4\u03bf \u03af\u03b4\u03b9\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf \u03bc\u03b5 \u03c4\u03bf Home Assistant \u03ae \u03b5\u03ac\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03b1\u03bb\u03b1\u03b9\u03bf\u03cd \u03c4\u03cd\u03c0\u03bf\u03c5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae.", "menu_options": { "cloud": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03ad\u03c3\u03c9 cloud", "local": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ac (\u03c0\u03c1\u03bf\u03c4\u03b9\u03bc\u03ac\u03c4\u03b1\u03b9)" diff --git a/homeassistant/components/awair/translations/lv.json b/homeassistant/components/awair/translations/lv.json new file mode 100644 index 00000000000..9eea6cd040d --- /dev/null +++ b/homeassistant/components/awair/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured_device": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/nl.json b/homeassistant/components/awair/translations/nl.json index 6b7591ec79b..8acba8fee26 100644 --- a/homeassistant/components/awair/translations/nl.json +++ b/homeassistant/components/awair/translations/nl.json @@ -20,6 +20,9 @@ "email": "E-mail" } }, + "discovery_confirm": { + "description": "Wil je {model} ({device_id}) instellen?" + }, "local_pick": { "data": { "device": "Apparaat", @@ -30,7 +33,8 @@ "data": { "access_token": "Toegangstoken", "email": "E-mail" - } + }, + "description": "Voer uw Awair ontwikkelaarstoegangstoken opnieuw in." }, "user": { "description": "U moet zich registreren voor een Awair-toegangstoken voor ontwikkelaars op: https://developer.getawair.com/onboard/login", diff --git a/homeassistant/components/awair/translations/tr.json b/homeassistant/components/awair/translations/tr.json index 7f9f025a566..15c5267da4a 100644 --- a/homeassistant/components/awair/translations/tr.json +++ b/homeassistant/components/awair/translations/tr.json @@ -22,7 +22,7 @@ "description": "Bir Awair geli\u015ftirici eri\u015fim belirtecine \u015fu adresten kaydolmal\u0131s\u0131n\u0131z: {url}" }, "discovery_confirm": { - "description": "{model} ( {device_id} ) kurulumunu yapmak istiyor musunuz?" + "description": "{model} ( {device_id} ) kurmak istiyor musunuz?" }, "local": { "description": "Awair Yerel API'sinin nas\u0131l etkinle\u015ftirilece\u011fiyle ilgili [bu talimatlar\u0131]( {url} ) uygulay\u0131n. \n\n \u0130\u015finiz bitti\u011finde g\u00f6nder'i t\u0131klay\u0131n." diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 56ddbc6d8c5..c4c05f1c515 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -2,11 +2,9 @@ import logging from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_DEVICE, CONF_MAC, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.device_registry import format_mac -from homeassistant.helpers.entity_registry import async_migrate_entries from .const import DOMAIN as AXIS_DOMAIN, PLATFORMS from .device import AxisNetworkDevice, get_axis_device @@ -27,7 +25,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b raise ConfigEntryAuthFailed from err device = AxisNetworkDevice(hass, config_entry, api) - hass.data[AXIS_DOMAIN][config_entry.unique_id] = device + hass.data[AXIS_DOMAIN][config_entry.entry_id] = device await device.async_update_device_registry() await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) device.async_setup_events() @@ -42,7 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload Axis device config entry.""" - device: AxisNetworkDevice = hass.data[AXIS_DOMAIN].pop(config_entry.unique_id) + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN].pop(config_entry.entry_id) return await device.async_reset() @@ -50,34 +48,10 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> """Migrate old entry.""" _LOGGER.debug("Migrating from version %s", config_entry.version) - # Flatten configuration but keep old data if user rollbacks HASS prior to 0.106 - if config_entry.version == 1: - unique_id = config_entry.data[CONF_MAC] - data = {**config_entry.data, **config_entry.data[CONF_DEVICE]} - hass.config_entries.async_update_entry( - config_entry, unique_id=unique_id, data=data - ) - config_entry.version = 2 - - # Normalise MAC address of device which also affects entity unique IDs - if config_entry.version == 2 and (old_unique_id := config_entry.unique_id): - new_unique_id = format_mac(old_unique_id) - - @callback - def update_unique_id(entity_entry): - """Update unique ID of entity entry.""" - return { - "new_unique_id": entity_entry.unique_id.replace( - old_unique_id, new_unique_id - ) - } - - if old_unique_id != new_unique_id: - await async_migrate_entries(hass, config_entry.entry_id, update_unique_id) - - hass.config_entries.async_update_entry( - config_entry, unique_id=new_unique_id - ) + if config_entry.version != 3: + # Home Assistant 2023.2 + config_entry.version = 3 + hass.config_entries.async_update_entry(config_entry) _LOGGER.info("Migration to version %s successful", config_entry.version) diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py deleted file mode 100644 index 2f21d900662..00000000000 --- a/homeassistant/components/axis/axis_base.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Base classes for Axis entities.""" -from axis.event_stream import AxisEvent - -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import DeviceInfo, Entity - -from .const import DOMAIN as AXIS_DOMAIN -from .device import AxisNetworkDevice - - -class AxisEntityBase(Entity): - """Base common to all Axis entities.""" - - _attr_has_entity_name = True - - def __init__(self, device: AxisNetworkDevice) -> None: - """Initialize the Axis event.""" - self.device = device - - self._attr_device_info = DeviceInfo( - identifiers={(AXIS_DOMAIN, device.unique_id)} - ) - - async def async_added_to_hass(self) -> None: - """Subscribe device events.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, self.device.signal_reachable, self.update_callback - ) - ) - - @property - def available(self) -> bool: - """Return True if device is available.""" - return self.device.available - - @callback - def update_callback(self, no_delay=None) -> None: - """Update the entities state.""" - self.async_write_ha_state() - - -class AxisEventBase(AxisEntityBase): - """Base common to all Axis entities from event stream.""" - - _attr_should_poll = False - - def __init__(self, event: AxisEvent, device: AxisNetworkDevice) -> None: - """Initialize the Axis event.""" - super().__init__(device) - self.event = event - - self._attr_name = f"{event.TYPE} {event.id}" - self._attr_unique_id = f"{device.unique_id}-{event.topic}-{event.id}" - - self._attr_device_class = event.CLASS - - async def async_added_to_hass(self) -> None: - """Subscribe sensors events.""" - self.event.register_callback(self.update_callback) - await super().async_added_to_hass() - - async def async_will_remove_from_hass(self) -> None: - """Disconnect device object when removed.""" - self.event.remove_callback(self.update_callback) diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index 3e90f5ff2a1..729d69ed45b 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -1,23 +1,10 @@ """Support for Axis binary sensors.""" from __future__ import annotations +from collections.abc import Callable from datetime import timedelta -from axis.event_stream import ( - CLASS_INPUT, - CLASS_LIGHT, - CLASS_MOTION, - CLASS_OUTPUT, - CLASS_PTZ, - CLASS_SOUND, - AxisBinaryEvent, - AxisEvent, - FenceGuard, - LoiteringGuard, - MotionGuard, - ObjectAnalytics, - Vmd4, -) +from axis.models.event import Event, EventGroup, EventOperation, EventTopic from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -25,22 +12,36 @@ from homeassistant.components.binary_sensor import ( ) 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 homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -from .axis_base import AxisEventBase from .const import DOMAIN as AXIS_DOMAIN from .device import AxisNetworkDevice +from .entity import AxisEventEntity DEVICE_CLASS = { - CLASS_INPUT: BinarySensorDeviceClass.CONNECTIVITY, - CLASS_LIGHT: BinarySensorDeviceClass.LIGHT, - CLASS_MOTION: BinarySensorDeviceClass.MOTION, - CLASS_SOUND: BinarySensorDeviceClass.SOUND, + EventGroup.INPUT: BinarySensorDeviceClass.CONNECTIVITY, + EventGroup.LIGHT: BinarySensorDeviceClass.LIGHT, + EventGroup.MOTION: BinarySensorDeviceClass.MOTION, + EventGroup.SOUND: BinarySensorDeviceClass.SOUND, } +EVENT_TOPICS = ( + EventTopic.DAY_NIGHT_VISION, + EventTopic.FENCE_GUARD, + EventTopic.LOITERING_GUARD, + EventTopic.MOTION_DETECTION, + EventTopic.MOTION_DETECTION_3, + EventTopic.MOTION_DETECTION_4, + EventTopic.MOTION_GUARD, + EventTopic.OBJECT_ANALYTICS, + EventTopic.PIR, + EventTopic.PORT_INPUT, + EventTopic.PORT_SUPERVISED_INPUT, + EventTopic.SOUND_TRIGGER_LEVEL, +) + async def async_setup_entry( hass: HomeAssistant, @@ -48,41 +49,37 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Axis binary sensor.""" - device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.entry_id] @callback - def async_add_sensor(event_id): - """Add binary sensor from Axis device.""" - event: AxisEvent = device.api.event[event_id] + def async_create_entity(event: Event) -> None: + """Create Axis binary sensor entity.""" + async_add_entities([AxisBinarySensor(event, device)]) - if event.CLASS not in (CLASS_OUTPUT, CLASS_PTZ) and not ( - event.CLASS == CLASS_LIGHT and event.TYPE == "Light" - ): - async_add_entities([AxisBinarySensor(event, device)]) - - config_entry.async_on_unload( - async_dispatcher_connect(hass, device.signal_new_event, async_add_sensor) + device.api.event.subscribe( + async_create_entity, + topic_filter=EVENT_TOPICS, + operation_filter=EventOperation.INITIALIZED, ) -class AxisBinarySensor(AxisEventBase, BinarySensorEntity): +class AxisBinarySensor(AxisEventEntity, BinarySensorEntity): """Representation of a binary Axis event.""" - event: AxisBinaryEvent - - def __init__(self, event: AxisEvent, device: AxisNetworkDevice) -> None: + def __init__(self, event: Event, device: AxisNetworkDevice) -> None: """Initialize the Axis binary sensor.""" super().__init__(event, device) - self.cancel_scheduled_update = None + self.cancel_scheduled_update: Callable[[], None] | None = None - self._attr_device_class = DEVICE_CLASS.get(self.event.CLASS) + self._attr_device_class = DEVICE_CLASS.get(event.group) + self._attr_is_on = event.is_tripped + + self._set_name(event) @callback - def update_callback(self, no_delay=False): - """Update the sensor's state, if needed. - - Parameter no_delay is True when device_event_reachable is sent. - """ + def async_event_callback(self, event: Event) -> None: + """Update the sensor's state, if needed.""" + self._attr_is_on = event.is_tripped @callback def scheduled_update(now): @@ -94,7 +91,7 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): self.cancel_scheduled_update() self.cancel_scheduled_update = None - if self.is_on or self.device.option_trigger_time == 0 or no_delay: + if self.is_on or self.device.option_trigger_time == 0: self.async_write_ha_state() return @@ -104,35 +101,30 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): utcnow() + timedelta(seconds=self.device.option_trigger_time), ) - @property - def is_on(self) -> bool: - """Return true if event is active.""" - return self.event.is_tripped - - @property - def name(self) -> str | None: - """Return the name of the event.""" + @callback + def _set_name(self, event: Event) -> None: + """Set binary sensor name.""" if ( - self.event.CLASS == CLASS_INPUT - and self.event.id in self.device.api.vapix.ports - and self.device.api.vapix.ports[self.event.id].name + event.group == EventGroup.INPUT + and event.id in self.device.api.vapix.ports + and self.device.api.vapix.ports[event.id].name ): - return self.device.api.vapix.ports[self.event.id].name + self._attr_name = self.device.api.vapix.ports[event.id].name - if self.event.CLASS == CLASS_MOTION: + elif event.group == EventGroup.MOTION: - for event_class, event_data in ( - (FenceGuard, self.device.api.vapix.fence_guard), - (LoiteringGuard, self.device.api.vapix.loitering_guard), - (MotionGuard, self.device.api.vapix.motion_guard), - (ObjectAnalytics, self.device.api.vapix.object_analytics), - (Vmd4, self.device.api.vapix.vmd4), + for event_topic, event_data in ( + (EventTopic.FENCE_GUARD, self.device.api.vapix.fence_guard), + (EventTopic.LOITERING_GUARD, self.device.api.vapix.loitering_guard), + (EventTopic.MOTION_GUARD, self.device.api.vapix.motion_guard), + (EventTopic.OBJECT_ANALYTICS, self.device.api.vapix.object_analytics), + (EventTopic.MOTION_DETECTION_4, self.device.api.vapix.vmd4), ): - if ( - isinstance(self.event, event_class) - and event_data - and self.event.id in event_data - ): - return f"{self.event.TYPE} {event_data[self.event.id].name}" - return self._attr_name + if ( + event.topic_base == event_topic + and event_data + and event.id in event_data + ): + self._attr_name = f"{self._event_type} {event_data[event.id].name}" + break diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index 992410d9725..c593c4fa419 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -9,9 +9,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .axis_base import AxisEntityBase from .const import DEFAULT_STREAM_PROFILE, DEFAULT_VIDEO_SOURCE, DOMAIN as AXIS_DOMAIN from .device import AxisNetworkDevice +from .entity import AxisEntity async def async_setup_entry( @@ -22,7 +22,7 @@ async def async_setup_entry( """Set up the Axis camera video stream.""" filter_urllib3_logging() - device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.entry_id] if not device.api.vapix.params.image_format: return @@ -30,14 +30,14 @@ async def async_setup_entry( async_add_entities([AxisCamera(device)]) -class AxisCamera(AxisEntityBase, MjpegCamera): +class AxisCamera(AxisEntity, MjpegCamera): """Representation of a Axis camera.""" _attr_supported_features = CameraEntityFeature.STREAM def __init__(self, device: AxisNetworkDevice) -> None: """Initialize Axis Communications camera component.""" - AxisEntityBase.__init__(self, device) + AxisEntity.__init__(self, device) MjpegCamera.__init__( self, diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 1fb9b9488fa..75354bb9884 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -232,7 +232,7 @@ class AxisOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Manage the Axis device options.""" - self.device = self.hass.data[AXIS_DOMAIN][self.config_entry.unique_id] + self.device = self.hass.data[AXIS_DOMAIN][self.config_entry.entry_id] return await self.async_step_configure_stream() async def async_step_configure_stream( diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index ea7edcf0483..2dce4b7692a 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -8,9 +8,8 @@ import async_timeout import axis from axis.configuration import Configuration from axis.errors import Unauthorized -from axis.event_stream import OPERATION_INITIALIZED -from axis.mqtt import mqtt_json_to_event -from axis.streammanager import SIGNAL_PLAYING, STATE_STOPPED +from axis.stream_manager import Signal, State +from axis.vapix.interfaces.mqtt import mqtt_json_to_event from homeassistant.components import mqtt from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN @@ -129,11 +128,6 @@ class AxisNetworkDevice: """Device specific event to signal a change in connection status.""" return f"axis_reachable_{self.unique_id}" - @property - def signal_new_event(self): - """Device specific event to signal new device event available.""" - return f"axis_new_event_{self.unique_id}" - @property def signal_new_address(self): """Device specific event to signal a change in device address.""" @@ -149,15 +143,9 @@ class AxisNetworkDevice: Only signal state change if state change is true. """ - if self.available != (status == SIGNAL_PLAYING): + if self.available != (status == Signal.PLAYING): self.available = not self.available - async_dispatcher_send(self.hass, self.signal_reachable, True) - - @callback - def async_event_callback(self, action, event_id): - """Call to configure events when initialized on event stream.""" - if action == OPERATION_INITIALIZED: - async_dispatcher_send(self.hass, self.signal_new_event, event_id) + async_dispatcher_send(self.hass, self.signal_reachable) @staticmethod async def async_new_address_callback( @@ -169,7 +157,7 @@ class AxisNetworkDevice: This is a static method because a class method (bound method), can not be used with weak references. """ - device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][entry.entry_id] device.api.config.host = device.host async_dispatcher_send(hass, device.signal_new_address) @@ -208,7 +196,7 @@ class AxisNetworkDevice: self.disconnect_from_stream() event = mqtt_json_to_event(message.payload) - self.api.event.update([event]) + self.api.event.handler(event) # Setup and teardown methods @@ -219,7 +207,7 @@ class AxisNetworkDevice: self.api.stream.connection_status_callback.append( self.async_connection_status_callback ) - self.api.enable_events(event_callback=self.async_event_callback) + self.api.enable_events() self.api.stream.start() if self.api.vapix.mqtt: @@ -228,7 +216,7 @@ class AxisNetworkDevice: @callback def disconnect_from_stream(self) -> None: """Stop stream.""" - if self.api.stream.state != STATE_STOPPED: + if self.api.stream.state != State.STOPPED: self.api.stream.connection_status_callback.clear() self.api.stream.stop() diff --git a/homeassistant/components/axis/diagnostics.py b/homeassistant/components/axis/diagnostics.py index 1c805e8f35b..277f24513de 100644 --- a/homeassistant/components/axis/diagnostics.py +++ b/homeassistant/components/axis/diagnostics.py @@ -20,7 +20,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.entry_id] diag: dict[str, Any] = {} diag["config"] = async_redact_data(config_entry.as_dict(), REDACT_CONFIG) diff --git a/homeassistant/components/axis/entity.py b/homeassistant/components/axis/entity.py new file mode 100644 index 00000000000..e511ee72d1b --- /dev/null +++ b/homeassistant/components/axis/entity.py @@ -0,0 +1,96 @@ +"""Base classes for Axis entities.""" + +from abc import abstractmethod + +from axis.models.event import Event, EventTopic + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo, Entity + +from .const import DOMAIN as AXIS_DOMAIN +from .device import AxisNetworkDevice + +TOPIC_TO_EVENT_TYPE = { + EventTopic.DAY_NIGHT_VISION: "DayNight", + EventTopic.FENCE_GUARD: "Fence Guard", + EventTopic.LIGHT_STATUS: "Light", + EventTopic.LOITERING_GUARD: "Loitering Guard", + EventTopic.MOTION_DETECTION: "Motion", + EventTopic.MOTION_DETECTION_3: "VMD3", + EventTopic.MOTION_DETECTION_4: "VMD4", + EventTopic.MOTION_GUARD: "Motion Guard", + EventTopic.OBJECT_ANALYTICS: "Object Analytics", + EventTopic.PIR: "PIR", + EventTopic.PORT_INPUT: "Input", + EventTopic.PORT_SUPERVISED_INPUT: "Supervised Input", + EventTopic.PTZ_IS_MOVING: "is_moving", + EventTopic.PTZ_ON_PRESET: "on_preset", + EventTopic.RELAY: "Relay", + EventTopic.SOUND_TRIGGER_LEVEL: "Sound", +} + + +class AxisEntity(Entity): + """Base common to all Axis entities.""" + + _attr_has_entity_name = True + + def __init__(self, device: AxisNetworkDevice) -> None: + """Initialize the Axis event.""" + self.device = device + + self._attr_device_info = DeviceInfo( + identifiers={(AXIS_DOMAIN, device.unique_id)} + ) + + async def async_added_to_hass(self) -> None: + """Subscribe device events.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self.device.signal_reachable, + self.async_signal_reachable_callback, + ) + ) + + @callback + def async_signal_reachable_callback(self) -> None: + """Call when device connection state change.""" + self._attr_available = self.device.available + self.async_write_ha_state() + + +class AxisEventEntity(AxisEntity): + """Base common to all Axis entities from event stream.""" + + _attr_should_poll = False + + def __init__(self, event: Event, device: AxisNetworkDevice) -> None: + """Initialize the Axis event.""" + super().__init__(device) + + self._event_id = event.id + self._event_topic = event.topic_base + self._event_type = TOPIC_TO_EVENT_TYPE[event.topic_base] + + self._attr_name = f"{self._event_type} {event.id}" + self._attr_unique_id = f"{device.unique_id}-{event.topic}-{event.id}" + + self._attr_device_class = event.group.value + + @callback + @abstractmethod + def async_event_callback(self, event: Event) -> None: + """Update the entities state.""" + + async def async_added_to_hass(self) -> None: + """Subscribe sensors events.""" + await super().async_added_to_hass() + self.async_on_remove( + self.device.api.event.subscribe( + self.async_event_callback, + id_filter=self._event_id, + topic_filter=self._event_topic, + ) + ) diff --git a/homeassistant/components/axis/light.py b/homeassistant/components/axis/light.py index c75d18b1908..10dc8258d7e 100644 --- a/homeassistant/components/axis/light.py +++ b/homeassistant/components/axis/light.py @@ -1,17 +1,16 @@ """Support for Axis lights.""" from typing import Any -from axis.event_stream import CLASS_LIGHT, AxisBinaryEvent, AxisEvent +from axis.models.event import Event, EventOperation, EventTopic from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, 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 .axis_base import AxisEventBase from .const import DOMAIN as AXIS_DOMAIN from .device import AxisNetworkDevice +from .entity import AxisEventEntity async def async_setup_entry( @@ -20,7 +19,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Axis light.""" - device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.entry_id] if ( device.api.vapix.light_control is None @@ -29,35 +28,34 @@ async def async_setup_entry( return @callback - def async_add_sensor(event_id): - """Add light from Axis device.""" - event: AxisEvent = device.api.event[event_id] + def async_create_entity(event: Event) -> None: + """Create Axis light entity.""" + async_add_entities([AxisLight(event, device)]) - if event.CLASS == CLASS_LIGHT and event.TYPE == "Light": - async_add_entities([AxisLight(event, device)]) - - config_entry.async_on_unload( - async_dispatcher_connect(hass, device.signal_new_event, async_add_sensor) + device.api.event.subscribe( + async_create_entity, + topic_filter=EventTopic.LIGHT_STATUS, + operation_filter=EventOperation.INITIALIZED, ) -class AxisLight(AxisEventBase, LightEntity): +class AxisLight(AxisEventEntity, LightEntity): """Representation of a light Axis event.""" _attr_should_poll = True - event: AxisBinaryEvent - def __init__(self, event: AxisEvent, device: AxisNetworkDevice) -> None: + def __init__(self, event: Event, device: AxisNetworkDevice) -> None: """Initialize the Axis light.""" super().__init__(event, device) - self.light_id = f"led{self.event.id}" + self._light_id = f"led{event.id}" self.current_intensity = 0 self.max_intensity = 0 - light_type = device.api.vapix.light_control[self.light_id].light_type - self._attr_name = f"{light_type} {event.TYPE} {event.id}" + light_type = device.api.vapix.light_control[self._light_id].light_type + self._attr_name = f"{light_type} {self._event_type} {event.id}" + self._attr_is_on = event.is_tripped self._attr_supported_color_modes = {ColorMode.BRIGHTNESS} self._attr_color_mode = ColorMode.BRIGHTNESS @@ -68,20 +66,21 @@ class AxisLight(AxisEventBase, LightEntity): current_intensity = ( await self.device.api.vapix.light_control.get_current_intensity( - self.light_id + self._light_id ) ) self.current_intensity = current_intensity["data"]["intensity"] max_intensity = await self.device.api.vapix.light_control.get_valid_intensity( - self.light_id + self._light_id ) self.max_intensity = max_intensity["data"]["ranges"][0]["high"] - @property - def is_on(self) -> bool: - """Return true if light is on.""" - return self.event.is_tripped + @callback + def async_event_callback(self, event: Event) -> None: + """Update light state.""" + self._attr_is_on = event.is_tripped + self.async_write_ha_state() @property def brightness(self) -> int: @@ -91,24 +90,24 @@ class AxisLight(AxisEventBase, LightEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on light.""" if not self.is_on: - await self.device.api.vapix.light_control.activate_light(self.light_id) + await self.device.api.vapix.light_control.activate_light(self._light_id) if ATTR_BRIGHTNESS in kwargs: intensity = int((kwargs[ATTR_BRIGHTNESS] / 255) * self.max_intensity) await self.device.api.vapix.light_control.set_manual_intensity( - self.light_id, intensity + self._light_id, intensity ) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off light.""" if self.is_on: - await self.device.api.vapix.light_control.deactivate_light(self.light_id) + await self.device.api.vapix.light_control.deactivate_light(self._light_id) async def async_update(self) -> None: """Update brightness.""" current_intensity = ( await self.device.api.vapix.light_control.get_current_intensity( - self.light_id + self._light_id ) ) self.current_intensity = current_intensity["data"]["intensity"] diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index aad057cd4b3..7a36079d52a 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -3,7 +3,7 @@ "name": "Axis", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/axis", - "requirements": ["axis==44"], + "requirements": ["axis==46"], "dhcp": [ { "registered_devices": true diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index 6576e9d519e..adcd1ba5525 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -1,17 +1,16 @@ """Support for Axis switches.""" from typing import Any -from axis.event_stream import CLASS_OUTPUT, AxisBinaryEvent, AxisEvent +from axis.models.event import Event, EventOperation, EventTopic from homeassistant.components.switch import SwitchEntity 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 .axis_base import AxisEventBase from .const import DOMAIN as AXIS_DOMAIN from .device import AxisNetworkDevice +from .entity import AxisEventEntity async def async_setup_entry( @@ -20,42 +19,41 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Axis switch.""" - device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device: AxisNetworkDevice = hass.data[AXIS_DOMAIN][config_entry.entry_id] @callback - def async_add_switch(event_id): - """Add switch from Axis device.""" - event: AxisEvent = device.api.event[event_id] + def async_create_entity(event: Event) -> None: + """Create Axis switch entity.""" + async_add_entities([AxisSwitch(event, device)]) - if event.CLASS == CLASS_OUTPUT: - async_add_entities([AxisSwitch(event, device)]) - - config_entry.async_on_unload( - async_dispatcher_connect(hass, device.signal_new_event, async_add_switch) + device.api.event.subscribe( + async_create_entity, + topic_filter=EventTopic.RELAY, + operation_filter=EventOperation.INITIALIZED, ) -class AxisSwitch(AxisEventBase, SwitchEntity): +class AxisSwitch(AxisEventEntity, SwitchEntity): """Representation of a Axis switch.""" - event: AxisBinaryEvent - - def __init__(self, event: AxisEvent, device: AxisNetworkDevice) -> None: + def __init__(self, event: Event, device: AxisNetworkDevice) -> None: """Initialize the Axis switch.""" super().__init__(event, device) if event.id and device.api.vapix.ports[event.id].name: self._attr_name = device.api.vapix.ports[event.id].name + self._attr_is_on = event.is_tripped - @property - def is_on(self) -> bool: - """Return true if event is active.""" - return self.event.is_tripped + @callback + def async_event_callback(self, event: Event) -> None: + """Update light state.""" + self._attr_is_on = event.is_tripped + self.async_write_ha_state() async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" - await self.device.api.vapix.ports[self.event.id].close() + await self.device.api.vapix.ports[self._event_id].close() async def async_turn_off(self, **kwargs: Any) -> None: """Turn off switch.""" - await self.device.api.vapix.ports[self.event.id].open() + await self.device.api.vapix.ports[self._event_id].open() diff --git a/homeassistant/components/axis/translations/lt.json b/homeassistant/components/axis/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/axis/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/translations/lv.json b/homeassistant/components/axis/translations/lv.json new file mode 100644 index 00000000000..862ef1ca431 --- /dev/null +++ b/homeassistant/components/axis/translations/lv.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_event_hub/translations/tr.json b/homeassistant/components/azure_event_hub/translations/tr.json index 89e6366f901..251728507a9 100644 --- a/homeassistant/components/azure_event_hub/translations/tr.json +++ b/homeassistant/components/azure_event_hub/translations/tr.json @@ -32,7 +32,7 @@ "event_hub_instance_name": "Event Hub \u00d6rnek Ad\u0131", "use_connection_string": "Ba\u011flant\u0131 Dizesini Kullan" }, - "title": "Azure Event Hub entegrasyonu kurun" + "title": "Azure Event Hub entegrasyonunuzu kurun" } } }, diff --git a/homeassistant/components/azure_event_hub/translations/uk.json b/homeassistant/components/azure_event_hub/translations/uk.json new file mode 100644 index 00000000000..fcffb285276 --- /dev/null +++ b/homeassistant/components/azure_event_hub/translations/uk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u043e\u0441\u043b\u0443\u0433\u0430 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0430", + "cannot_connect": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0445 \u0434\u0430\u043d\u0438\u0445 \u0456\u0437 \u0444\u0430\u0439\u043b\u0443 configuration.yaml, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u0456\u0437 yaml \u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043f\u043e\u0442\u0456\u043a \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457.", + "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": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0445 \u0434\u0430\u043d\u0438\u0445 \u0456\u0437 \u0444\u0430\u0439\u043b\u0443 configuration.yaml \u0447\u0435\u0440\u0435\u0437 \u043d\u0435\u0432\u0456\u0434\u043e\u043c\u0443 \u043f\u043e\u043c\u0438\u043b\u043a\u0443. \u0412\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u0456\u0437 yaml \u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043f\u043e\u0442\u0456\u043a \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "unknown": "\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/baf/binary_sensor.py b/homeassistant/components/baf/binary_sensor.py index 8a00a7e8bd5..fcfd0f3241d 100644 --- a/homeassistant/components/baf/binary_sensor.py +++ b/homeassistant/components/baf/binary_sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Optional, cast +from typing import cast from aiobafi6 import Device @@ -41,7 +41,7 @@ OCCUPANCY_SENSORS = ( key="occupancy", name="Occupancy", device_class=BinarySensorDeviceClass.OCCUPANCY, - value_fn=lambda device: cast(Optional[bool], device.fan_occupancy_detected), + value_fn=lambda device: cast(bool | None, device.fan_occupancy_detected), ), ) diff --git a/homeassistant/components/baf/number.py b/homeassistant/components/baf/number.py index 0e014de6750..1cab994376d 100644 --- a/homeassistant/components/baf/number.py +++ b/homeassistant/components/baf/number.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Optional, cast +from typing import cast from aiobafi6 import Device @@ -43,7 +43,7 @@ AUTO_COMFORT_NUMBER_DESCRIPTIONS = ( native_min_value=0, native_max_value=SPEED_RANGE[1] - 1, entity_category=EntityCategory.CONFIG, - value_fn=lambda device: cast(Optional[int], device.comfort_min_speed), + value_fn=lambda device: cast(int | None, device.comfort_min_speed), mode=NumberMode.BOX, ), BAFNumberDescription( @@ -52,7 +52,7 @@ AUTO_COMFORT_NUMBER_DESCRIPTIONS = ( native_min_value=1, native_max_value=SPEED_RANGE[1], entity_category=EntityCategory.CONFIG, - value_fn=lambda device: cast(Optional[int], device.comfort_max_speed), + value_fn=lambda device: cast(int | None, device.comfort_max_speed), mode=NumberMode.BOX, ), BAFNumberDescription( @@ -61,7 +61,7 @@ AUTO_COMFORT_NUMBER_DESCRIPTIONS = ( native_min_value=SPEED_RANGE[0], native_max_value=SPEED_RANGE[1], entity_category=EntityCategory.CONFIG, - value_fn=lambda device: cast(Optional[int], device.comfort_heat_assist_speed), + value_fn=lambda device: cast(int | None, device.comfort_heat_assist_speed), mode=NumberMode.BOX, ), ) @@ -74,7 +74,7 @@ FAN_NUMBER_DESCRIPTIONS = ( native_max_value=HALF_DAY_SECS, entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTime.SECONDS, - value_fn=lambda device: cast(Optional[int], device.return_to_auto_timeout), + value_fn=lambda device: cast(int | None, device.return_to_auto_timeout), mode=NumberMode.SLIDER, ), BAFNumberDescription( @@ -84,7 +84,7 @@ FAN_NUMBER_DESCRIPTIONS = ( native_max_value=ONE_DAY_SECS, entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTime.SECONDS, - value_fn=lambda device: cast(Optional[int], device.motion_sense_timeout), + value_fn=lambda device: cast(int | None, device.motion_sense_timeout), mode=NumberMode.SLIDER, ), ) @@ -97,9 +97,7 @@ LIGHT_NUMBER_DESCRIPTIONS = ( native_max_value=HALF_DAY_SECS, entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTime.SECONDS, - value_fn=lambda device: cast( - Optional[int], device.light_return_to_auto_timeout - ), + value_fn=lambda device: cast(int | None, device.light_return_to_auto_timeout), mode=NumberMode.SLIDER, ), BAFNumberDescription( @@ -109,7 +107,7 @@ LIGHT_NUMBER_DESCRIPTIONS = ( native_max_value=ONE_DAY_SECS, entity_category=EntityCategory.CONFIG, native_unit_of_measurement=UnitOfTime.SECONDS, - value_fn=lambda device: cast(Optional[int], device.light_auto_motion_timeout), + value_fn=lambda device: cast(int | None, device.light_auto_motion_timeout), mode=NumberMode.SLIDER, ), ) diff --git a/homeassistant/components/baf/sensor.py b/homeassistant/components/baf/sensor.py index 81b48ae59d7..950a746cddd 100644 --- a/homeassistant/components/baf/sensor.py +++ b/homeassistant/components/baf/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Optional, cast +from typing import cast from aiobafi6 import Device @@ -46,7 +46,7 @@ AUTO_COMFORT_SENSORS = ( native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda device: cast(Optional[float], device.temperature), + value_fn=lambda device: cast(float | None, device.temperature), ), ) @@ -57,7 +57,7 @@ DEFINED_ONLY_SENSORS = ( native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, - value_fn=lambda device: cast(Optional[float], device.humidity), + value_fn=lambda device: cast(float | None, device.humidity), ), ) @@ -68,7 +68,7 @@ FAN_SENSORS = ( native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda device: cast(Optional[int], device.current_rpm), + value_fn=lambda device: cast(int | None, device.current_rpm), ), BAFSensorDescription( key="target_rpm", @@ -76,21 +76,21 @@ FAN_SENSORS = ( native_unit_of_measurement=REVOLUTIONS_PER_MINUTE, entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda device: cast(Optional[int], device.target_rpm), + value_fn=lambda device: cast(int | None, device.target_rpm), ), BAFSensorDescription( key="wifi_ssid", name="WiFi SSID", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda device: cast(Optional[int], device.wifi_ssid), + value_fn=lambda device: cast(int | None, device.wifi_ssid), ), BAFSensorDescription( key="ip_address", name="IP Address", entity_registry_enabled_default=False, entity_category=EntityCategory.DIAGNOSTIC, - value_fn=lambda device: cast(Optional[str], device.ip_address), + value_fn=lambda device: cast(str | None, device.ip_address), ), ) diff --git a/homeassistant/components/baf/switch.py b/homeassistant/components/baf/switch.py index 44671e68458..50ee178f9b1 100644 --- a/homeassistant/components/baf/switch.py +++ b/homeassistant/components/baf/switch.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Optional, cast +from typing import Any, cast from aiobafi6 import Device @@ -38,13 +38,13 @@ BASE_SWITCHES = [ key="legacy_ir_remote_enable", name="Legacy IR Remote", entity_category=EntityCategory.CONFIG, - value_fn=lambda device: cast(Optional[bool], device.legacy_ir_remote_enable), + value_fn=lambda device: cast(bool | None, device.legacy_ir_remote_enable), ), BAFSwitchDescription( key="led_indicators_enable", name="Led Indicators", entity_category=EntityCategory.CONFIG, - value_fn=lambda device: cast(Optional[bool], device.led_indicators_enable), + value_fn=lambda device: cast(bool | None, device.led_indicators_enable), ), ] @@ -53,7 +53,7 @@ AUTO_COMFORT_SWITCHES = [ key="comfort_heat_assist_enable", name="Auto Comfort Heat Assist", entity_category=EntityCategory.CONFIG, - value_fn=lambda device: cast(Optional[bool], device.comfort_heat_assist_enable), + value_fn=lambda device: cast(bool | None, device.comfort_heat_assist_enable), ), ] @@ -62,31 +62,31 @@ FAN_SWITCHES = [ key="fan_beep_enable", name="Beep", entity_category=EntityCategory.CONFIG, - value_fn=lambda device: cast(Optional[bool], device.fan_beep_enable), + value_fn=lambda device: cast(bool | None, device.fan_beep_enable), ), BAFSwitchDescription( key="eco_enable", name="Eco Mode", entity_category=EntityCategory.CONFIG, - value_fn=lambda device: cast(Optional[bool], device.eco_enable), + value_fn=lambda device: cast(bool | None, device.eco_enable), ), BAFSwitchDescription( key="motion_sense_enable", name="Motion Sense", entity_category=EntityCategory.CONFIG, - value_fn=lambda device: cast(Optional[bool], device.motion_sense_enable), + value_fn=lambda device: cast(bool | None, device.motion_sense_enable), ), BAFSwitchDescription( key="return_to_auto_enable", name="Return to Auto", entity_category=EntityCategory.CONFIG, - value_fn=lambda device: cast(Optional[bool], device.return_to_auto_enable), + value_fn=lambda device: cast(bool | None, device.return_to_auto_enable), ), BAFSwitchDescription( key="whoosh_enable", name="Whoosh", # Not a configuration switch - value_fn=lambda device: cast(Optional[bool], device.whoosh_enable), + value_fn=lambda device: cast(bool | None, device.whoosh_enable), ), ] @@ -96,15 +96,13 @@ LIGHT_SWITCHES = [ key="light_dim_to_warm_enable", name="Dim to Warm", entity_category=EntityCategory.CONFIG, - value_fn=lambda device: cast(Optional[bool], device.light_dim_to_warm_enable), + value_fn=lambda device: cast(bool | None, device.light_dim_to_warm_enable), ), BAFSwitchDescription( key="light_return_to_auto_enable", name="Light Return to Auto", entity_category=EntityCategory.CONFIG, - value_fn=lambda device: cast( - Optional[bool], device.light_return_to_auto_enable - ), + value_fn=lambda device: cast(bool | None, device.light_return_to_auto_enable), ), ] diff --git a/homeassistant/components/baf/translations/lv.json b/homeassistant/components/baf/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/baf/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/baf/translations/tr.json b/homeassistant/components/baf/translations/tr.json index ffa458b7366..9182b9c0bee 100644 --- a/homeassistant/components/baf/translations/tr.json +++ b/homeassistant/components/baf/translations/tr.json @@ -11,7 +11,7 @@ "flow_title": "{name} - {model} ({ip_address})", "step": { "discovery_confirm": { - "description": "{name} - {model} ( {ip_address} ) kurulumunu yapmak istiyor musunuz?" + "description": "{name} - {model} ( {ip_address} ) kurmak istiyor musunuz?" }, "user": { "data": { diff --git a/homeassistant/components/balboa/translations/lv.json b/homeassistant/components/balboa/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/balboa/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/it.json b/homeassistant/components/binary_sensor/translations/it.json index 933e72a285a..03e68af468f 100644 --- a/homeassistant/components/binary_sensor/translations/it.json +++ b/homeassistant/components/binary_sensor/translations/it.json @@ -8,7 +8,7 @@ "is_gas": "{entity_name} sta rilevando il gas", "is_hot": "{entity_name} \u00e8 caldo", "is_light": "{entity_name} sta rilevando la luce", - "is_locked": "{entity_name} \u00e8 bloccato", + "is_locked": "{entity_name} \u00e8 chiusa", "is_moist": "{entity_name} \u00e8 umido", "is_motion": "{entity_name} sta rilevando il movimento", "is_moving": "{entity_name} si sta muovendo", @@ -25,7 +25,7 @@ "is_not_cold": "{entity_name} non \u00e8 freddo", "is_not_connected": "{entity_name} \u00e8 disconnesso", "is_not_hot": "{entity_name} non \u00e8 caldo", - "is_not_locked": "{entity_name} \u00e8 sbloccato", + "is_not_locked": "{entity_name} \u00e8 aperta", "is_not_moist": "{entity_name} \u00e8 asciutto", "is_not_moving": "{entity_name} non si sta muovendo", "is_not_occupied": "{entity_name} non \u00e8 occupato", @@ -165,8 +165,8 @@ "on": "Luce rilevata" }, "lock": { - "off": "Bloccato", - "on": "Sbloccato" + "off": "Chiusa", + "on": "Aperta" }, "moisture": { "off": "Asciutto", diff --git a/homeassistant/components/binary_sensor/translations/lt.json b/homeassistant/components/binary_sensor/translations/lt.json index 1214ac53470..0757b8331ce 100644 --- a/homeassistant/components/binary_sensor/translations/lt.json +++ b/homeassistant/components/binary_sensor/translations/lt.json @@ -1,4 +1,14 @@ { + "device_automation": { + "condition_type": { + "is_motion": "{entity_name} aptiko judes\u012f", + "is_no_motion": "{entity_name} judesio n\u0117ra" + }, + "trigger_type": { + "motion": "{entity_name} prad\u0117jo aptikti judes\u012f", + "no_motion": "{entity_name} nustojo aptikti judes\u012f" + } + }, "state": { "_": { "off": "I\u0161jungta", diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index f8a38bbe470..29a7957fde7 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -20,12 +20,8 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by blockchain.com" - DEFAULT_CURRENCY = "USD" -ICON = "mdi:currency-btc" - SCAN_INTERVAL = timedelta(minutes=5) SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( @@ -167,10 +163,12 @@ def setup_platform( class BitcoinSensor(SensorEntity): """Representation of a Bitcoin sensor.""" - _attr_attribution = ATTRIBUTION - _attr_icon = ICON + _attr_attribution = "Data provided by blockchain.com" + _attr_icon = "mdi:currency-btc" - def __init__(self, data, currency, description: SensorEntityDescription): + def __init__( + self, data: BitcoinData, currency: str, description: SensorEntityDescription + ) -> None: """Initialize the sensor.""" self.entity_description = description self.data = data @@ -231,12 +229,10 @@ class BitcoinSensor(SensorEntity): class BitcoinData: """Get the latest data and update the states.""" - def __init__(self): - """Initialize the data object.""" - self.stats = None - self.ticker = None + stats: statistics.Stats + ticker: dict[str, exchangerates.Currency] - def update(self): + def update(self) -> None: """Get the latest data from blockchain.com.""" self.stats = statistics.get() diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json index 39a0d57558f..76dad200e95 100644 --- a/homeassistant/components/blebox/manifest.json +++ b/homeassistant/components/blebox/manifest.json @@ -3,7 +3,7 @@ "name": "BleBox devices", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/blebox", - "requirements": ["blebox_uniapi==2.1.3"], + "requirements": ["blebox_uniapi==2.1.4"], "codeowners": ["@bbx-a", "@riokuu"], "iot_class": "local_polling", "loggers": ["blebox_uniapi"], diff --git a/homeassistant/components/blebox/translations/lv.json b/homeassistant/components/blebox/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/blebox/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/el.json b/homeassistant/components/blink/translations/el.json index 690d2bf0539..5e697198294 100644 --- a/homeassistant/components/blink/translations/el.json +++ b/homeassistant/components/blink/translations/el.json @@ -14,7 +14,7 @@ "data": { "2fa": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b4\u03cd\u03bf \u03c0\u03b1\u03c1\u03b1\u03b3\u03cc\u03bd\u03c4\u03c9\u03bd" }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf PIN \u03c0\u03bf\u03c5 \u03c3\u03c4\u03ac\u03bb\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c4\u03bf email \u03c3\u03b1\u03c2", + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf PIN \u03c0\u03bf\u03c5 \u03b1\u03c0\u03bf\u03c3\u03c4\u03ad\u03bb\u03bb\u03b5\u03c4\u03b1\u03b9 \u03bc\u03ad\u03c3\u03c9 email \u03ae SMS", "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": { diff --git a/homeassistant/components/blink/translations/lt.json b/homeassistant/components/blink/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/blink/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/lv.json b/homeassistant/components/blink/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/blink/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/pl.json b/homeassistant/components/blink/translations/pl.json index 72b6c32e5be..1fc8144fec6 100644 --- a/homeassistant/components/blink/translations/pl.json +++ b/homeassistant/components/blink/translations/pl.json @@ -14,7 +14,7 @@ "data": { "2fa": "Kod uwierzytelniania dwusk\u0142adnikowego" }, - "description": "Wprowad\u017a kod PIN wys\u0142any na Tw\u00f3j adres e-mail.", + "description": "Wprowad\u017a kod PIN wys\u0142any e-mailem lub smsem.", "title": "Uwierzytelnianie dwusk\u0142adnikowe" }, "user": { diff --git a/homeassistant/components/blink/translations/tr.json b/homeassistant/components/blink/translations/tr.json index 1a7444cb644..a5f2694f689 100644 --- a/homeassistant/components/blink/translations/tr.json +++ b/homeassistant/components/blink/translations/tr.json @@ -14,7 +14,7 @@ "data": { "2fa": "\u0130ki ad\u0131ml\u0131 kimlik do\u011frulama kodu" }, - "description": "E-postan\u0131za g\u00f6nderilen PIN kodunu girin", + "description": "E-posta veya SMS yoluyla g\u00f6nderilen PIN'i girin", "title": "\u0130ki fakt\u00f6rl\u00fc kimlik do\u011frulama" }, "user": { diff --git a/homeassistant/components/bluemaestro/manifest.json b/homeassistant/components/bluemaestro/manifest.json index 0ff9cdd0794..277e02ab488 100644 --- a/homeassistant/components/bluemaestro/manifest.json +++ b/homeassistant/components/bluemaestro/manifest.json @@ -9,8 +9,8 @@ "connectable": false } ], - "requirements": ["bluemaestro-ble==0.2.0"], - "dependencies": ["bluetooth"], + "requirements": ["bluemaestro-ble==0.2.1"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" } diff --git a/homeassistant/components/bluemaestro/sensor.py b/homeassistant/components/bluemaestro/sensor.py index d3776d418e5..b4b10ed2ee6 100644 --- a/homeassistant/components/bluemaestro/sensor.py +++ b/homeassistant/components/bluemaestro/sensor.py @@ -1,8 +1,6 @@ """Support for BlueMaestro sensors.""" from __future__ import annotations -from typing import Optional, Union - from bluemaestro_ble import ( SensorDeviceClass as BlueMaestroSensorDeviceClass, SensorUpdate, @@ -137,9 +135,7 @@ async def async_setup_entry( class BlueMaestroBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a BlueMaestro sensor.""" diff --git a/homeassistant/components/bluemaestro/translations/lv.json b/homeassistant/components/bluemaestro/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluemaestro/translations/tr.json b/homeassistant/components/bluemaestro/translations/tr.json index f0ddbc274c9..36347c44f7f 100644 --- a/homeassistant/components/bluemaestro/translations/tr.json +++ b/homeassistant/components/bluemaestro/translations/tr.json @@ -9,13 +9,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/bluemaestro/translations/uk.json b/homeassistant/components/bluemaestro/translations/uk.json new file mode 100644 index 00000000000..e58b49d4c9e --- /dev/null +++ b/homeassistant/components/bluemaestro/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index d0a69bfe379..add7dad1a1f 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -58,9 +58,10 @@ from .api import ( async_register_scanner, async_scanner_by_source, async_scanner_count, + async_scanner_devices_by_address, async_track_unavailable, ) -from .base_scanner import BaseHaRemoteScanner, BaseHaScanner +from .base_scanner import BaseHaRemoteScanner, BaseHaScanner, BluetoothScannerDevice from .const import ( BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS, CONF_ADAPTER, @@ -99,6 +100,7 @@ __all__ = [ "async_track_unavailable", "async_scanner_by_source", "async_scanner_count", + "async_scanner_devices_by_address", "BaseHaScanner", "BaseHaRemoteScanner", "BluetoothCallbackMatcher", @@ -107,6 +109,7 @@ __all__ = [ "BluetoothServiceInfoBleak", "BluetoothScanningMode", "BluetoothCallback", + "BluetoothScannerDevice", "HaBluetoothConnector", "SOURCE_LOCAL", "FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS", diff --git a/homeassistant/components/bluetooth/active_update_coordinator.py b/homeassistant/components/bluetooth/active_update_coordinator.py index c4d40d5eaeb..09567aada05 100644 --- a/homeassistant/components/bluetooth/active_update_coordinator.py +++ b/homeassistant/components/bluetooth/active_update_coordinator.py @@ -1,4 +1,7 @@ -"""A Bluetooth passive coordinator that receives data from advertisements but can also poll.""" +"""A Bluetooth passive coordinator. + +Receives data from advertisements but can also poll. +""" from __future__ import annotations from collections.abc import Callable, Coroutine @@ -21,7 +24,7 @@ _T = TypeVar("_T") class ActiveBluetoothDataUpdateCoordinator( - Generic[_T], PassiveBluetoothDataUpdateCoordinator + PassiveBluetoothDataUpdateCoordinator, Generic[_T] ): """ A coordinator that receives passive data from advertisements but can also poll. @@ -33,16 +36,19 @@ class ActiveBluetoothDataUpdateCoordinator( out if a poll is needed. This should return True if it is and False if it is not needed. - def needs_poll_method(svc_info: BluetoothServiceInfoBleak, last_poll: float | None) -> bool: + def needs_poll_method( + svc_info: BluetoothServiceInfoBleak, + last_poll: float | None + ) -> bool: return True - If there has been no poll since HA started, `last_poll` will be None. Otherwise it is - the number of seconds since one was last attempted. + If there has been no poll since HA started, `last_poll` will be None. + Otherwise it is the number of seconds since one was last attempted. If a poll is needed, the coordinator will call poll_method. This is a coroutine. - It should return the same type of data as your update_method. The expectation is that - data from advertisements and from polling are being parsed and fed into a shared - object that represents the current state of the device. + It should return the same type of data as your update_method. The expectation is + that data from advertisements and from polling are being parsed and fed into + a shared object that represents the current state of the device. async def poll_method(svc_info: BluetoothServiceInfoBleak) -> YourDataType: return YourDataType(....) diff --git a/homeassistant/components/bluetooth/active_update_processor.py b/homeassistant/components/bluetooth/active_update_processor.py index e175fc665f4..b91ac2cbf4d 100644 --- a/homeassistant/components/bluetooth/active_update_processor.py +++ b/homeassistant/components/bluetooth/active_update_processor.py @@ -1,4 +1,7 @@ -"""A Bluetooth passive processor coordinator that collects data from advertisements but can also poll.""" +"""A Bluetooth passive processor coordinator. + +Collects data from advertisements but can also poll. +""" from __future__ import annotations from collections.abc import Callable, Coroutine @@ -23,23 +26,27 @@ _T = TypeVar("_T") class ActiveBluetoothProcessorCoordinator( Generic[_T], PassiveBluetoothProcessorCoordinator[_T] ): - """ - A processor coordinator that parses passive data from advertisements but can also poll. + """A processor coordinator that parses passive data. + + Parses passive data from advertisements but can also poll. Every time an advertisement is received, needs_poll_method is called to work out if a poll is needed. This should return True if it is and False if it is not needed. - def needs_poll_method(svc_info: BluetoothServiceInfoBleak, last_poll: float | None) -> bool: + def needs_poll_method( + svc_info: BluetoothServiceInfoBleak, + last_poll: float | None + ) -> bool: return True - If there has been no poll since HA started, `last_poll` will be None. Otherwise it is - the number of seconds since one was last attempted. + If there has been no poll since HA started, `last_poll` will be None. + Otherwise it is the number of seconds since one was last attempted. If a poll is needed, the coordinator will call poll_method. This is a coroutine. - It should return the same type of data as your update_method. The expectation is that - data from advertisements and from polling are being parsed and fed into a shared - object that represents the current state of the device. + It should return the same type of data as your update_method. The expectation is + that data from advertisements and from polling are being parsed and fed into a + shared object that represents the current state of the device. async def poll_method(svc_info: BluetoothServiceInfoBleak) -> YourDataType: return YourDataType(....) diff --git a/homeassistant/components/bluetooth/advertisement_tracker.py b/homeassistant/components/bluetooth/advertisement_tracker.py index f4577496e04..3936435f84e 100644 --- a/homeassistant/components/bluetooth/advertisement_tracker.py +++ b/homeassistant/components/bluetooth/advertisement_tracker.py @@ -9,6 +9,11 @@ from .models import BluetoothServiceInfoBleak ADVERTISING_TIMES_NEEDED = 16 +# Each scanner may buffer incoming packets so +# we need to give a bit of leeway before we +# mark a device unavailable +TRACKER_BUFFERING_WOBBLE_SECONDS = 5 + class AdvertisementTracker: """Tracker to determine the interval that a device is advertising.""" diff --git a/homeassistant/components/bluetooth/api.py b/homeassistant/components/bluetooth/api.py index cd6b4ac959b..6c232e2a42c 100644 --- a/homeassistant/components/bluetooth/api.py +++ b/homeassistant/components/bluetooth/api.py @@ -13,7 +13,7 @@ from home_assistant_bluetooth import BluetoothServiceInfoBleak from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback -from .base_scanner import BaseHaScanner +from .base_scanner import BaseHaScanner, BluetoothScannerDevice from .const import DATA_MANAGER from .manager import BluetoothManager from .match import BluetoothCallbackMatcher @@ -93,6 +93,14 @@ def async_ble_device_from_address( return _get_manager(hass).async_ble_device_from_address(address, connectable) +@hass_callback +def async_scanner_devices_by_address( + hass: HomeAssistant, address: str, connectable: bool = True +) -> list[BluetoothScannerDevice]: + """Return all discovered BluetoothScannerDevice for an address.""" + return _get_manager(hass).async_scanner_devices_by_address(address, connectable) + + @hass_callback def async_address_present( hass: HomeAssistant, address: str, connectable: bool = True diff --git a/homeassistant/components/bluetooth/base_scanner.py b/homeassistant/components/bluetooth/base_scanner.py index d522d69cdbe..00cc9fff0fe 100644 --- a/homeassistant/components/bluetooth/base_scanner.py +++ b/homeassistant/components/bluetooth/base_scanner.py @@ -4,6 +4,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import Callable, Generator from contextlib import contextmanager +from dataclasses import dataclass import datetime from datetime import timedelta import logging @@ -29,7 +30,6 @@ from homeassistant.util.dt import monotonic_time_coarse from . import models from .const import ( CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, - FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, SCANNER_WATCHDOG_INTERVAL, SCANNER_WATCHDOG_TIMEOUT, ) @@ -39,6 +39,15 @@ MONOTONIC_TIME: Final = monotonic_time_coarse _LOGGER = logging.getLogger(__name__) +@dataclass +class BluetoothScannerDevice: + """Data for a bluetooth device from a given scanner.""" + + scanner: BaseHaScanner + ble_device: BLEDevice + advertisement: AdvertisementData + + class BaseHaScanner(ABC): """Base class for Ha Scanners.""" @@ -107,7 +116,8 @@ class BaseHaScanner(ABC): def _async_scanner_watchdog(self, now: datetime.datetime) -> None: """Check if the scanner is running. - Override this method if you need to do something else when the watchdog is triggered. + Override this method if you need to do something else when the watchdog + is triggered. """ if self._async_watchdog_triggered(): _LOGGER.info( @@ -144,6 +154,7 @@ class BaseHaScanner(ABC): async def async_diagnostics(self) -> dict[str, Any]: """Return diagnostic information about the scanner.""" + device_adv_datas = self.discovered_devices_and_advertisement_data.values() return { "name": self.name, "start_time": self._start_time, @@ -160,7 +171,7 @@ class BaseHaScanner(ABC): "advertisement_data": device_adv[1], "details": device_adv[0].details, } - for device_adv in self.discovered_devices_and_advertisement_data.values() + for device_adv in device_adv_datas ], } @@ -183,7 +194,7 @@ class BaseHaRemoteScanner(BaseHaScanner): scanner_id: str, name: str, new_info_callback: Callable[[BluetoothServiceInfoBleak], None], - connector: HaBluetoothConnector, + connector: HaBluetoothConnector | None, connectable: bool, ) -> None: """Initialize the scanner.""" @@ -195,13 +206,11 @@ class BaseHaRemoteScanner(BaseHaScanner): self._discovered_device_timestamps: dict[str, float] = {} self.connectable = connectable self._details: dict[str, str | HaBluetoothConnector] = {"source": scanner_id} - self._expire_seconds = FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + # Scanners only care about connectable devices. The manager + # will handle taking care of availability for non-connectable devices + self._expire_seconds = CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS assert models.MANAGER is not None self._storage = models.MANAGER.storage - if connectable: - self._expire_seconds = ( - CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS - ) @hass_callback def async_setup(self) -> CALLBACK_TYPE: @@ -258,9 +267,10 @@ class BaseHaRemoteScanner(BaseHaScanner): @property def discovered_devices(self) -> list[BLEDevice]: """Return a list of discovered devices.""" + device_adv_datas = self._discovered_device_advertisement_datas.values() return [ device_advertisement_data[0] - for device_advertisement_data in self._discovered_device_advertisement_datas.values() + for device_advertisement_data in device_adv_datas ] @property @@ -297,17 +307,26 @@ class BaseHaRemoteScanner(BaseHaScanner): and len(prev_device.name) > len(local_name) ): local_name = prev_device.name - if prev_advertisement.service_uuids: + if service_uuids and service_uuids != prev_advertisement.service_uuids: service_uuids = list( set(service_uuids + prev_advertisement.service_uuids) ) - if prev_advertisement.service_data: + elif not service_uuids: + service_uuids = prev_advertisement.service_uuids + if service_data and service_data != prev_advertisement.service_data: service_data = {**prev_advertisement.service_data, **service_data} - if prev_advertisement.manufacturer_data: + elif not service_data: + service_data = prev_advertisement.service_data + if ( + manufacturer_data + and manufacturer_data != prev_advertisement.manufacturer_data + ): manufacturer_data = { **prev_advertisement.manufacturer_data, **manufacturer_data, } + elif not manufacturer_data: + manufacturer_data = prev_advertisement.manufacturer_data advertisement_data = AdvertisementData( local_name=None if local_name == "" else local_name, diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index 748b685d866..1523f41bf1f 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -28,8 +28,11 @@ from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval from homeassistant.util.dt import monotonic_time_coarse -from .advertisement_tracker import AdvertisementTracker -from .base_scanner import BaseHaScanner +from .advertisement_tracker import ( + TRACKER_BUFFERING_WOBBLE_SECONDS, + AdvertisementTracker, +) +from .base_scanner import BaseHaScanner, BluetoothScannerDevice from .const import ( FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, UNAVAILABLE_TRACK_SECONDS, @@ -206,6 +209,20 @@ class BluetoothManager: self._bluetooth_adapters, self.storage ) self.async_setup_unavailable_tracking() + seen: set[str] = set() + for address, service_info in itertools.chain( + self._connectable_history.items(), self._all_history.items() + ): + if address in seen: + continue + seen.add(address) + for domain in self._integration_matcher.match_domains(service_info): + discovery_flow.async_create_flow( + self.hass, + domain, + {"source": config_entries.SOURCE_BLUETOOTH}, + service_info, + ) @hass_callback def async_stop(self, event: Event) -> None: @@ -217,23 +234,29 @@ class BluetoothManager: uninstall_multiple_bleak_catcher() @hass_callback - def async_get_scanner_discovered_devices_and_advertisement_data_by_address( + def async_scanner_devices_by_address( self, address: str, connectable: bool - ) -> list[tuple[BaseHaScanner, BLEDevice, AdvertisementData]]: - """Get scanner, devices, and advertisement_data by address.""" - types_ = (True,) if connectable else (True, False) - results: list[tuple[BaseHaScanner, BLEDevice, AdvertisementData]] = [] - for type_ in types_: - for scanner in self._get_scanners_by_type(type_): - if device_advertisement_data := scanner.discovered_devices_and_advertisement_data.get( + ) -> list[BluetoothScannerDevice]: + """Get BluetoothScannerDevice by address.""" + scanners = self._get_scanners_by_type(True) + if not connectable: + scanners.extend(self._get_scanners_by_type(False)) + return [ + BluetoothScannerDevice(scanner, *device_adv) + for scanner in scanners + if ( + device_adv := scanner.discovered_devices_and_advertisement_data.get( address - ): - results.append((scanner, *device_advertisement_data)) - return results + ) + ) + ] @hass_callback def _async_all_discovered_addresses(self, connectable: bool) -> Iterable[str]: - """Return all of discovered addresses from all the scanners including duplicates.""" + """Return all of discovered addresses. + + Include addresses from all the scanners including duplicates. + """ yield from itertools.chain.from_iterable( scanner.discovered_devices_and_advertisement_data for scanner in self._get_scanners_by_type(True) @@ -281,13 +304,18 @@ class BluetoothManager: # # For non-connectable devices we also check the device has exceeded # the advertising interval before we mark it as unavailable - # since it may have gone to sleep and since we do not need an active connection - # to it we can only determine its availability by the lack of advertisements - # + # since it may have gone to sleep and since we do not need an active + # connection to it we can only determine its availability + # by the lack of advertisements if advertising_interval := intervals.get(address): - time_since_seen = monotonic_now - all_history[address].time - if time_since_seen <= advertising_interval: - continue + advertising_interval += TRACKER_BUFFERING_WOBBLE_SECONDS + else: + advertising_interval = ( + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + ) + time_since_seen = monotonic_now - all_history[address].time + if time_since_seen <= advertising_interval: + continue # The second loop (connectable=False) is responsible for removing # the device from all the interval tracking since it is no longer @@ -335,7 +363,8 @@ class BluetoothManager: if (new.rssi or NO_RSSI_VALUE) - RSSI_SWITCH_THRESHOLD > ( old.rssi or NO_RSSI_VALUE ): - # If new advertisement is RSSI_SWITCH_THRESHOLD more, the new one is preferred + # If new advertisement is RSSI_SWITCH_THRESHOLD more, + # the new one is preferred. if debug: _LOGGER.debug( ( @@ -381,19 +410,21 @@ class BluetoothManager: source = service_info.source debug = _LOGGER.isEnabledFor(logging.DEBUG) - # This logic is complex due to the many combinations of scanners that are supported. + # This logic is complex due to the many combinations of scanners + # that are supported. # # We need to handle multiple connectable and non-connectable scanners # and we need to handle the case where a device is connectable on one scanner # but not on another. # - # The device may also be connectable only by a scanner that has worse signal strength - # than a non-connectable scanner. + # The device may also be connectable only by a scanner that has worse + # signal strength than a non-connectable scanner. # - # all_history - the history of all advertisements from all scanners with the best - # advertisement from each scanner - # connectable_history - the history of all connectable advertisements from all scanners - # with the best advertisement from each connectable scanner + # all_history - the history of all advertisements from all scanners with the + # best advertisement from each scanner + # connectable_history - the history of all connectable advertisements from all + # scanners with the best advertisement from each + # connectable scanner # if ( (old_service_info := all_history.get(address)) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index b20a3f17b50..895b987470f 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -6,12 +6,12 @@ "after_dependencies": ["hassio"], "quality_scale": "internal", "requirements": [ - "bleak==0.19.2", + "bleak==0.19.5", "bleak-retry-connector==2.13.0", "bluetooth-adapters==0.15.2", "bluetooth-auto-recovery==1.0.3", "bluetooth-data-tools==0.3.1", - "dbus-fast==1.82.0" + "dbus-fast==1.84.0" ], "codeowners": ["@bdraco"], "config_flow": true, diff --git a/homeassistant/components/bluetooth/match.py b/homeassistant/components/bluetooth/match.py index 1a59ee6fe4c..a7308bfd7ff 100644 --- a/homeassistant/components/bluetooth/match.py +++ b/homeassistant/components/bluetooth/match.py @@ -282,7 +282,10 @@ class BluetoothMatcherIndex(BluetoothMatcherIndexBase[BluetoothMatcher]): class BluetoothCallbackMatcherIndex( BluetoothMatcherIndexBase[BluetoothCallbackMatcherWithCallback] ): - """Bluetooth matcher for the bluetooth integration that supports matching on addresses.""" + """Bluetooth matcher for the bluetooth integration. + + Supports matching on addresses. + """ def __init__(self) -> None: """Initialize the matcher index.""" diff --git a/homeassistant/components/bluetooth/passive_update_coordinator.py b/homeassistant/components/bluetooth/passive_update_coordinator.py index 1eae49a6cab..6f1749aeef2 100644 --- a/homeassistant/components/bluetooth/passive_update_coordinator.py +++ b/homeassistant/components/bluetooth/passive_update_coordinator.py @@ -1,10 +1,13 @@ """Passive update coordinator for the Bluetooth integration.""" from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + BaseCoordinatorEntity, + BaseDataUpdateCoordinatorProtocol, +) from .update_coordinator import BasePassiveBluetoothCoordinator @@ -14,8 +17,15 @@ if TYPE_CHECKING: from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak +_PassiveBluetoothDataUpdateCoordinatorT = TypeVar( + "_PassiveBluetoothDataUpdateCoordinatorT", + bound="PassiveBluetoothDataUpdateCoordinator", +) -class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): + +class PassiveBluetoothDataUpdateCoordinator( + BasePassiveBluetoothCoordinator, BaseDataUpdateCoordinatorProtocol +): """Class to manage passive bluetooth advertisements. This coordinator is responsible for dispatching the bluetooth data @@ -78,11 +88,11 @@ class PassiveBluetoothDataUpdateCoordinator(BasePassiveBluetoothCoordinator): self.async_update_listeners() -class PassiveBluetoothCoordinatorEntity(CoordinatorEntity): +class PassiveBluetoothCoordinatorEntity( + BaseCoordinatorEntity[_PassiveBluetoothDataUpdateCoordinatorT] +): """A class for entities using DataUpdateCoordinator.""" - coordinator: PassiveBluetoothDataUpdateCoordinator - async def async_update(self) -> None: """All updates are passive.""" diff --git a/homeassistant/components/bluetooth/translations/el.json b/homeassistant/components/bluetooth/translations/el.json index 0e0e512e9a5..5175abe95d3 100644 --- a/homeassistant/components/bluetooth/translations/el.json +++ b/homeassistant/components/bluetooth/translations/el.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af", - "no_adapters": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03b5\u03af\u03c2 Bluetooth" + "no_adapters": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03bf\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03b5\u03af\u03c2 Bluetooth" }, "flow_title": "{name}", "step": { @@ -36,7 +36,7 @@ "step": { "init": { "data": { - "passive": "\u03a0\u03b1\u03b8\u03b7\u03c4\u03b9\u03ba\u03ae \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7" + "passive": "\u03a0\u03b1\u03b8\u03b7\u03c4\u03b9\u03ba\u03ae \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7" } } } diff --git a/homeassistant/components/bluetooth/translations/he.json b/homeassistant/components/bluetooth/translations/he.json index b2cb1c6814d..64c000e80b7 100644 --- a/homeassistant/components/bluetooth/translations/he.json +++ b/homeassistant/components/bluetooth/translations/he.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u05e9\u05d9\u05e8\u05d5\u05ea \u05d6\u05d4 \u05db\u05d1\u05e8 \u05de\u05d5\u05d2\u05d3\u05e8", - "no_adapters": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05ea\u05d0\u05de\u05d9 \u05e9\u05df \u05db\u05d7\u05d5\u05dc\u05d4 \u05e9\u05d0\u05d9\u05e0\u05dd \u05de\u05d5\u05d2\u05d3\u05e8\u05d9\u05dd" + "no_adapters": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05de\u05ea\u05d0\u05de\u05d9 Bluetooth \u05dc\u05d0 \u05de\u05d5\u05d2\u05d3\u05e8\u05d9\u05dd" }, "flow_title": "{name}", "step": { @@ -13,10 +13,10 @@ "data": { "adapter": "\u05de\u05ea\u05d0\u05dd" }, - "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05de\u05ea\u05d0\u05dd \u05e9\u05df \u05db\u05d7\u05d5\u05dc\u05d4 \u05dc\u05d4\u05ea\u05e7\u05e0\u05d4" + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05de\u05ea\u05d0\u05dd Bluetooth \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4" }, "single_adapter": { - "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05de\u05ea\u05d0\u05dd \u05e9\u05df \u05db\u05d7\u05d5\u05dc\u05d4 {name}?" + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea \u05de\u05ea\u05d0\u05dd Bluetooth {name}?" }, "user": { "data": { diff --git a/homeassistant/components/bluetooth/translations/lv.json b/homeassistant/components/bluetooth/translations/lv.json new file mode 100644 index 00000000000..e8940bef26a --- /dev/null +++ b/homeassistant/components/bluetooth/translations/lv.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/nl.json b/homeassistant/components/bluetooth/translations/nl.json index 3fee489370e..60b63b4e38b 100644 --- a/homeassistant/components/bluetooth/translations/nl.json +++ b/homeassistant/components/bluetooth/translations/nl.json @@ -26,5 +26,14 @@ "haos_outdated": { "title": "Update naar het Home Assistant-besturingssysteem versie 9.0 of hoger" } + }, + "options": { + "step": { + "init": { + "data": { + "passive": "Passief scannen" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/bluetooth/translations/tr.json b/homeassistant/components/bluetooth/translations/tr.json index 9e8516ba25c..43740b5fe54 100644 --- a/homeassistant/components/bluetooth/translations/tr.json +++ b/homeassistant/components/bluetooth/translations/tr.json @@ -7,13 +7,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "multiple_adapters": { "data": { "adapter": "Adapt\u00f6r" }, - "description": "Kurulum i\u00e7in bir Bluetooth adapt\u00f6r\u00fc se\u00e7in" + "description": "Kurmak i\u00e7in bir Bluetooth adapt\u00f6r\u00fc se\u00e7in" }, "single_adapter": { "description": "{name} Bluetooth adapt\u00f6r\u00fcn\u00fc kurmak istiyor musunuz?" @@ -22,7 +22,7 @@ "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } }, diff --git a/homeassistant/components/bluetooth/translations/uk.json b/homeassistant/components/bluetooth/translations/uk.json new file mode 100644 index 00000000000..8f725d57d17 --- /dev/null +++ b/homeassistant/components/bluetooth/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "address": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth/usage.py b/homeassistant/components/bluetooth/usage.py index 0b1e615ddda..b751559e7a4 100644 --- a/homeassistant/components/bluetooth/usage.py +++ b/homeassistant/components/bluetooth/usage.py @@ -16,17 +16,22 @@ ORIGINAL_BLEAK_RETRY_CONNECTOR_CLIENT = ( def install_multiple_bleak_catcher() -> None: - """Wrap the bleak classes to return the shared instance if multiple instances are detected.""" + """Wrap the bleak classes to return the shared instance. + + In case multiple instances are detected. + """ bleak.BleakScanner = HaBleakScannerWrapper # type: ignore[misc, assignment] bleak.BleakClient = HaBleakClientWrapper # type: ignore[misc] - bleak_retry_connector.BleakClientWithServiceCache = HaBleakClientWithServiceCache # type: ignore[misc,assignment] + bleak_retry_connector.BleakClientWithServiceCache = HaBleakClientWithServiceCache # type: ignore[misc,assignment] # noqa: E501 def uninstall_multiple_bleak_catcher() -> None: """Unwrap the bleak classes.""" bleak.BleakScanner = ORIGINAL_BLEAK_SCANNER # type: ignore[misc] bleak.BleakClient = ORIGINAL_BLEAK_CLIENT # type: ignore[misc] - bleak_retry_connector.BleakClientWithServiceCache = ORIGINAL_BLEAK_RETRY_CONNECTOR_CLIENT # type: ignore[misc] + bleak_retry_connector.BleakClientWithServiceCache = ( # type: ignore[misc] + ORIGINAL_BLEAK_RETRY_CONNECTOR_CLIENT + ) class HaBleakClientWithServiceCache(HaBleakClientWrapper): diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 5419fa79e1c..e78eb51a38c 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -15,7 +15,10 @@ from .storage import BluetoothStorage def async_load_history_from_system( adapters: BluetoothAdapters, storage: BluetoothStorage ) -> tuple[dict[str, BluetoothServiceInfoBleak], dict[str, BluetoothServiceInfoBleak]]: - """Load the device and advertisement_data history if available on the current system.""" + """Load the device and advertisement_data history. + + Only loads if available on the current system. + """ now_monotonic = monotonic_time_coarse() connectable_loaded_history: dict[str, BluetoothServiceInfoBleak] = {} all_loaded_history: dict[str, BluetoothServiceInfoBleak] = {} diff --git a/homeassistant/components/bluetooth/wrappers.py b/homeassistant/components/bluetooth/wrappers.py index a2c417ca382..6b463423c73 100644 --- a/homeassistant/components/bluetooth/wrappers.py +++ b/homeassistant/components/bluetooth/wrappers.py @@ -12,11 +12,7 @@ from typing import TYPE_CHECKING, Any, Final from bleak import BleakClient, BleakError from bleak.backends.client import BaseBleakClient, get_platform_client_backend_type from bleak.backends.device import BLEDevice -from bleak.backends.scanner import ( - AdvertisementData, - AdvertisementDataCallback, - BaseBleakScanner, -) +from bleak.backends.scanner import AdvertisementDataCallback, BaseBleakScanner from bleak_retry_connector import ( NO_RSSI_VALUE, ble_device_description, @@ -28,7 +24,7 @@ from homeassistant.core import CALLBACK_TYPE, callback as hass_callback from homeassistant.helpers.frame import report from . import models -from .base_scanner import BaseHaScanner +from .base_scanner import BaseHaScanner, BluetoothScannerDevice FILTER_UUIDS: Final = "UUIDs" _LOGGER = logging.getLogger(__name__) @@ -119,10 +115,11 @@ class HaBleakScannerWrapper(BaseBleakScanner): def register_detection_callback( self, callback: AdvertisementDataCallback | None ) -> None: - """Register a callback that is called when a device is discovered or has a property changed. + """Register a detection callback. - This method takes the callback and registers it with the long running - scanner. + The callback is called when a device is discovered or has a property changed. + + This method takes the callback and registers it with the long running sscanner. """ self._advertisement_data_callback = callback self._setup_detection_callback() @@ -148,13 +145,13 @@ class HaBleakScannerWrapper(BaseBleakScanner): def _rssi_sorter_with_connection_failure_penalty( - scanner_device_advertisement_data: tuple[ - BaseHaScanner, BLEDevice, AdvertisementData - ], + device: BluetoothScannerDevice, connection_failure_count: dict[BaseHaScanner, int], rssi_diff: int, ) -> float: - """Get a sorted list of scanner, device, advertisement data adjusting for previous connection failures. + """Get a sorted list of scanner, device, advertisement data. + + Adjusting for previous connection failures. When a connection fails, we want to try the next best adapter so we apply a penalty to the RSSI value to make it less likely to be chosen @@ -165,9 +162,8 @@ def _rssi_sorter_with_connection_failure_penalty( best adapter twice before moving on to the next best adapter since the first failure may be a transient service resolution issue. """ - scanner, _, advertisement_data = scanner_device_advertisement_data - base_rssi = advertisement_data.rssi or NO_RSSI_VALUE - if connect_failures := connection_failure_count.get(scanner): + base_rssi = device.advertisement.rssi or NO_RSSI_VALUE + if connect_failures := connection_failure_count.get(device.scanner): if connect_failures > 1 and not rssi_diff: rssi_diff = 1 return base_rssi - (rssi_diff * connect_failures * 0.51) @@ -227,7 +223,10 @@ class HaBleakClientWrapper(BleakClient): """Set the disconnect callback.""" self.__disconnected_callback = callback if self._backend: - self._backend.set_disconnected_callback(callback, **kwargs) # type: ignore[arg-type] + self._backend.set_disconnected_callback( + callback, # type: ignore[arg-type] + **kwargs, + ) async def connect(self, **kwargs: Any) -> bool: """Connect to the specified GATT server.""" @@ -294,15 +293,10 @@ class HaBleakClientWrapper(BleakClient): that has a free connection slot. """ address = self.__address - scanner_device_advertisement_datas = manager.async_get_scanner_discovered_devices_and_advertisement_data_by_address( - address, True - ) - sorted_scanner_device_advertisement_datas = sorted( - scanner_device_advertisement_datas, - key=lambda scanner_device_advertisement_data: scanner_device_advertisement_data[ - 2 - ].rssi - or NO_RSSI_VALUE, + devices = manager.async_scanner_devices_by_address(self.__address, True) + sorted_devices = sorted( + devices, + key=lambda device: device.advertisement.rssi or NO_RSSI_VALUE, reverse=True, ) @@ -310,31 +304,28 @@ class HaBleakClientWrapper(BleakClient): # to prefer the adapter/scanner with the less failures so # we don't keep trying to connect with an adapter # that is failing - if ( - self.__connect_failures - and len(sorted_scanner_device_advertisement_datas) > 1 - ): + if self.__connect_failures and len(sorted_devices) > 1: # We use the rssi diff between to the top two # to adjust the rssi sorter so that each failure # will reduce the rssi sorter by the diff amount rssi_diff = ( - sorted_scanner_device_advertisement_datas[0][2].rssi - - sorted_scanner_device_advertisement_datas[1][2].rssi + sorted_devices[0].advertisement.rssi + - sorted_devices[1].advertisement.rssi ) adjusted_rssi_sorter = partial( _rssi_sorter_with_connection_failure_penalty, connection_failure_count=self.__connect_failures, rssi_diff=rssi_diff, ) - sorted_scanner_device_advertisement_datas = sorted( - scanner_device_advertisement_datas, + sorted_devices = sorted( + devices, key=adjusted_rssi_sorter, reverse=True, ) - for (scanner, ble_device, _) in sorted_scanner_device_advertisement_datas: + for device in sorted_devices: if backend := self._async_get_backend_for_ble_device( - manager, scanner, ble_device + manager, device.scanner, device.ble_device ): return backend diff --git a/homeassistant/components/bluetooth_adapters/__init__.py b/homeassistant/components/bluetooth_adapters/__init__.py new file mode 100644 index 00000000000..c2af10d5455 --- /dev/null +++ b/homeassistant/components/bluetooth_adapters/__init__.py @@ -0,0 +1,20 @@ +"""The Bluetooth Adapters integration.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + +DOMAIN = "bluetooth_adapters" + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up Bluetooth Adapters from a config entry. + + This integration is only used as a dependency for other integrations + that need Bluetooth Adapters. + + All integrations that provide Bluetooth Adapters must be listed + in after_dependencies in the manifest.json file to ensure + they are loaded before this integration. + """ + return True diff --git a/homeassistant/components/bluetooth_adapters/manifest.json b/homeassistant/components/bluetooth_adapters/manifest.json new file mode 100644 index 00000000000..a4297871480 --- /dev/null +++ b/homeassistant/components/bluetooth_adapters/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "bluetooth_adapters", + "name": "Bluetooth Adapters", + "documentation": "https://www.home-assistant.io/integrations/bluetooth_adapters", + "dependencies": ["bluetooth"], + "after_dependencies": ["esphome", "shelly", "ruuvi_gateway"], + "quality_scale": "internal", + "codeowners": ["@bdraco"], + "iot_class": "local_push", + "integration_type": "system" +} diff --git a/homeassistant/components/bluetooth_le_tracker/manifest.json b/homeassistant/components/bluetooth_le_tracker/manifest.json index 6d1d4ba2d4a..c2eeaa10415 100644 --- a/homeassistant/components/bluetooth_le_tracker/manifest.json +++ b/homeassistant/components/bluetooth_le_tracker/manifest.json @@ -2,7 +2,7 @@ "domain": "bluetooth_le_tracker", "name": "Bluetooth LE Tracker", "documentation": "https://www.home-assistant.io/integrations/bluetooth_le_tracker", - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": [], "iot_class": "local_push", "loggers": [] diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 43beb2cbf81..df25efb6d5e 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -38,10 +38,10 @@ ALLOWED_CONDITION_BASED_SERVICE_KEYS = { "VEHICLE_CHECK", "VEHICLE_TUV", } -LOGGED_CONDITION_BASED_SERVICE_WARNINGS = set() +LOGGED_CONDITION_BASED_SERVICE_WARNINGS: set[str] = set() ALLOWED_CHECK_CONTROL_MESSAGE_KEYS = {"ENGINE_OIL", "TIRE_PRESSURE"} -LOGGED_CHECK_CONTROL_MESSAGE_WARNINGS = set() +LOGGED_CHECK_CONTROL_MESSAGE_WARNINGS: set[str] = set() def _condition_based_services( diff --git a/homeassistant/components/bmw_connected_drive/diagnostics.py b/homeassistant/components/bmw_connected_drive/diagnostics.py new file mode 100644 index 00000000000..c69d06d818f --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/diagnostics.py @@ -0,0 +1,98 @@ +"""Diagnostics support for the BMW Connected Drive integration.""" +from __future__ import annotations + +from dataclasses import asdict +import json +from typing import TYPE_CHECKING, Any + +from bimmer_connected.utils import MyBMWJSONEncoder + +from homeassistant.components.diagnostics.util 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.device_registry import DeviceEntry + +from .const import CONF_REFRESH_TOKEN, DOMAIN + +if TYPE_CHECKING: + from bimmer_connected.vehicle import MyBMWVehicle + + from .coordinator import BMWDataUpdateCoordinator + +TO_REDACT_INFO = [CONF_USERNAME, CONF_PASSWORD, CONF_REFRESH_TOKEN] +TO_REDACT_DATA = [ + "lat", + "latitude", + "lon", + "longitude", + "heading", + "vin", + "licensePlate", + "city", + "street", + "streetNumber", + "postalCode", + "phone", + "formatted", + "subtitle", +] + + +def vehicle_to_dict(vehicle: MyBMWVehicle | None) -> dict: + """Convert a MyBMWVehicle to a dictionary using MyBMWJSONEncoder.""" + retval: dict = json.loads(json.dumps(vehicle, cls=MyBMWJSONEncoder)) + return retval + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + coordinator.account.config.log_responses = True + await coordinator.account.get_vehicles(force_init=True) + + diagnostics_data = { + "info": async_redact_data(config_entry.data, TO_REDACT_INFO), + "data": [ + async_redact_data(vehicle_to_dict(vehicle), TO_REDACT_DATA) + for vehicle in coordinator.account.vehicles + ], + "fingerprint": async_redact_data( + [asdict(r) for r in coordinator.account.get_stored_responses()], + TO_REDACT_DATA, + ), + } + + coordinator.account.config.log_responses = False + + return diagnostics_data + + +async def async_get_device_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry +) -> dict[str, Any]: + """Return diagnostics for a device.""" + coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + coordinator.account.config.log_responses = True + await coordinator.account.get_vehicles(force_init=True) + + vin = next(iter(device.identifiers))[1] + vehicle = coordinator.account.get_vehicle(vin) + + diagnostics_data = { + "info": async_redact_data(config_entry.data, TO_REDACT_INFO), + "data": async_redact_data(vehicle_to_dict(vehicle), TO_REDACT_DATA), + # Always have to get the full fingerprint as the VIN is redacted beforehand by the library + "fingerprint": async_redact_data( + [asdict(r) for r in coordinator.account.get_stored_responses()], + TO_REDACT_DATA, + ), + } + + coordinator.account.config.log_responses = False + + return diagnostics_data diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py index f9f09cfe3cb..8c9fef6bd7f 100644 --- a/homeassistant/components/bond/entity.py +++ b/homeassistant/components/bond/entity.py @@ -3,9 +3,8 @@ from __future__ import annotations from abc import abstractmethod from asyncio import Lock, TimeoutError as AsyncIOTimeoutError -from datetime import timedelta +from datetime import datetime, timedelta import logging -from typing import Any from aiohttp import ClientError from bond_async import BPUPSubscriptions @@ -50,7 +49,7 @@ class BondEntity(Entity): self._sub_device = sub_device self._attr_available = True self._bpup_subs = bpup_subs - self._update_lock: Lock | None = None + self._update_lock = Lock() self._initialized = False if sub_device_id: sub_device_id = f"_{sub_device_id}" @@ -104,7 +103,8 @@ class BondEntity(Entity): """Fetch assumed state of the cover from the hub using API.""" await self._async_update_from_api() - async def _async_update_if_bpup_not_alive(self, *_: Any) -> None: + @callback + def _async_update_if_bpup_not_alive(self, now: datetime) -> None: """Fetch via the API if BPUP is not alive.""" if ( self.hass.is_stopping @@ -113,8 +113,6 @@ class BondEntity(Entity): and self.available ): return - - assert self._update_lock is not None if self._update_lock.locked(): _LOGGER.warning( "Updating %s took longer than the scheduled update interval %s", @@ -122,7 +120,10 @@ class BondEntity(Entity): _FALLBACK_SCAN_INTERVAL, ) return + self.hass.async_create_task(self._async_update()) + async def _async_update(self) -> None: + """Fetch via the API.""" async with self._update_lock: await self._async_update_from_api() self.async_write_ha_state() @@ -170,7 +171,6 @@ class BondEntity(Entity): async def async_added_to_hass(self) -> None: """Subscribe to BPUP and start polling.""" await super().async_added_to_hass() - self._update_lock = Lock() self._bpup_subs.subscribe(self._device_id, self._async_bpup_callback) self.async_on_remove( async_track_time_interval( diff --git a/homeassistant/components/bosch_shc/config_flow.py b/homeassistant/components/bosch_shc/config_flow.py index 7cc4527a64f..27d428423d6 100644 --- a/homeassistant/components/bosch_shc/config_flow.py +++ b/homeassistant/components/bosch_shc/config_flow.py @@ -1,8 +1,10 @@ """Config flow for Bosch Smart Home Controller integration.""" +from __future__ import annotations + from collections.abc import Mapping import logging from os import makedirs -from typing import Any +from typing import Any, cast from boschshcpy import SHCRegisterClient, SHCSession from boschshcpy.exceptions import ( @@ -13,9 +15,10 @@ from boschshcpy.exceptions import ( ) import voluptuous as vol -from homeassistant import config_entries, core +from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TOKEN +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from .const import ( @@ -36,14 +39,19 @@ HOST_SCHEMA = vol.Schema( ) -def write_tls_asset(hass: core.HomeAssistant, filename: str, asset: bytes) -> None: +def write_tls_asset(hass: HomeAssistant, filename: str, asset: bytes) -> None: """Write the tls assets to disk.""" makedirs(hass.config.path(DOMAIN), exist_ok=True) with open(hass.config.path(DOMAIN, filename), "w", encoding="utf8") as file_handle: file_handle.write(asset.decode("utf-8")) -def create_credentials_and_validate(hass, host, user_input, zeroconf_instance): +def create_credentials_and_validate( + hass: HomeAssistant, + host: str, + user_input: dict[str, Any], + zeroconf_instance: zeroconf.HaZeroconf, +) -> dict[str, Any] | None: """Create and store credentials and validate session.""" helper = SHCRegisterClient(host, user_input[CONF_PASSWORD]) result = helper.register(host, "HomeAssistant") @@ -64,7 +72,9 @@ def create_credentials_and_validate(hass, host, user_input, zeroconf_instance): return result -def get_info_from_host(hass, host, zeroconf_instance): +def get_info_from_host( + hass: HomeAssistant, host: str, zeroconf_instance: zeroconf.HaZeroconf +) -> dict[str, str | None]: """Get information from host.""" session = SHCSession( host, @@ -81,15 +91,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Bosch SHC.""" VERSION = 1 - info = None - host = None - hostname = None + info: dict[str, str | None] + host: str | None = None async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" 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.""" if user_input is None: return self.async_show_form( @@ -100,9 +111,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.info = await self._get_info(host) return await self.async_step_credentials() - 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] = {} if user_input is not None: host = user_input[CONF_HOST] try: @@ -122,9 +135,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=HOST_SCHEMA, errors=errors ) - async def async_step_credentials(self, user_input=None): + async def async_step_credentials( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the credentials step.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: zeroconf_instance = await zeroconf.async_get_instance(self.hass) try: @@ -149,6 +164,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: + assert result entry_data = { CONF_SSL_CERTIFICATE: self.hass.config.path(DOMAIN, CONF_SHC_CERT), CONF_SSL_KEY: self.hass.config.path(DOMAIN, CONF_SHC_KEY), @@ -166,7 +182,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="reauth_successful") return self.async_create_entry( - title=self.info["title"], + title=cast(str, self.info["title"]), data=entry_data, ) else: @@ -198,16 +214,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.host = discovery_info.host local_name = discovery_info.hostname[:-1] - node_name = local_name[: -len(".local")] + node_name = local_name.removesuffix(".local") await self.async_set_unique_id(self.info["unique_id"]) self._abort_if_unique_id_configured({CONF_HOST: self.host}) self.context["title_placeholders"] = {"name": node_name} return await self.async_step_confirm_discovery() - async def async_step_confirm_discovery(self, user_input=None): + async def async_step_confirm_discovery( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle discovery confirm.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: return await self.async_step_credentials() @@ -220,7 +238,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def _get_info(self, host): + async def _get_info(self, host: str) -> dict[str, str | None]: """Get additional information.""" zeroconf_instance = await zeroconf.async_get_instance(self.hass) diff --git a/homeassistant/components/bosch_shc/strings.json b/homeassistant/components/bosch_shc/strings.json index 15fb061ef2b..2b5720f0849 100644 --- a/homeassistant/components/bosch_shc/strings.json +++ b/homeassistant/components/bosch_shc/strings.json @@ -14,11 +14,14 @@ } }, "confirm_discovery": { - "description": "Please press the Bosch Smart Home Controller's front-side button until LED starts flashing.\nReady to continue to set up {model} @ {host} with Home Assistant?" + "description": "Smart Home Controller I: Please press the front-side button until LED starts flashing.\nSmart Home Controller II: Press the function button shortly. Cloud and network lights start blinking orange.\nDevice is now ready to be paired.\n\nReady to continue to set up {model} @ {host} with Home Assistant?" }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "The bosch_shc integration needs to re-authenticate your account" + "description": "The bosch_shc integration needs to re-authenticate your account", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } } }, "error": { diff --git a/homeassistant/components/bosch_shc/translations/de.json b/homeassistant/components/bosch_shc/translations/de.json index 6ace3cedc95..9c0a9a359f9 100644 --- a/homeassistant/components/bosch_shc/translations/de.json +++ b/homeassistant/components/bosch_shc/translations/de.json @@ -14,7 +14,7 @@ "flow_title": "Bosch SHC: {name}", "step": { "confirm_discovery": { - "description": "Bitte dr\u00fccke die frontseitige Taste des Bosch Smart Home Controllers, bis die LED zu blinken beginnt.\nBist du bereit, mit der Einrichtung von {model} @ {host} in Home Assistant fortzufahren?" + "description": "Smart Home Controller I: Bitte dr\u00fccke die frontseitige Taste, bis die LED zu blinken beginnt.\nSmart Home Controller II: Dr\u00fccke kurz die Funktionstaste. Die Cloud- und Netzwerkleuchten beginnen orange zu blinken.\nDas Ger\u00e4t ist nun f\u00fcr die Kopplung bereit.\n\nBist du bereit, mit der Einrichtung von {model} @ {host} in Home Assistant fortzufahren?" }, "credentials": { "data": { @@ -23,7 +23,10 @@ }, "reauth_confirm": { "description": "Die bosch_shc Integration muss dein Konto neu authentifizieren", - "title": "Integration erneut authentifizieren" + "title": "Integration erneut authentifizieren", + "data": { + "host": "Host" + } }, "user": { "data": { diff --git a/homeassistant/components/bosch_shc/translations/en.json b/homeassistant/components/bosch_shc/translations/en.json index ab5cde9ef27..ad7859e80e6 100644 --- a/homeassistant/components/bosch_shc/translations/en.json +++ b/homeassistant/components/bosch_shc/translations/en.json @@ -14,7 +14,7 @@ "flow_title": "Bosch SHC: {name}", "step": { "confirm_discovery": { - "description": "Please press the Bosch Smart Home Controller's front-side button until LED starts flashing.\nReady to continue to set up {model} @ {host} with Home Assistant?" + "description": "Smart Home Controller I: Please press the front-side button until LED starts flashing.\nSmart Home Controller II: Press the function button shortly. Cloud and network lights start blinking orange.\nDevice is now ready to be paired.\n\nReady to continue to set up {model} @ {host} with Home Assistant?" }, "credentials": { "data": { @@ -23,7 +23,10 @@ }, "reauth_confirm": { "description": "The bosch_shc integration needs to re-authenticate your account", - "title": "Reauthenticate Integration" + "title": "Reauthenticate Integration", + "data": { + "host": "Host" + } }, "user": { "data": { diff --git a/homeassistant/components/bosch_shc/translations/lv.json b/homeassistant/components/bosch_shc/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bosch_shc/translations/uk.json b/homeassistant/components/bosch_shc/translations/uk.json new file mode 100644 index 00000000000..7f79c289987 --- /dev/null +++ b/homeassistant/components/bosch_shc/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "credentials": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c Smart Home Controller" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/__init__.py b/homeassistant/components/braviatv/__init__.py index 321d864f036..ecf119c8a3d 100644 --- a/homeassistant/components/braviatv/__init__.py +++ b/homeassistant/components/braviatv/__init__.py @@ -4,14 +4,14 @@ from __future__ import annotations from typing import Final from aiohttp import CookieJar -from pybravia import BraviaTV +from pybravia import BraviaClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_create_clientsession -from .const import CONF_IGNORED_SOURCES, DOMAIN +from .const import DOMAIN from .coordinator import BraviaTVCoordinator PLATFORMS: Final[list[Platform]] = [ @@ -25,17 +25,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b """Set up a config entry.""" host = config_entry.data[CONF_HOST] mac = config_entry.data[CONF_MAC] - ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES, []) session = async_create_clientsession( hass, cookie_jar=CookieJar(unsafe=True, quote_cookie=False) ) - client = BraviaTV(host, mac, session=session) + client = BraviaClient(host, mac, session=session) coordinator = BraviaTVCoordinator( hass=hass, client=client, config=config_entry.data, - ignored_sources=ignored_sources, ) config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/braviatv/config_flow.py b/homeassistant/components/braviatv/config_flow.py index 43d2059c547..3fb6e6b3b40 100644 --- a/homeassistant/components/braviatv/config_flow.py +++ b/homeassistant/components/braviatv/config_flow.py @@ -6,27 +6,23 @@ from typing import Any from urllib.parse import urlparse from aiohttp import CookieJar -from pybravia import BraviaTV, BraviaTVAuthError, BraviaTVError, BraviaTVNotSupported +from pybravia import BraviaAuthError, BraviaClient, BraviaError, BraviaNotSupported import voluptuous as vol from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PIN -from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import instance_id from homeassistant.helpers.aiohttp_client import async_create_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.util.network import is_host_valid -from . import BraviaTVCoordinator from .const import ( ATTR_CID, ATTR_MAC, ATTR_MODEL, CONF_CLIENT_ID, - CONF_IGNORED_SOURCES, CONF_NICKNAME, CONF_USE_PSK, DOMAIN, @@ -41,16 +37,10 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize config flow.""" - self.client: BraviaTV | None = None + self.client: BraviaClient | None = None self.device_config: dict[str, Any] = {} self.entry: ConfigEntry | None = None - @staticmethod - @callback - def async_get_options_flow(config_entry: ConfigEntry) -> BraviaTVOptionsFlowHandler: - """Bravia TV options callback.""" - return BraviaTVOptionsFlowHandler(config_entry) - def create_client(self) -> None: """Create Bravia TV client from config.""" host = self.device_config[CONF_HOST] @@ -58,7 +48,7 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.hass, cookie_jar=CookieJar(unsafe=True, quote_cookie=False), ) - self.client = BraviaTV(host=host, session=session) + self.client = BraviaClient(host=host, session=session) async def gen_instance_ids(self) -> tuple[str, str]: """Generate client_id and nickname.""" @@ -162,18 +152,18 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self.entry: return await self.async_reauth_device() return await self.async_create_device() - except BraviaTVAuthError: + except BraviaAuthError: errors["base"] = "invalid_auth" - except BraviaTVNotSupported: + except BraviaNotSupported: errors["base"] = "unsupported_model" - except BraviaTVError: + except BraviaError: errors["base"] = "cannot_connect" assert self.client try: await self.client.pair(client_id, nickname) - except BraviaTVError: + except BraviaError: return self.async_abort(reason="no_ip_control") return self.async_show_form( @@ -198,11 +188,11 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self.entry: return await self.async_reauth_device() return await self.async_create_device() - except BraviaTVAuthError: + except BraviaAuthError: errors["base"] = "invalid_auth" - except BraviaTVNotSupported: + except BraviaNotSupported: errors["base"] = "unsupported_model" - except BraviaTVError: + except BraviaError: errors["base"] = "cannot_connect" return self.async_show_form( @@ -257,51 +247,3 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) self.device_config = {**entry_data} return await self.async_step_authorize() - - -class BraviaTVOptionsFlowHandler(config_entries.OptionsFlowWithConfigEntry): - """Config flow options for Bravia TV.""" - - data_schema: vol.Schema - - async def async_step_init( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Manage the options.""" - coordinator: BraviaTVCoordinator - coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id] - - try: - await coordinator.async_update_sources() - except BraviaTVError: - return self.async_abort(reason="failed_update") - - sources = coordinator.source_map.values() - source_list = [item["title"] for item in sources] - ignored_sources = self.options.get(CONF_IGNORED_SOURCES, []) - - for item in ignored_sources: - if item not in source_list: - source_list.append(item) - - self.data_schema = vol.Schema( - { - vol.Optional(CONF_IGNORED_SOURCES): cv.multi_select(source_list), - } - ) - - return await self.async_step_user() - - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle a flow initialized by the user.""" - if user_input is not None: - return self.async_create_entry(title="", data=user_input) - - return self.async_show_form( - step_id="user", - data_schema=self.add_suggested_values_to_schema( - self.data_schema, self.options - ), - ) diff --git a/homeassistant/components/braviatv/const.py b/homeassistant/components/braviatv/const.py index e7bdf00d507..5925a97422a 100644 --- a/homeassistant/components/braviatv/const.py +++ b/homeassistant/components/braviatv/const.py @@ -3,16 +3,25 @@ from __future__ import annotations from typing import Final +from homeassistant.backports.enum import StrEnum + ATTR_CID: Final = "cid" ATTR_MAC: Final = "macAddr" ATTR_MANUFACTURER: Final = "Sony" ATTR_MODEL: Final = "model" CONF_CLIENT_ID: Final = "client_id" -CONF_IGNORED_SOURCES: Final = "ignored_sources" CONF_NICKNAME: Final = "nickname" CONF_USE_PSK: Final = "use_psk" DOMAIN: Final = "braviatv" LEGACY_CLIENT_ID: Final = "HomeAssistant" NICKNAME_PREFIX: Final = "Home Assistant" + + +class SourceType(StrEnum): + """Source type for Sony TV Integration.""" + + APP = "app" + CHANNEL = "channel" + INPUT = "input" diff --git a/homeassistant/components/braviatv/coordinator.py b/homeassistant/components/braviatv/coordinator.py index 3d57850a648..6923bacc1ac 100644 --- a/homeassistant/components/braviatv/coordinator.py +++ b/homeassistant/components/braviatv/coordinator.py @@ -6,18 +6,17 @@ from datetime import timedelta from functools import wraps import logging from types import MappingProxyType -from typing import Any, Final, TypeVar +from typing import Any, Concatenate, Final, ParamSpec, TypeVar from pybravia import ( - BraviaTV, - BraviaTVAuthError, - BraviaTVConnectionError, - BraviaTVConnectionTimeout, - BraviaTVError, - BraviaTVNotFound, - BraviaTVTurnedOff, + BraviaAuthError, + BraviaClient, + BraviaConnectionError, + BraviaConnectionTimeout, + BraviaError, + BraviaNotFound, + BraviaTurnedOff, ) -from typing_extensions import Concatenate, ParamSpec from homeassistant.components.media_player import MediaType from homeassistant.const import CONF_PIN @@ -33,6 +32,7 @@ from .const import ( DOMAIN, LEGACY_CLIENT_ID, NICKNAME_PREFIX, + SourceType, ) _BraviaTVCoordinatorT = TypeVar("_BraviaTVCoordinatorT", bound="BraviaTVCoordinator") @@ -45,7 +45,7 @@ SCAN_INTERVAL: Final = timedelta(seconds=10) def catch_braviatv_errors( func: Callable[Concatenate[_BraviaTVCoordinatorT, _P], Awaitable[None]] ) -> Callable[Concatenate[_BraviaTVCoordinatorT, _P], Coroutine[Any, Any, None]]: - """Catch BraviaTV errors.""" + """Catch Bravia errors.""" @wraps(func) async def wrapper( @@ -53,10 +53,10 @@ def catch_braviatv_errors( *args: _P.args, **kwargs: _P.kwargs, ) -> None: - """Catch BraviaTV errors and log message.""" + """Catch Bravia errors and log message.""" try: await func(self, *args, **kwargs) - except BraviaTVError as err: + except BraviaError as err: _LOGGER.error("Command error: %s", err) await self.async_request_refresh() @@ -69,9 +69,8 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): def __init__( self, hass: HomeAssistant, - client: BraviaTV, + client: BraviaClient, config: MappingProxyType[str, Any], - ignored_sources: list[str], ) -> None: """Initialize Bravia TV Client.""" @@ -80,11 +79,11 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.use_psk = config.get(CONF_USE_PSK, False) self.client_id = config.get(CONF_CLIENT_ID, LEGACY_CLIENT_ID) self.nickname = config.get(CONF_NICKNAME, NICKNAME_PREFIX) - self.ignored_sources = ignored_sources self.source: str | None = None self.source_list: list[str] = [] self.source_map: dict[str, dict] = {} self.media_title: str | None = None + self.media_channel: str | None = None self.media_content_id: str | None = None self.media_content_type: MediaType | None = None self.media_uri: str | None = None @@ -93,10 +92,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.volume_target: str | None = None self.volume_muted = False self.is_on = False - self.is_channel = False self.connected = False - # Assume that the TV is in Play mode - self.playing = True self.skipped_updates = 0 super().__init__( @@ -109,16 +105,23 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): ), ) - def _sources_extend(self, sources: list[dict], source_type: str) -> None: + def _sources_extend( + self, + sources: list[dict], + source_type: SourceType, + add_to_list: bool = False, + sort_by: str | None = None, + ) -> None: """Extend source map and source list.""" + if sort_by: + sources = sorted(sources, key=lambda d: d.get(sort_by, "")) for item in sources: - item["type"] = source_type title = item.get("title") uri = item.get("uri") if not title or not uri: continue - self.source_map[uri] = item - if title not in self.ignored_sources: + self.source_map[uri] = {**item, "type": source_type} + if add_to_list and title not in self.source_list: self.source_list.append(title) async def _async_update_data(self) -> None: @@ -135,7 +138,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): nickname=self.nickname, ) self.connected = True - except BraviaTVAuthError as err: + except BraviaAuthError as err: raise ConfigEntryAuthFailed from err power_status = await self.client.get_power_status() @@ -149,41 +152,26 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): await self.async_update_sources() await self.async_update_volume() await self.async_update_playing() - except BraviaTVNotFound as err: + except BraviaNotFound as err: if self.skipped_updates < 10: self.connected = False self.skipped_updates += 1 _LOGGER.debug("Update skipped, Bravia API service is reloading") return raise UpdateFailed("Error communicating with device") from err - except (BraviaTVConnectionError, BraviaTVConnectionTimeout, BraviaTVTurnedOff): + except (BraviaConnectionError, BraviaConnectionTimeout, BraviaTurnedOff): self.is_on = False self.connected = False _LOGGER.debug("Update skipped, Bravia TV is off") - except BraviaTVError as err: + except BraviaError as err: self.is_on = False self.connected = False raise UpdateFailed("Error communicating with device") from err - async def async_update_sources(self) -> None: - """Update sources.""" - self.source_list = [] - self.source_map = {} - - externals = await self.client.get_external_status() - self._sources_extend(externals, "input") - - apps = await self.client.get_app_list() - self._sources_extend(apps, "app") - - channels = await self.client.get_content_list_all("tv") - self._sources_extend(channels, "channel") - async def async_update_volume(self) -> None: """Update volume information.""" volume_info = await self.client.get_volume_info() - volume_level = volume_info.get("volume") - if volume_level is not None: + if volume_level := volume_info.get("volume"): self.volume_level = volume_level / 100 self.volume_muted = volume_info.get("mute", False) self.volume_target = volume_info.get("target") @@ -194,27 +182,68 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): self.media_title = playing_info.get("title") self.media_uri = playing_info.get("uri") self.media_duration = playing_info.get("durationSec") - if program_title := playing_info.get("programTitle"): - self.media_title = f"{self.media_title}: {program_title}" + self.media_channel = None + self.media_content_id = None + self.media_content_type = None + self.source = None if self.media_uri: - source = self.source_map.get(self.media_uri, {}) - self.source = source.get("title") - self.is_channel = self.media_uri[:2] == "tv" - if self.is_channel: + self.media_content_id = self.media_uri + if self.media_uri[:8] == "extInput": + self.source = playing_info.get("title") + if self.media_uri[:2] == "tv": + self.media_title = playing_info.get("programTitle") + self.media_channel = playing_info.get("title") self.media_content_id = playing_info.get("dispNum") self.media_content_type = MediaType.CHANNEL - else: - self.media_content_id = self.media_uri - self.media_content_type = None - else: - self.source = None - self.is_channel = False - self.media_content_id = None - self.media_content_type = None if not playing_info: self.media_title = "Smart TV" self.media_content_type = MediaType.APP + async def async_update_sources(self) -> None: + """Update all sources.""" + self.source_list = [] + self.source_map = {} + + inputs = await self.client.get_external_status() + self._sources_extend(inputs, SourceType.INPUT, add_to_list=True) + + apps = await self.client.get_app_list() + self._sources_extend(apps, SourceType.APP, sort_by="title") + + channels = await self.client.get_content_list_all("tv") + self._sources_extend(channels, SourceType.CHANNEL) + + async def async_source_start(self, uri: str, source_type: SourceType | str) -> None: + """Select source by uri.""" + if source_type == SourceType.APP: + await self.client.set_active_app(uri) + else: + await self.client.set_play_content(uri) + + async def async_source_find( + self, query: str, source_type: SourceType | str + ) -> None: + """Find and select source by query.""" + if query.startswith(("extInput:", "tv:", "com.sony.dtv.")): + return await self.async_source_start(query, source_type) + coarse_uri = None + is_numeric_search = source_type == SourceType.CHANNEL and query.isnumeric() + for uri, item in self.source_map.items(): + if item["type"] == source_type: + if is_numeric_search: + num = item.get("dispNum") + if num and int(query) == int(num): + return await self.async_source_start(uri, source_type) + else: + title: str = item["title"] + if query.lower() == title.lower(): + return await self.async_source_start(uri, source_type) + if query.lower() in title.lower(): + coarse_uri = uri + if coarse_uri: + return await self.async_source_start(coarse_uri, source_type) + raise ValueError(f"Not found {source_type}: {query}") + @catch_braviatv_errors async def async_turn_on(self) -> None: """Turn the device on.""" @@ -249,13 +278,11 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): async def async_media_play(self) -> None: """Send play command to device.""" await self.client.play() - self.playing = True @catch_braviatv_errors async def async_media_pause(self) -> None: """Send pause command to device.""" await self.client.pause() - self.playing = False @catch_braviatv_errors async def async_media_stop(self) -> None: @@ -265,7 +292,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): @catch_braviatv_errors async def async_media_next_track(self) -> None: """Send next track command.""" - if self.is_channel: + if self.media_content_type == MediaType.CHANNEL: await self.client.channel_up() else: await self.client.next_track() @@ -273,21 +300,24 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]): @catch_braviatv_errors async def async_media_previous_track(self) -> None: """Send previous track command.""" - if self.is_channel: + if self.media_content_type == MediaType.CHANNEL: await self.client.channel_down() else: await self.client.previous_track() + @catch_braviatv_errors + async def async_play_media( + self, media_type: MediaType | str, media_id: str, **kwargs: Any + ) -> None: + """Play a piece of media.""" + if media_type not in (MediaType.APP, MediaType.CHANNEL): + raise ValueError(f"Invalid media type: {media_type}") + await self.async_source_find(media_id, media_type) + @catch_braviatv_errors async def async_select_source(self, source: str) -> None: """Set the input source.""" - for uri, item in self.source_map.items(): - if item.get("title") == source: - if item.get("type") == "app": - await self.client.set_active_app(uri) - else: - await self.client.set_play_content(uri) - break + await self.async_source_find(source, SourceType.INPUT) @catch_braviatv_errors async def async_send_command(self, command: Iterable[str], repeats: int) -> None: diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index 83fe34fed28..107a00c9338 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -2,7 +2,7 @@ "domain": "braviatv", "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": ["pybravia==0.2.5"], + "requirements": ["pybravia==0.3.1"], "codeowners": ["@bieniu", "@Drafteed"], "ssdp": [ { diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 65a8e46946e..917bd1d5419 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -1,18 +1,23 @@ """Media player support for Bravia TV integration.""" from __future__ import annotations +from typing import Any + from homeassistant.components.media_player import ( + BrowseError, + MediaClass, MediaPlayerDeviceClass, MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, MediaType, ) +from homeassistant.components.media_player.browse_media import BrowseMedia from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN +from .const import DOMAIN, SourceType from .entity import BraviaTVEntity @@ -35,6 +40,7 @@ async def async_setup_entry( class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): """Representation of a Bravia TV Media Player.""" + _attr_assumed_state = True _attr_device_class = MediaPlayerDeviceClass.TV _attr_supported_features = ( MediaPlayerEntityFeature.PAUSE @@ -48,17 +54,15 @@ class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.BROWSE_MEDIA ) @property def state(self) -> MediaPlayerState: """Return the state of the device.""" if self.coordinator.is_on: - return ( - MediaPlayerState.PLAYING - if self.coordinator.playing - else MediaPlayerState.PAUSED - ) + return MediaPlayerState.ON return MediaPlayerState.OFF @property @@ -86,6 +90,11 @@ class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): """Title of current playing media.""" return self.coordinator.media_title + @property + def media_channel(self) -> str | None: + """Channel currently playing.""" + return self.coordinator.media_channel + @property def media_content_id(self) -> str | None: """Content ID of current playing media.""" @@ -125,6 +134,123 @@ class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): """Send mute command.""" await self.coordinator.async_volume_mute(mute) + async def async_browse_media( + self, + media_content_type: str | None = None, + media_content_id: str | None = None, + ) -> BrowseMedia: + """Browse apps and channels.""" + if not media_content_id: + await self.coordinator.async_update_sources() + return await self.async_browse_media_root() + + path = media_content_id.partition("/") + if path[0] == "apps": + return await self.async_browse_media_apps(True) + if path[0] == "channels": + return await self.async_browse_media_channels(True) + + raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}") + + async def async_browse_media_root(self) -> BrowseMedia: + """Return root media objects.""" + + return BrowseMedia( + title="Sony TV", + media_class=MediaClass.DIRECTORY, + media_content_id="", + media_content_type="", + can_play=False, + can_expand=True, + children=[ + await self.async_browse_media_apps(), + await self.async_browse_media_channels(), + ], + ) + + async def async_browse_media_apps(self, expanded: bool = False) -> BrowseMedia: + """Return apps media objects.""" + if expanded: + children = [ + BrowseMedia( + title=item["title"], + media_class=MediaClass.APP, + media_content_id=uri, + media_content_type=MediaType.APP, + can_play=False, + can_expand=False, + thumbnail=self.get_browse_image_url( + MediaType.APP, uri, media_image_id=None + ), + ) + for uri, item in self.coordinator.source_map.items() + if item["type"] == SourceType.APP + ] + else: + children = None + + return BrowseMedia( + title="Applications", + media_class=MediaClass.DIRECTORY, + media_content_id="apps", + media_content_type=MediaType.APPS, + children_media_class=MediaClass.APP, + can_play=False, + can_expand=True, + children=children, + ) + + async def async_browse_media_channels(self, expanded: bool = False) -> BrowseMedia: + """Return channels media objects.""" + if expanded: + children = [ + BrowseMedia( + title=item["title"], + media_class=MediaClass.CHANNEL, + media_content_id=uri, + media_content_type=MediaType.CHANNEL, + can_play=False, + can_expand=False, + ) + for uri, item in self.coordinator.source_map.items() + if item["type"] == SourceType.CHANNEL + ] + else: + children = None + + return BrowseMedia( + title="Channels", + media_class=MediaClass.DIRECTORY, + media_content_id="channels", + media_content_type=MediaType.CHANNELS, + children_media_class=MediaClass.CHANNEL, + can_play=False, + can_expand=True, + children=children, + ) + + 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]: + """Serve album art. Returns (content, content_type).""" + if media_content_type == MediaType.APP and media_content_id: + if icon := self.coordinator.source_map[media_content_id].get("icon"): + (content, content_type) = await self._async_fetch_image(icon) + if content_type: + # Fix invalid Content-Type header returned by Bravia + content_type = content_type.replace("Content-Type: ", "") + return (content, content_type) + return None, None + + async def async_play_media( + self, media_type: MediaType | str, media_id: str, **kwargs: Any + ) -> None: + """Play a piece of media.""" + await self.coordinator.async_play_media(media_type, media_id, **kwargs) + async def async_select_source(self, source: str) -> None: """Set the input source.""" await self.coordinator.async_select_source(source) @@ -137,6 +263,10 @@ class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity): """Send pause command.""" await self.coordinator.async_media_pause() + async def async_media_play_pause(self) -> None: + """Send pause command that toggle play/pause.""" + await self.coordinator.async_media_pause() + async def async_media_stop(self) -> None: """Send media stop command to media player.""" await self.coordinator.async_media_stop() diff --git a/homeassistant/components/braviatv/strings.json b/homeassistant/components/braviatv/strings.json index f40494f2251..d66f44acc6c 100644 --- a/homeassistant/components/braviatv/strings.json +++ b/homeassistant/components/braviatv/strings.json @@ -44,18 +44,5 @@ "not_bravia_device": "The device is not a Bravia TV.", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } - }, - "options": { - "step": { - "user": { - "title": "Options for Sony Bravia TV", - "data": { - "ignored_sources": "List of ignored sources" - } - } - }, - "abort": { - "failed_update": "An error occurred while updating the list of sources.\n\n Ensure that your TV is turned on before trying to set it up." - } } } diff --git a/homeassistant/components/braviatv/translations/bg.json b/homeassistant/components/braviatv/translations/bg.json index 3eedf02caab..208a3f92df7 100644 --- a/homeassistant/components/braviatv/translations/bg.json +++ b/homeassistant/components/braviatv/translations/bg.json @@ -3,8 +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", "not_bravia_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Bravia.", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", - "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u0438 \u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", @@ -15,17 +14,20 @@ "step": { "authorize": { "data": { - "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", "use_psk": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 PSK \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" } }, "confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u0437\u0430\u043f\u043e\u0447\u043d\u0435\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0442\u0430?" }, - "reauth_confirm": { + "pin": { "data": { - "pin": "\u041f\u0418\u041d \u043a\u043e\u0434", - "use_psk": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 PSK \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" + "pin": "\u041f\u0418\u041d \u043a\u043e\u0434" + } + }, + "psk": { + "data": { + "pin": "PSK" } }, "user": { diff --git a/homeassistant/components/braviatv/translations/ca.json b/homeassistant/components/braviatv/translations/ca.json index 2b15a496af0..42843bd75ae 100644 --- a/homeassistant/components/braviatv/translations/ca.json +++ b/homeassistant/components/braviatv/translations/ca.json @@ -4,8 +4,7 @@ "already_configured": "El dispositiu ja est\u00e0 configurat", "no_ip_control": "El control IP del teu televisor est\u00e0 desactivat o aquest no \u00e9s compatible.", "not_bravia_device": "El dispositiu no \u00e9s un televisor Bravia.", - "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", - "reauth_unsuccessful": "La re-autenticaci\u00f3 no ha tingut \u00e8xit, elimina la integraci\u00f3 i torna-la a configurar." + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", @@ -16,21 +15,27 @@ "step": { "authorize": { "data": { - "pin": "Codi PIN", "use_psk": "Utilitza autenticaci\u00f3 PSK" }, - "description": "Introdueix el codi PIN que es mostra al televisor Sony Bravia.\n\nSi no es mostra el codi, has d'eliminar Home Assistant del teu televisor. V\u00e9s a: Configuraci\u00f3 -> Xarxa -> Configuraci\u00f3 de dispositiu remot -> Elimina dispositiu remot.\n\nPots utilitzar una clau PSK (Pre-Shared-Key) enlloc d'un codi PIN. La clau PSK est\u00e0 definida per l'usuari i s'utilitza per al control d'acc\u00e9s. Es recomana aquest m\u00e8tode d'autenticaci\u00f3, ja que \u00e9s m\u00e9s estable. Per activar la clau PSK, v\u00e9s a: Configuraci\u00f3 -> Xarxa -> Configuraci\u00f3 de xarxa local -> Control IP. Tot seguit, marca la casella \u00abUtilitza autenticaci\u00f3 PSK\u00bb i introdueix la clau que desitgis enlloc del PIN.", + "description": "Assegureu-vos que teniu habilitada l'opci\u00f3 \u00abControl Remot\u00bb al vostre aparell de TV; per fer-ho aneu a: Configuraci\u00f3 -> Xarxa -> Configuraci\u00f3 de dispositiu remot -> Control remot.\n\nHi ha dos m\u00e8todes d'autenticaci\u00f3: Codi PIN o PSK (Pre-Shared-Key). L'autorizaci\u00f3 via PSK \u00e9s la recomanada perqu\u00e8 \u00e9s m\u00e9s estable.", "title": "Autoritzaci\u00f3 del televisor Sony Bravia" }, "confirm": { "description": "Vols comen\u00e7ar la configuraci\u00f3?" }, - "reauth_confirm": { + "pin": { "data": { - "pin": "Codi PIN", - "use_psk": "Utilitza autenticaci\u00f3 PSK" + "pin": "Codi PIN" }, - "description": "Introdueix el codi PIN que es mostra al televisor Sony Bravia.\n\nSi no es mostra el codi, has d'eliminar Home Assistant del teu televisor. V\u00e9s a: Configuraci\u00f3 -> Xarxa -> Configuraci\u00f3 de dispositiu remot -> Elimina dispositiu remot.\n\nPots utilitzar una clau PSK (Pre-Shared-Key) enlloc d'un codi PIN. La clau PSK est\u00e0 definida per l'usuari i s'utilitza per al control d'acc\u00e9s. Es recomana aquest m\u00e8tode d'autenticaci\u00f3, ja que \u00e9s m\u00e9s estable. Per activar la clau PSK, v\u00e9s a: Configuraci\u00f3 -> Xarxa -> Configuraci\u00f3 de xarxa local -> Control IP. Tot seguit, marca la casella \u00abUtilitza autenticaci\u00f3 PSK\u00bb i introdueix la clau que desitgis enlloc del PIN." + "description": "Introdu\u00efu el codi PIN que es mostra a la TV Sony Bravia.\n\nSi el codi PIN no es mostra, haureu d'eliminar el registre del Home Assistant del vostre aparell de TV; aneu a: Configuraci\u00f3 -> Xarxa -> Par\u00e0metres del dispositiu remot -> Elimina el registre del dispositiu remot.", + "title": "Autoritza la TV Sony Bravia" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "Per tal d'establir la PSK a la vostra TV, aneu a: Configuraci\u00f3 -> Xarxa -> Configuraci\u00f3 de la Xarxa Local -> Control de la IP. Establiu l'\u00abAutenticaci\u00f3\u00bb a \u00abNormal i Clau Pre-Compartida\u00ab o b\u00e9 \u00abClau Pre-Compartida\u00bb i definiu la vostra Clau (p.ex.: sony)", + "title": "Autoritza la TV Sony Bravia" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/cs.json b/homeassistant/components/braviatv/translations/cs.json index 583ad34efac..43dbe9a1ddc 100644 --- a/homeassistant/components/braviatv/translations/cs.json +++ b/homeassistant/components/braviatv/translations/cs.json @@ -12,9 +12,6 @@ }, "step": { "authorize": { - "data": { - "pin": "PIN k\u00f3d" - }, "description": "Zadejte PIN k\u00f3d zobrazen\u00fd na televizi Sony Bravia.\n\nPokud se PIN k\u00f3d nezobraz\u00ed, je t\u0159eba zru\u0161it registraci Home Assistant na televizi, p\u0159ejd\u011bte na: Nastaven\u00ed -> S\u00ed\u0165 -> Nastaven\u00ed vzd\u00e1len\u00e9ho za\u0159\u00edzen\u00ed -> Zru\u0161it registraci vzd\u00e1len\u00e9ho za\u0159\u00edzen\u00ed.", "title": "Autorizujte televizi Sony Bravia" }, diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json index 8f376cdce9e..753de7f9770 100644 --- a/homeassistant/components/braviatv/translations/de.json +++ b/homeassistant/components/braviatv/translations/de.json @@ -4,8 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "no_ip_control": "IP-Steuerung ist auf deinen Fernseher deaktiviert oder der Fernseher wird nicht unterst\u00fctzt.", "not_bravia_device": "Das Ger\u00e4t ist kein Bravia-Fernseher.", - "reauth_successful": "Die erneute Authentifizierung war erfolgreich", - "reauth_unsuccessful": "Die erneute Authentifizierung war nicht erfolgreich. Bitte entferne die Integration und richte sie erneut ein." + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", @@ -16,21 +15,27 @@ "step": { "authorize": { "data": { - "pin": "PIN-Code", "use_psk": "PSK-Authentifizierung verwenden" }, - "description": "Gib den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, musst du die Registrierung von Home Assistant auf deinem Fernseher aufheben, gehe zu: Einstellungen \u2192 Netzwerk \u2192 Remote-Ger\u00e4teeinstellungen \u2192 Remote-Ger\u00e4t abmelden. \n\nDu kannst PSK (Pre-Shared-Key) anstelle der PIN verwenden. PSK ist ein benutzerdefinierter geheimer Schl\u00fcssel, der f\u00fcr die Zugriffskontrolle verwendet wird. Diese Authentifizierungsmethode wird als stabiler empfohlen. Um PSK auf deinem Fernseher zu aktivieren, gehe zu: Einstellungen \u2192 Netzwerk \u2192 Heimnetzwerk-Setup \u2192 IP-Steuerung. Aktiviere dann das Kontrollk\u00e4stchen \u00abPSK-Authentifizierung verwenden\u00bb und gib deinen PSK anstelle der PIN ein.", + "description": "Vergewissere dich, dass \"Fernsteuerung\" auf deinem Fernsehger\u00e4t aktiviert ist, gehe zu: \nEinstellungen -> Netzwerk -> Einstellungen f\u00fcr Fernbedienungsger\u00e4te -> Fernsteuerung. \n\nEs gibt zwei Autorisierungsmethoden: PIN-Code oder PSK (Pre-Shared Key). \nDie Autorisierung \u00fcber PSK wird empfohlen, da sie stabiler ist.", "title": "Autorisiere Sony Bravia TV" }, "confirm": { "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" }, - "reauth_confirm": { + "pin": { "data": { - "pin": "PIN-Code", - "use_psk": "PSK-Authentifizierung verwenden" + "pin": "PIN-Code" }, - "description": "Gib den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, musst du die Registrierung von Home Assistant auf deinem Fernseher aufheben, gehe zu: Einstellungen \u2192 Netzwerk \u2192 Remote-Ger\u00e4teeinstellungen \u2192 Remote-Ger\u00e4t abmelden. \n\nDu kannst PSK (Pre-Shared-Key) anstelle der PIN verwenden. PSK ist ein benutzerdefinierter geheimer Schl\u00fcssel, der f\u00fcr die Zugriffskontrolle verwendet wird. Diese Authentifizierungsmethode wird als stabiler empfohlen. Um PSK auf deinem Fernseher zu aktivieren, gehe zu: Einstellungen \u2192 Netzwerk \u2192 Heimnetzwerk-Setup \u2192 IP-Steuerung. Aktiviere dann das Kontrollk\u00e4stchen \u00abPSK-Authentifizierung verwenden\u00bb und gib deinen PSK anstelle der PIN ein." + "description": "Gib den auf dem Sony Bravia TV angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, musst du die Registrierung des Home Assistant auf deinem Fernsehger\u00e4t aufheben, indem du zu Einstellungen -> Netzwerk -> Einstellungen f\u00fcr Fernbedienungsger\u00e4te -> Deregistrierung des Fernbedienungsger\u00e4ts gehst.", + "title": "Sony Bravia TV autorisieren" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "Um PSK auf deinem Fernseher einzurichten, gehe zu: Einstellungen -> Netzwerk -> Heimnetzwerk-Setup -> IP-Steuerung. Stelle \"Authentifizierung\" auf \"Normal und Pre-Shared Key\" oder \"Pre-Shared Key\" und definiere deine Pre-Shared-Key-Zeichenfolge (z. B. sony). \n\nGib dann hier deinen PSK ein.", + "title": "Sony Bravia TV autorisieren" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/el.json b/homeassistant/components/braviatv/translations/el.json index 98ea6682eec..941d89fa314 100644 --- a/homeassistant/components/braviatv/translations/el.json +++ b/homeassistant/components/braviatv/translations/el.json @@ -4,8 +4,7 @@ "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.", "not_bravia_device": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7 Bravia.", - "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." + "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", @@ -16,27 +15,33 @@ "step": { "authorize": { "data": { - "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", "use_psk": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 PSK" }, - "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.", + "description": "\u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03bf \u00ab\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03b1\u03c0\u03cc \u03b1\u03c0\u03cc\u03c3\u03c4\u03b1\u03c3\u03b7\u00bb \u03b5\u03af\u03bd\u03b1\u03b9 \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, \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7:\n \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 - > \u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03b1\u03c0\u03cc \u03b1\u03c0\u03cc\u03c3\u03c4\u03b1\u03c3\u03b7. \n\n \u03a5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03b4\u03cd\u03bf \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf\u03b9 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2: \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03ae PSK (Pre-Shared Key).\n \u0397 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 \u03bc\u03ad\u03c3\u03c9 PSK \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9 \u03c9\u03c2 \u03c0\u03b9\u03bf \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae.", "title": "\u0395\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 Sony Bravia TV" }, "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;" }, - "reauth_confirm": { + "pin": { "data": { - "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN", - "use_psk": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 PSK" + "pin": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN" }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\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 \u03b4\u03b5\u03bd \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN, \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\u03bf: \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 - > \u039a\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2. \n\n \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 PSK (Pre-Shared-Key) \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 PIN. \u03a4\u03bf PSK \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1 \u03bc\u03c5\u03c3\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c0\u03bf\u03c5 \u03bf\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2. \u0391\u03c5\u03c4\u03ae \u03b7 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9 \u03c9\u03c2 \u03c0\u03b9\u03bf \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf PSK \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\u03b9\u03c2 \u03b5\u03be\u03ae\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2: \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 - > \u0394\u03af\u03ba\u03c4\u03c5\u03bf - > \u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03bf\u03b9\u03ba\u03b9\u03b1\u03ba\u03bf\u03cd \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 - > \u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 IP. \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03c0\u03bb\u03b1\u03af\u03c3\u03b9\u03bf \u00ab\u03a7\u03c1\u03ae\u03c3\u03b7 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 PSK\u00bb \u03ba\u03b1\u03b9 \u03c0\u03bb\u03b7\u03ba\u03c4\u03c1\u03bf\u03bb\u03bf\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf PSK \u03c3\u03b1\u03c2 \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03c4\u03bf PIN." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\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 \u03b4\u03b5\u03bd \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN, \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\u03bf: \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 - > \u039a\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\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" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf PSK \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\u03b9\u03c2 \u03b5\u03be\u03ae\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2: \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 - > \u0394\u03af\u03ba\u03c4\u03c5\u03bf - > \u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03bf\u03b9\u03ba\u03b9\u03b1\u03ba\u03bf\u03cd \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 - > \u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 IP. \u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf \u00abAuthentication\u00bb \u03c3\u03b5 \u00abNormal and Pre-Shared Key\u00bb \u03ae \u00abPre-Shared Key\u00bb \u03ba\u03b1\u03b9 \u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03c3\u03b1\u03c2 Pre-Shared-Key (\u03c0.\u03c7. sony). \n\n \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf PSK \u03c3\u03b1\u03c2 \u03b5\u03b4\u03ce.", + "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." + "description": "\u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03c0\u03c1\u03b9\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03ae\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5." } } }, diff --git a/homeassistant/components/braviatv/translations/es.json b/homeassistant/components/braviatv/translations/es.json index c467c5dafca..c69c3611d4d 100644 --- a/homeassistant/components/braviatv/translations/es.json +++ b/homeassistant/components/braviatv/translations/es.json @@ -4,8 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "no_ip_control": "El Control IP est\u00e1 desactivado en tu TV o la TV no es compatible.", "not_bravia_device": "El dispositivo no es una TV Bravia.", - "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", - "reauth_unsuccessful": "No se pudo volver a autenticar, por favor, elimina la integraci\u00f3n y vuelve a configurarla." + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { "cannot_connect": "No se pudo conectar", @@ -16,21 +15,27 @@ "step": { "authorize": { "data": { - "pin": "C\u00f3digo PIN", "use_psk": "Usar autenticaci\u00f3n PSK" }, - "description": "Introduce el c\u00f3digo PIN que se muestra en la TV Sony Bravia. \n\nSi no se muestra el c\u00f3digo PIN, debes cancelar el registro de Home Assistant en tu TV, ve a: Configuraci\u00f3n - > Red - > Configuraci\u00f3n del dispositivo remoto - > Cancelar el registro del dispositivo remoto. \n\nPuedes usar PSK (clave precompartida) en lugar de PIN. PSK es una clave secreta definida por el usuario que se utiliza para el control de acceso. Este m\u00e9todo de autenticaci\u00f3n se recomienda como m\u00e1s estable. Para habilitar PSK en tu TV, ve a: Configuraci\u00f3n - > Red - > Configuraci\u00f3n de red dom\u00e9stica - > Control de IP. Luego marca la casilla \u00abUsar autenticaci\u00f3n PSK\u00bb e introduce tu PSK en lugar de PIN.", + "description": "Aseg\u00farate de que \u00abControlar de forma remota\u00bb est\u00e9 habilitado en tu televisor, ve a:\nConfiguraci\u00f3n -> Red -> Configuraci\u00f3n de dispositivo remoto -> Controlar de forma remota. \n\nHay dos m\u00e9todos de autorizaci\u00f3n: c\u00f3digo PIN o PSK (clave precompartida).\nSe recomienda la autorizaci\u00f3n a trav\u00e9s de PSK ya que es m\u00e1s estable.", "title": "Autorizar Sony Bravia TV" }, "confirm": { "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" }, - "reauth_confirm": { + "pin": { "data": { - "pin": "C\u00f3digo PIN", - "use_psk": "Usar autenticaci\u00f3n PSK" + "pin": "C\u00f3digo PIN" }, - "description": "Introduce el c\u00f3digo PIN que se muestra en la TV Sony Bravia. \n\nSi no se muestra el c\u00f3digo PIN, debes cancelar el registro de Home Assistant en tu TV, ve a: Configuraci\u00f3n -> Red -> Configuraci\u00f3n del dispositivo remoto -> Cancelar el registro del dispositivo remoto. \n\nPuedes usar PSK (clave precompartida) en lugar de PIN. PSK es una clave secreta definida por el usuario que se utiliza para el control de acceso. Este m\u00e9todo de autenticaci\u00f3n se recomienda como m\u00e1s estable. Para habilitar PSK en tu TV, ve a: Configuraci\u00f3n -> Red -> Configuraci\u00f3n de red dom\u00e9stica -> Control de IP. Luego marca la casilla \u00abUsar autenticaci\u00f3n PSK\u00bb e introduce tu PSK en lugar de PIN." + "description": "Introduce el c\u00f3digo PIN que se muestra en Sony Bravia TV. \n\nSi no se muestra el c\u00f3digo PIN, debes cancelar el registro de Home Assistant en tu TV, ve a: Configuraci\u00f3n - > Red - > Configuraci\u00f3n del dispositivo remoto - > Cancelar el registro del dispositivo remoto.", + "title": "Autorizar Sony Bravia TV" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "Para configurar PSK en tu televisor, ve a: Configuraci\u00f3n -> Red -> Configuraci\u00f3n de red dom\u00e9stica -> Control de IP. Establece \u00abAutenticaci\u00f3n\u00bb en \u00abClave normal y precompartida\u00bb o \u00abClave precompartida\u00bb y define tu cadena de clave precompartida (p. ej., sony). \nA continuaci\u00f3n introduce tu PSK aqu\u00ed.", + "title": "Autorizar Sony Bravia TV" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/et.json b/homeassistant/components/braviatv/translations/et.json index c650b6abd9f..d78c180664b 100644 --- a/homeassistant/components/braviatv/translations/et.json +++ b/homeassistant/components/braviatv/translations/et.json @@ -4,8 +4,7 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "no_ip_control": "Teleris on IP-juhtimine keelatud v\u00f5i telerit ei toetata.", "not_bravia_device": "Seade ei ole Bravia teler.", - "reauth_successful": "Taastuvastamine \u00f5nnestus", - "reauth_unsuccessful": "Taasautentimine eba\u00f5nnestus, eemalda sidumine ja seadista see uuesti." + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", @@ -16,21 +15,27 @@ "step": { "authorize": { "data": { - "pin": "PIN kood", "use_psk": "PSK autentimise kasutamine" }, - "description": "Sisestage Sony Bravia teleril n\u00e4idatud PIN-kood. \n\nKui PIN-koodi ei kuvata, peate teleril Home Assistant'i registreerimise t\u00fchistama, minge aadressile: Seaded -> Network -> Remote device settings -> Deregister remote device. \n\nPIN-koodi asemel v\u00f5ite kasutada PSK (Pre-Shared-Key). PSK on kasutaja m\u00e4\u00e4ratud salajane v\u00f5ti, mida kasutatakse juurdep\u00e4\u00e4su kontrollimiseks. See autentimismeetod on soovitatav kui stabiilsem. PSK lubamiseks teleril minge aadressil: Settings -> Network -> Home Network Setup -> IP Control. Seej\u00e4rel m\u00e4rgistage ruut \"Kasutage PSK autentimist\" ja sisestage PIN-koodi asemel PSK.", + "description": "Veendu, et \"Kaugjuhtimine\" on teleril lubatud, mine aadressil: \nSeaded -> Network -> Remote device settings -> Control remotely. \n\nOn kaks autoriseerimismeetodit: PIN-kood v\u00f5i PSK (Pre-Shared Key). \nAutoriseerimine PSK kaudu on soovitatav kui stabiilsem.", "title": "Sony Bravia TV autoriseerimine" }, "confirm": { "description": "Kas alustada seadistamist?" }, - "reauth_confirm": { + "pin": { "data": { - "pin": "PIN kood", - "use_psk": "PSK autentimise kasutamine" + "pin": "PIN kood" }, - "description": "Sisesta Sony Bravia teleril n\u00e4idatud PIN-kood. \n\nKui PIN-koodi ei kuvata, peadeleril Home Assistant'i registreerimise t\u00fchistama, mine aadressile: Seaded -> Network -> Remote device settings -> Deregister remote device. \n\nPIN-koodi asemel v\u00f5id kasutada PSK (Pre-Shared-Key). PSK on kasutaja m\u00e4\u00e4ratud salajane v\u00f5ti, mida kasutatakse juurdep\u00e4\u00e4su kontrollimiseks. See autentimismeetod on soovitatav kui stabiilsem. PSK lubamiseks teleril mine aadressil: Settings -> Network -> Home Network Setup -> IP Control. Seej\u00e4rel m\u00e4rgista ruut \"Kasutage PSK autentimist\" ja sisesta PIN-koodi asemel PSK." + "description": "Sisesta Sony Bravia teleris kuvatav PIN-kood. \n\n Kui PIN-koodi ei kuvata, pead oma teleris Home Assistanti registreerimise t\u00fchistama. Avag: Seaded - > V\u00f5rk - > Kaugseadme seaded - > T\u00fchista kaugseadme registreerimine.", + "title": "Sony Bravia TV autoriseerimine" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "PSK seadistamiseks teleris ava: Seaded - > V\u00f5rk - > Koduv\u00f5rgu h\u00e4\u00e4lestus - > IP-juhtimine. M\u00e4\u00e4ra \"Authentication\" v\u00e4\u00e4rtuseks \"Tavaline ja eeljagatud v\u00f5ti\" v\u00f5i \"Eeljagatud v\u00f5ti\" ja m\u00e4\u00e4ra oma eeljagatud v\u00f5tme string (nt sony). \n\n Seej\u00e4rel sisesta siia oma PSK.", + "title": "Sony Bravia TV autoriseerimine" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/fr.json b/homeassistant/components/braviatv/translations/fr.json index d71bbd0e8ac..ff53465afe9 100644 --- a/homeassistant/components/braviatv/translations/fr.json +++ b/homeassistant/components/braviatv/translations/fr.json @@ -4,8 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "no_ip_control": "Le contr\u00f4le IP est d\u00e9sactiv\u00e9 sur votre t\u00e9l\u00e9viseur ou le t\u00e9l\u00e9viseur n'est pas pris en charge.", "not_bravia_device": "L'appareil n'est pas un t\u00e9l\u00e9viseur Bravia.", - "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", - "reauth_unsuccessful": "La r\u00e9authentification a \u00e9chou\u00e9, veuillez supprimer l'int\u00e9gration puis la configurer \u00e0 nouveau." + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { "cannot_connect": "\u00c9chec de connexion", @@ -16,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "Code PIN", "use_psk": "Utiliser l'authentification PSK" }, "description": "Saisissez le code PIN affich\u00e9 sur le t\u00e9l\u00e9viseur Sony Bravia. \n\nSi le code PIN n'est pas affich\u00e9, vous devez d\u00e9senregistrer Home Assistant de votre t\u00e9l\u00e9viseur, allez dans: Param\u00e8tres - > R\u00e9seau - > Param\u00e8tres de l'appareil distant - > Annuler l'enregistrement de l'appareil distant.", @@ -25,13 +23,6 @@ "confirm": { "description": "Voulez-vous commencer la configuration\u00a0?" }, - "reauth_confirm": { - "data": { - "pin": "Code PIN", - "use_psk": "Utiliser l'authentification PSK" - }, - "description": "Saisissez le code PIN affich\u00e9 sur le t\u00e9l\u00e9viseur Sony Bravia. \n\nSi le code PIN n'est pas affich\u00e9, vous devez supprimer Home Assistant du t\u00e9l\u00e9viseur. Pour cela, allez dans : Param\u00e8tres -> R\u00e9seau -> Param\u00e8tres du p\u00e9riph\u00e9rique distant ->Supprimer le p\u00e9riph\u00e9rique distant. \n\nVous pouvez utiliser PSK (Pre-Shared-Key) au lieu du code PIN. PSK est une cl\u00e9 secr\u00e8te d\u00e9finie par l'utilisateur utilis\u00e9e pour le contr\u00f4le d'acc\u00e8s. Cette m\u00e9thode d'authentification est recommand\u00e9e car elle est plus stable. Pour activer PSK sur votre t\u00e9l\u00e9viseur, allez dans : Param\u00e8tres -> R\u00e9seau -> Configuration du r\u00e9seau domestique -> Contr\u00f4le IP. Cochez ensuite la case \"Utiliser l'authentification PSK\" et entrez votre PSK au lieu du code PIN." - }, "user": { "data": { "host": "H\u00f4te" diff --git a/homeassistant/components/braviatv/translations/he.json b/homeassistant/components/braviatv/translations/he.json index 29c90cda769..2539c0ba8fc 100644 --- a/homeassistant/components/braviatv/translations/he.json +++ b/homeassistant/components/braviatv/translations/he.json @@ -10,15 +10,10 @@ "invalid_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd" }, "step": { - "authorize": { - "data": { - "pin": "\u05e7\u05d5\u05d3 PIN" - } - }, "confirm": { "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" }, - "reauth_confirm": { + "pin": { "data": { "pin": "\u05e7\u05d5\u05d3 PIN" } diff --git a/homeassistant/components/braviatv/translations/hu.json b/homeassistant/components/braviatv/translations/hu.json index 97ee61c4b53..cd4648a5251 100644 --- a/homeassistant/components/braviatv/translations/hu.json +++ b/homeassistant/components/braviatv/translations/hu.json @@ -4,8 +4,7 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "no_ip_control": "Az IP-vez\u00e9rl\u00e9s le van tiltva a TV-n, vagy a TV nem t\u00e1mogatja.", "not_bravia_device": "A k\u00e9sz\u00fcl\u00e9k nem egy Bravia TV.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", - "reauth_unsuccessful": "Az \u00fajrahiteles\u00edt\u00e9s sikertelen volt, k\u00e9rem, t\u00e1vol\u00edtsa el az integr\u00e1ci\u00f3t, \u00e9s \u00e1ll\u00edtsa be \u00fajra." + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", @@ -16,21 +15,25 @@ "step": { "authorize": { "data": { - "pin": "PIN-k\u00f3d", "use_psk": "PSK hiteles\u00edt\u00e9s haszn\u00e1lata" }, - "description": "\u00cdrja be a Sony Bravia TV -n l\u00e1that\u00f3 PIN -k\u00f3dot. \n\nHa a PIN -k\u00f3d nem jelenik meg, t\u00f6r\u00f6lje a Home Assistant regisztr\u00e1ci\u00f3j\u00e1t a t\u00e9v\u00e9n, az al\u00e1bbiak szerint: Be\u00e1ll\u00edt\u00e1sok - > H\u00e1l\u00f3zat - > T\u00e1voli eszk\u00f6z be\u00e1ll\u00edt\u00e1sai - > T\u00e1vol\u00edtsa el a t\u00e1voli eszk\u00f6z regisztr\u00e1ci\u00f3j\u00e1t.\n\nA PIN-k\u00f3d helyett haszn\u00e1lhat PSK-t (Pre-Shared-Key). A PSK egy felhaszn\u00e1l\u00f3 \u00e1ltal meghat\u00e1rozott titkos kulcs, amelyet a hozz\u00e1f\u00e9r\u00e9s ellen\u0151rz\u00e9s\u00e9re haszn\u00e1lnak. Ez a hiteles\u00edt\u00e9si m\u00f3dszer aj\u00e1nlott, mivel stabilabb. A PSK enged\u00e9lyez\u00e9s\u00e9hez a TV-n, l\u00e9pjen a k\u00f6vetkez\u0151 oldalra: Be\u00e1ll\u00edt\u00e1sok -> H\u00e1l\u00f3zat -> Otthoni h\u00e1l\u00f3zat be\u00e1ll\u00edt\u00e1sa -> IP-vez\u00e9rl\u00e9s. Ezut\u00e1n jel\u00f6lje be a \"PSK hiteles\u00edt\u00e9s haszn\u00e1lata\" jel\u00f6l\u0151n\u00e9gyzetet, \u00e9s adja meg a PSK-t a PIN-k\u00f3d helyett.", + "description": "Gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy a \"T\u00e1voli vez\u00e9rl\u00e9s\" enged\u00e9lyezve van a TV-n, n\u00e9zze meg itt: \nBe\u00e1ll\u00edt\u00e1sok -> H\u00e1l\u00f3zat -> T\u00e1voli eszk\u00f6zbe\u00e1ll\u00edt\u00e1sok -> T\u00e1voli vez\u00e9rl\u00e9s\n(Settings -> Network -> Remote device settings -> Control remotely)\n\nK\u00e9t enged\u00e9lyez\u00e9si m\u00f3dszer l\u00e9tezik: PIN-k\u00f3d vagy PSK (Pre-Shared Key). \nA PSK-n kereszt\u00fcli enged\u00e9lyez\u00e9s aj\u00e1nlott, mivel stabilabb.", "title": "Sony Bravia TV enged\u00e9lyez\u00e9se" }, "confirm": { "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" }, - "reauth_confirm": { + "pin": { "data": { - "pin": "PIN-k\u00f3d", - "use_psk": "PSK hiteles\u00edt\u00e9s haszn\u00e1lata" + "pin": "PIN-k\u00f3d" }, - "description": "\u00cdrja be a Sony Bravia TV -n l\u00e1that\u00f3 PIN -k\u00f3dot. \n\nHa a PIN -k\u00f3d nem jelenik meg, t\u00f6r\u00f6lje a Home Assistant regisztr\u00e1ci\u00f3j\u00e1t a t\u00e9v\u00e9n, az al\u00e1bbiak szerint: Be\u00e1ll\u00edt\u00e1sok - > H\u00e1l\u00f3zat - > T\u00e1voli eszk\u00f6z be\u00e1ll\u00edt\u00e1sai - > T\u00e1vol\u00edtsa el a t\u00e1voli eszk\u00f6z regisztr\u00e1ci\u00f3j\u00e1t.\n\nA PIN-k\u00f3d helyett haszn\u00e1lhat PSK-t (Pre-Shared-Key). A PSK egy felhaszn\u00e1l\u00f3 \u00e1ltal meghat\u00e1rozott titkos kulcs, amelyet a hozz\u00e1f\u00e9r\u00e9s ellen\u0151rz\u00e9s\u00e9re haszn\u00e1lnak. Ez a hiteles\u00edt\u00e9si m\u00f3dszer aj\u00e1nlott, mivel stabilabb. A PSK enged\u00e9lyez\u00e9s\u00e9hez a TV-n, l\u00e9pjen a k\u00f6vetkez\u0151 oldalra: Be\u00e1ll\u00edt\u00e1sok -> H\u00e1l\u00f3zat -> Otthoni h\u00e1l\u00f3zat be\u00e1ll\u00edt\u00e1sa -> IP-vez\u00e9rl\u00e9s. Ezut\u00e1n jel\u00f6lje be a \"PSK hiteles\u00edt\u00e9s haszn\u00e1lata\" jel\u00f6l\u0151n\u00e9gyzetet, \u00e9s adja meg a PSK-t a PIN-k\u00f3d helyett." + "title": "Sony Bravia TV enged\u00e9lyez\u00e9se" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "title": "Sony Bravia TV enged\u00e9lyez\u00e9se" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/id.json b/homeassistant/components/braviatv/translations/id.json index 6c357e6e2bb..75f6ed63008 100644 --- a/homeassistant/components/braviatv/translations/id.json +++ b/homeassistant/components/braviatv/translations/id.json @@ -4,8 +4,7 @@ "already_configured": "Perangkat sudah dikonfigurasi", "no_ip_control": "Kontrol IP dinonaktifkan di TV Anda atau TV tidak didukung.", "not_bravia_device": "Perangkat ini bukan TV Bravia.", - "reauth_successful": "Autentikasi ulang berhasil", - "reauth_unsuccessful": "Autentikasi ulang tidak berhasil, hapus integrasi dan siapkan kembali." + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { "cannot_connect": "Gagal terhubung", @@ -16,21 +15,27 @@ "step": { "authorize": { "data": { - "pin": "Kode PIN", "use_psk": "Gunakan autentikasi PSK" }, - "description": "Masukkan kode PIN yang ditampilkan di TV Sony Bravia.\n\nJika kode PIN tidak ditampilkan, Anda harus membatalkan pendaftaran Home Assistant di TV, buka: Pengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Batalkan pendaftaran perangkat jarak jauh.\n\nAnda bisa menggunakan PSK (Pre-Shared-Key) alih-alih menggunakan PIN. PSK merupakan kunci rahasia yang ditentukan pengguna untuk mengakses kontrol. Metode autentikasi ini disarankan karena lebih stabil. Untuk mengaktifkan PSK di TV Anda, buka Pengaturan -> Jaringan -> Penyiapan Jaringan Rumah -> Kontrol IP, lalu centang \u00abGunakan autentikasi PSK\u00bb dan masukkan PSK Anda, bukan PIN.", + "description": "Pastikan bahwa \u00abKontrol dari jarak jauh\u00bb diaktifkan di TV Anda, buka: \nPengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Kontrol dari jarak jauh. \n\nAda dua metode otorisasi: Kode PIN atau PSK (Pre-Shared Key). \nOtorisasi melalui PSK direkomendasikan karena lebih stabil.", "title": "Otorisasi TV Sony Bravia" }, "confirm": { "description": "Ingin memulai penyiapan?" }, - "reauth_confirm": { + "pin": { "data": { - "pin": "Kode PIN", - "use_psk": "Gunakan autentikasi PSK" + "pin": "Kode PIN" }, - "description": "Masukkan kode PIN yang ditampilkan di TV Sony Bravia.\n\nJika kode PIN tidak ditampilkan, Anda harus membatalkan pendaftaran Home Assistant di TV, buka: Pengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Batalkan pendaftaran perangkat jarak jauh.\n\nAnda bisa menggunakan PSK (Pre-Shared-Key) alih-alih menggunakan PIN. PSK merupakan kunci rahasia yang ditentukan pengguna untuk mengakses kontrol. Metode autentikasi ini disarankan karena lebih stabil. Untuk mengaktifkan PSK di TV Anda, buka Pengaturan -> Jaringan -> Penyiapan Jaringan Rumah -> Kontrol IP, lalu centang \u00abGunakan autentikasi PSK\u00bb dan masukkan PSK Anda, bukan PIN." + "description": "Masukkan kode PIN yang ditampilkan di TV Sony Bravia. \n\nJika kode PIN tidak ditampilkan, Anda harus membatalkan pendaftaran Home Assistant di TV, buka: Pengaturan -> Jaringan -> Pengaturan perangkat jarak jauh -> Batalkan pendaftaran perangkat jarak jauh.", + "title": "Otorisasi TV Sony Bravia" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "Untuk mengatur PSK di TV Anda, buka: Pengaturan -> Jaringan -> Pengaturan Jaringan Rumah -> Kontrol IP. Atur \u00abAutentikasi\u00bb ke \u00abNormal dan Pre-Shared Key\u00bb atau \"Pre-Shared Key\" dan tentukan string Pre-Shared-Key Anda (misalnya, sony). \n\nKemudian masukkan PSK Anda di sini.", + "title": "Otorisasi TV Sony Bravia" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/it.json b/homeassistant/components/braviatv/translations/it.json index 23d61c324b7..983ef4dda99 100644 --- a/homeassistant/components/braviatv/translations/it.json +++ b/homeassistant/components/braviatv/translations/it.json @@ -4,8 +4,7 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "no_ip_control": "Il controllo IP \u00e8 disabilitato sulla TV o la TV non \u00e8 supportata.", "not_bravia_device": "Il dispositivo non \u00e8 una TV Bravia.", - "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", - "reauth_unsuccessful": "La nuova autenticazione non ha avuto esito positivo, rimuovere l'integrazione e configurarla di nuovo." + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { "cannot_connect": "Impossibile connettersi", @@ -16,21 +15,27 @@ "step": { "authorize": { "data": { - "pin": "Codice PIN", "use_psk": "Usa l'autenticazione PSK" }, - "description": "Inserisci il codice PIN mostrato sul Sony Bravia TV. \n\nSe il codice PIN non viene visualizzato, devi annullare la registrazione di Home Assistant sulla TV, vai su: Impostazioni -> Rete -> Impostazioni dispositivo remoto -> Annulla registrazione dispositivo remoto. \n\nPuoi usare PSK (Pre-Shared-Key) invece del PIN. PSK \u00e8 una chiave segreta definita dall'utente utilizzata per il controllo degli accessi. Questo metodo di autenticazione \u00e8 consigliato come pi\u00f9 stabile. Per abilitare PSK sulla tua TV, vai su: Impostazioni -> Rete -> Configurazione rete domestica -> Controllo IP. Quindi seleziona la casella \u00abUtilizza l'autenticazione PSK\u00bb e inserisci la tua PSK anzich\u00e9 il PIN.", + "description": "Assicurati che \u00abControllo remoto\u00bb sia abilitato sul televisore, vai a: \nImpostazioni -> Rete -> Impostazioni dispositivo remoto -> Controllo remoto. \n\nEsistono due metodi di autorizzazione: codice PIN o PSK (Pre-Shared Key - Chiave Pre-Condivisa). \nL'autorizzazione tramite PSK \u00e8 consigliata in quanto pi\u00f9 stabile.", "title": "Autorizza Sony Bravia TV" }, "confirm": { "description": "Vuoi avviare la configurazione?" }, - "reauth_confirm": { + "pin": { "data": { - "pin": "Codice PIN", - "use_psk": "Usa l'autenticazione PSK" + "pin": "Codice PIN" }, - "description": "Inserisci il codice PIN mostrato sul Sony Bravia TV. \n\nSe il codice PIN non viene visualizzato, devi annullare la registrazione di Home Assistant sulla TV, vai su: Impostazioni -> Rete -> Impostazioni dispositivo remoto -> Annulla registrazione dispositivo remoto. \n\nPuoi usare PSK (Pre-Shared-Key) invece del PIN. PSK \u00e8 una chiave segreta definita dall'utente utilizzata per il controllo degli accessi. Questo metodo di autenticazione \u00e8 consigliato poich\u00e9 pi\u00f9 stabile. Per abilitare PSK sulla tua TV, vai su: Impostazioni -> Rete -> Configurazione rete domestica -> Controllo IP. Quindi seleziona la casella \u00abUtilizza l'autenticazione PSK\u00bb e inserisci la tua chiave PSK anzich\u00e9 il PIN." + "description": "Inserisci il codice PIN visualizzato sul TV Sony Bravia. \n\nSe il codice PIN non viene visualizzato, devi annullare la registrazione di Home Assistant sulla tua TV, vai a: Impostazioni -> Rete -> Impostazioni dispositivo remoto -> Annulla registrazione dispositivo remoto.", + "title": "Autorizza TV Sony Bravia" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "Per configurare PSK sul televisore, vai a: Impostazioni -> Rete -> Configurazione rete domestica -> Controllo IP. Impostare \u00abAutenticazione\u00bb su \u00abChiave normale e precondivisa\u00bb o \u00abChiave precondivisa\u00bb e definire la stringa della chiave precondivisa (ad es. sony). \n\nQuindi inserisci qui il tuo PSK.", + "title": "Autorizza TV Sony Bravia" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/ja.json b/homeassistant/components/braviatv/translations/ja.json index 3b541f3a424..070bedd1e5e 100644 --- a/homeassistant/components/braviatv/translations/ja.json +++ b/homeassistant/components/braviatv/translations/ja.json @@ -14,7 +14,6 @@ "step": { "authorize": { "data": { - "pin": "PIN\u30b3\u30fc\u30c9", "use_psk": "PSK\u8a8d\u8a3c\u3092\u4f7f\u7528\u3059\u308b" }, "description": "\u30bd\u30cb\u30fc Bravia TV\u306b\u8868\u793a\u3055\u308c\u3066\u3044\u308bPIN\u30b3\u30fc\u30c9\u3092\u5165\u529b\u3057\u307e\u3059\u3002 \n\nPIN\u30b3\u30fc\u30c9\u304c\u8868\u793a\u3055\u308c\u3066\u3044\u306a\u3044\u5834\u5408\u306f\u3001\u30c6\u30ec\u30d3\u304b\u3089Home Assistant\u306e\u767b\u9332\u3092\u89e3\u9664\u3059\u308b\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059\u306e\u3067\u3001\u6b21\u306e\u624b\u9806\u3067\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002\u8a2d\u5b9a \u2192 \u30cd\u30c3\u30c8\u30ef\u30fc\u30af \u2192 \u30ea\u30e2\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u8a2d\u5b9a \u2192 \u30ea\u30e2\u30fc\u30c8\u30c7\u30d0\u30a4\u30b9\u306e\u767b\u9332\u89e3\u9664 \u3092\u884c\u3063\u3066\u304f\u3060\u3055\u3044\u3002", @@ -23,12 +22,6 @@ "confirm": { "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" }, - "reauth_confirm": { - "data": { - "pin": "PIN\u30b3\u30fc\u30c9", - "use_psk": "PSK\u8a8d\u8a3c\u3092\u4f7f\u7528\u3059\u308b" - } - }, "user": { "data": { "host": "\u30db\u30b9\u30c8" diff --git a/homeassistant/components/braviatv/translations/ko.json b/homeassistant/components/braviatv/translations/ko.json index 00382c06ca0..0bc80db4d36 100644 --- a/homeassistant/components/braviatv/translations/ko.json +++ b/homeassistant/components/braviatv/translations/ko.json @@ -13,20 +13,12 @@ }, "step": { "authorize": { - "data": { - "pin": "PIN \ucf54\ub4dc" - }, "description": "Sony Bravia TV\uc5d0 \ud45c\uc2dc\ub41c PIN \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.\n\nPIN \ucf54\ub4dc\uac00 \ud45c\uc2dc\ub418\uc9c0 \uc54a\uc73c\uba74 TV\uc5d0\uc11c Home Assistant\ub97c \ub4f1\ub85d \ud574\uc81c\ud558\uc5ec\uc57c \ud569\ub2c8\ub2e4. Settings -> Network -> Remote device settings -> Unregister remote device\ub85c \uc774\ub3d9\ud558\uc5ec \ub4f1\ub85d\uc744 \ud574\uc81c\ud574\uc8fc\uc138\uc694.", "title": "Sony Bravia TV \uc2b9\uc778\ud558\uae30" }, "confirm": { "description": "\uc124\uc815\uc744 \uc2dc\uc791\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" }, - "reauth_confirm": { - "data": { - "pin": "PIN \ucf54\ub4dc" - } - }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8" diff --git a/homeassistant/components/braviatv/translations/lb.json b/homeassistant/components/braviatv/translations/lb.json index 109ce1c7e20..b6cf98c3236 100644 --- a/homeassistant/components/braviatv/translations/lb.json +++ b/homeassistant/components/braviatv/translations/lb.json @@ -11,9 +11,6 @@ }, "step": { "authorize": { - "data": { - "pin": "PIN-Code" - }, "description": "G\u00ebff de PIN code an deen op der Sony Bravia TV ugewise g\u00ebtt.\n\nFalls kee PIN code ugewise g\u00ebtt muss den Home Assistant um Fernseh ofgemellt ginn, um TV: Settings -> Network -> Remote device settings -> Unregister remote device.", "title": "Sony Bravia TV erlaaben" }, diff --git a/homeassistant/components/braviatv/translations/lv.json b/homeassistant/components/braviatv/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/braviatv/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/nl.json b/homeassistant/components/braviatv/translations/nl.json index 31dc4844163..312ffa11329 100644 --- a/homeassistant/components/braviatv/translations/nl.json +++ b/homeassistant/components/braviatv/translations/nl.json @@ -15,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "Pincode", "use_psk": "PSK-authenticatie gebruiken" }, "description": "Voer de pincode in die wordt weergegeven op de Sony Bravia tv. \n\nAls de pincode niet wordt weergegeven, moet u de Home Assistant op uw tv afmelden, ga naar: Instellingen -> Netwerk -> Instellingen extern apparaat -> Afmelden extern apparaat.", @@ -24,11 +23,17 @@ "confirm": { "description": "Wil je beginnen met instellen?" }, - "reauth_confirm": { + "pin": { "data": { - "pin": "Pincode", - "use_psk": "PSK-authenticatie gebruiken" - } + "pin": "Pincode" + }, + "title": "Autoriseer Sony Bravia TV" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "title": "Autoriseer Sony Bravia TV" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json index a568e364c12..1bd719f5261 100644 --- a/homeassistant/components/braviatv/translations/no.json +++ b/homeassistant/components/braviatv/translations/no.json @@ -4,8 +4,7 @@ "already_configured": "Enheten er allerede konfigurert", "no_ip_control": "IP-kontrollen er deaktivert p\u00e5 TVen eller TV-en st\u00f8ttes ikke.", "not_bravia_device": "Enheten er ikke en Bravia TV.", - "reauth_successful": "Re-autentisering var vellykket", - "reauth_unsuccessful": "Re-autentisering mislyktes. Fjern integrasjonen og konfigurer den p\u00e5 nytt." + "reauth_successful": "Re-autentisering var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -16,21 +15,27 @@ "step": { "authorize": { "data": { - "pin": "PIN kode", "use_psk": "Bruk PSK-autentisering" }, - "description": "Skriv inn PIN-koden som vises p\u00e5 Sony Bravia TV. \n\n Hvis PIN-koden ikke vises, m\u00e5 du avregistrere Home Assistant p\u00e5 TV-en din, g\u00e5 til: Innstillinger - > Nettverk - > Innstillinger for ekstern enhet - > Avregistrer ekstern enhet. \n\n Du kan bruke PSK (Pre-Shared-Key) i stedet for PIN. PSK er en brukerdefinert hemmelig n\u00f8kkel som brukes til tilgangskontroll. Denne autentiseringsmetoden anbefales som mer stabil. For \u00e5 aktivere PSK p\u00e5 TV-en, g\u00e5 til: Innstillinger - > Nettverk - > Oppsett for hjemmenettverk - > IP-kontroll. Kryss s\u00e5 av \u00abBruk PSK-autentisering\u00bb-boksen og skriv inn din PSK i stedet for PIN-kode.", + "description": "S\u00f8rg for at \u00abFjernkontroll\u00bb er aktivert p\u00e5 TV-en din, g\u00e5 til:\n Innstillinger - > Nettverk - > Innstillinger for ekstern enhet - > Fjernkontroll. \n\n Det er to autorisasjonsmetoder: PIN-kode eller PSK (Pre-Shared Key).\n Autorisasjon via PSK anbefales som mer stabil.", "title": "Godkjenn Sony Bravia TV" }, "confirm": { "description": "Vil du starte oppsettet?" }, - "reauth_confirm": { + "pin": { "data": { - "pin": "PIN kode", - "use_psk": "Bruk PSK-autentisering" + "pin": "PIN kode" }, - "description": "Skriv inn PIN-koden som vises p\u00e5 Sony Bravia TV. \n\n Hvis PIN-koden ikke vises, m\u00e5 du avregistrere Home Assistant p\u00e5 TV-en din, g\u00e5 til: Innstillinger - > Nettverk - > Innstillinger for ekstern enhet - > Avregistrer ekstern enhet. \n\n Du kan bruke PSK (Pre-Shared-Key) i stedet for PIN. PSK er en brukerdefinert hemmelig n\u00f8kkel som brukes til tilgangskontroll. Denne autentiseringsmetoden anbefales som mer stabil. For \u00e5 aktivere PSK p\u00e5 TV-en, g\u00e5 til: Innstillinger - > Nettverk - > Oppsett for hjemmenettverk - > IP-kontroll. Kryss s\u00e5 av \u00abBruk PSK-autentisering\u00bb-boksen og skriv inn din PSK i stedet for PIN-kode." + "description": "Skriv inn PIN-koden som vises p\u00e5 Sony Bravia TV. \n\n Hvis PIN-koden ikke vises, m\u00e5 du avregistrere Home Assistant p\u00e5 TV-en din, g\u00e5 til: Innstillinger - > Nettverk - > Innstillinger for ekstern enhet - > Avregistrer ekstern enhet.", + "title": "Autoriser Sony Bravia TV" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "For \u00e5 sette opp PSK p\u00e5 TV-en, g\u00e5 til: Innstillinger - > Nettverk - > Oppsett for hjemmenettverk - > IP-kontroll. Sett \u00abAutentisering\u00bb til \u00abNormal og forh\u00e5ndsdelt n\u00f8kkel\u00bb eller \u00abForh\u00e5ndsdelt n\u00f8kkel\u00bb og definer din forh\u00e5ndsdelte n\u00f8kkelstreng (f.eks. Sony). \n\n Skriv inn din PSK her.", + "title": "Autoriser Sony Bravia TV" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/pl.json b/homeassistant/components/braviatv/translations/pl.json index 53847bf7b2c..03cc4b20961 100644 --- a/homeassistant/components/braviatv/translations/pl.json +++ b/homeassistant/components/braviatv/translations/pl.json @@ -4,8 +4,7 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "no_ip_control": "Sterowanie IP jest wy\u0142\u0105czone w telewizorze lub telewizor nie jest obs\u0142ugiwany", "not_bravia_device": "Urz\u0105dzenie nie jest telewizorem Bravia", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", - "reauth_unsuccessful": "B\u0142\u0105d ponownego uwierzytelnienia, usu\u0144 integracj\u0119 i skonfiguruj j\u0105 ponownie" + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", @@ -16,21 +15,27 @@ "step": { "authorize": { "data": { - "pin": "Kod PIN", "use_psk": "U\u017cyj uwierzytelniania PSK" }, - "description": "Wprowad\u017a kod PIN wy\u015bwietlany na telewizorze Sony Bravia. \n\nJe\u015bli kod PIN nie jest wy\u015bwietlany, musisz wyrejestrowa\u0107 Home Assistanta na telewizorze. Przejd\u017a do: Ustawienia - > Sie\u0107 - > Ustawienia urz\u0105dzenia zdalnego - > Wyrejestruj zdalne urz\u0105dzenie. \n\nMo\u017cesz u\u017cy\u0107 PSK (Pre-Shared-Key) zamiast kodu PIN. PSK to zdefiniowany przez u\u017cytkownika tajny klucz u\u017cywany do kontroli dost\u0119pu. Ta metoda uwierzytelniania jest zalecana jako bardziej stabilna. Aby w\u0142\u0105czy\u0107 PSK na telewizorze, przejd\u017a do: Ustawienia - > Sie\u0107 - > Konfiguracja sieci domowej - > Sterowanie IP. Nast\u0119pnie zaznacz pole \u201eU\u017cyj uwierzytelniania PSK\u201d i wprowad\u017a sw\u00f3j PSK zamiast kodu PIN.", + "description": "Upewnij si\u0119, \u017ce w telewizorze w\u0142\u0105czona jest opcja \u201eSteruj zdalnie\u201d. Przejd\u017a do:\nUstawienia -> Sie\u0107 -> Ustawienia urz\u0105dzenia zdalnego -> Steruj zdalnie. \n\nIstniej\u0105 dwie metody autoryzacji: kod PIN lub klucz PSK (Pre-Shared Key).\nAutoryzacja przez PSK jest zalecana jako bardziej stabilna.", "title": "Autoryzacja Sony Bravia TV" }, "confirm": { "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" }, - "reauth_confirm": { + "pin": { "data": { - "pin": "Kod PIN", - "use_psk": "U\u017cyj uwierzytelniania PSK" + "pin": "Kod PIN" }, - "description": "Wprowad\u017a kod PIN wy\u015bwietlany na telewizorze Sony Bravia. \n\nJe\u015bli kod PIN nie jest wy\u015bwietlany, musisz wyrejestrowa\u0107 Home Assistanta na telewizorze. Przejd\u017a do: Ustawienia - > Sie\u0107 - > Ustawienia urz\u0105dzenia zdalnego - > Wyrejestruj zdalne urz\u0105dzenie. \n\nMo\u017cesz u\u017cy\u0107 PSK (Pre-Shared-Key) zamiast kodu PIN. PSK to zdefiniowany przez u\u017cytkownika tajny klucz u\u017cywany do kontroli dost\u0119pu. Ta metoda uwierzytelniania jest zalecana jako bardziej stabilna. Aby w\u0142\u0105czy\u0107 PSK na telewizorze, przejd\u017a do: Ustawienia - > Sie\u0107 - > Konfiguracja sieci domowej - > Sterowanie IP. Nast\u0119pnie zaznacz pole \u201eU\u017cyj uwierzytelniania PSK\u201d i wprowad\u017a sw\u00f3j PSK zamiast kodu PIN." + "description": "Wprowad\u017a kod PIN wy\u015bwietlany na telewizorze Sony Bravia. \n\nJe\u015bli kod PIN nie jest wy\u015bwietlany, musisz wyrejestrowa\u0107 Home Assistanta na swoim telewizorze, przejd\u017a do Ustawienia -> Sie\u0107 -> Ustawienia urz\u0105dzenia zdalnego -> Wyrejestruj urz\u0105dzenie zdalne.", + "title": "Autoryzacja Sony Bravia TV" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "Aby skonfigurowa\u0107 PSK w telewizorze, przejd\u017a do: Ustawienia -> Sie\u0107 -> Konfiguracja sieci domowej -> Kontrola IP. Ustaw \u201eUwierzytelnianie\u201d na \u201eNormalne i Pre-Shared Key\u201d lub \u201ePre-Shared Key\u201d i zdefiniuj sw\u00f3j ci\u0105g Pre-Shared Key (np. sony). \n\nNast\u0119pnie wpisz tutaj sw\u00f3j Pre-Shared Key.", + "title": "Autoryzacja Sony Bravia TV" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/pt-BR.json b/homeassistant/components/braviatv/translations/pt-BR.json index e048568e351..c99c80b0103 100644 --- a/homeassistant/components/braviatv/translations/pt-BR.json +++ b/homeassistant/components/braviatv/translations/pt-BR.json @@ -4,8 +4,7 @@ "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.", "not_bravia_device": "O dispositivo n\u00e3o \u00e9 uma TV Bravia.", - "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." + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" }, "error": { "cannot_connect": "Falha ao conectar", @@ -16,21 +15,27 @@ "step": { "authorize": { "data": { - "pin": "C\u00f3digo PIN", "use_psk": "Usar autentica\u00e7\u00e3o PSK" }, - "description": "Digite o c\u00f3digo PIN mostrado na TV Sony Bravia. \n\n Se o c\u00f3digo PIN n\u00e3o for exibido, 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 o registro do dispositivo remoto. \n\n Voc\u00ea pode usar PSK (Pre-Shared-Key) em vez de PIN. PSK \u00e9 uma chave secreta definida pelo usu\u00e1rio usada para controle de acesso. Este m\u00e9todo de autentica\u00e7\u00e3o \u00e9 recomendado como mais est\u00e1vel. Para ativar o PSK em sua TV, v\u00e1 para: Configura\u00e7\u00f5es - > Rede - > Configura\u00e7\u00e3o de rede dom\u00e9stica - > Controle de IP. Em seguida, marque a caixa \u00abUsar autentica\u00e7\u00e3o PSK\u00bb e digite seu PSK em vez do PIN.", + "description": "Certifique-se de que \u00abControlar remotamente\u00bb est\u00e1 ativado na sua TV, v\u00e1 para:\n Configura\u00e7\u00f5es - > Rede - > Configura\u00e7\u00f5es do dispositivo remoto - > Controle remotamente. \n\n Existem dois m\u00e9todos de autoriza\u00e7\u00e3o: c\u00f3digo PIN ou PSK (chave pr\u00e9-compartilhada).\n A autoriza\u00e7\u00e3o via PSK \u00e9 recomendada como mais est\u00e1vel.", "title": "Autorizar a TV Sony Bravia" }, "confirm": { "description": "Deseja iniciar a configura\u00e7\u00e3o?" }, - "reauth_confirm": { + "pin": { "data": { - "pin": "C\u00f3digo PIN", - "use_psk": "Usar autentica\u00e7\u00e3o PSK" + "pin": "C\u00f3digo PIN" }, - "description": "Digite o c\u00f3digo PIN mostrado na TV Sony Bravia. \n\n Se o c\u00f3digo PIN n\u00e3o for exibido, 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 o registro do dispositivo remoto. \n\n Voc\u00ea pode usar PSK (Pre-Shared-Key) em vez de PIN. PSK \u00e9 uma chave secreta definida pelo usu\u00e1rio usada para controle de acesso. Este m\u00e9todo de autentica\u00e7\u00e3o \u00e9 recomendado como mais est\u00e1vel. Para ativar o PSK em sua TV, v\u00e1 para: Configura\u00e7\u00f5es - > Rede - > Configura\u00e7\u00e3o de rede dom\u00e9stica - > Controle de IP. Em seguida, marque a caixa \u00abUsar autentica\u00e7\u00e3o PSK\u00bb e digite seu PSK em vez do PIN." + "description": "Insira o c\u00f3digo PIN exibido na TV Sony Bravia. \n\n Se o c\u00f3digo PIN n\u00e3o for exibido, voc\u00ea dever\u00e1 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 TV Sony Bravia" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "Para configurar o PSK na sua TV, v\u00e1 para: Configura\u00e7\u00f5es - > Rede - > Configura\u00e7\u00e3o de rede dom\u00e9stica - > Controle de IP. Defina \u00abAutentica\u00e7\u00e3o\u00bb como \u00abChave normal e pr\u00e9-compartilhada\u00bb ou \u00abChave pr\u00e9-compartilhada\u00bb e defina sua sequ\u00eancia de chave pr\u00e9-compartilhada (por exemplo, sony). \n\n Em seguida, insira seu PSK aqui.", + "title": "Autorizar TV Sony Bravia" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/pt.json b/homeassistant/components/braviatv/translations/pt.json index f41faab1272..eac2cc13563 100644 --- a/homeassistant/components/braviatv/translations/pt.json +++ b/homeassistant/components/braviatv/translations/pt.json @@ -13,20 +13,12 @@ }, "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 exibido, \u00e9 necess\u00e1rio cancelar o registro do Home Assistant na TV, v\u00e1 para: Configura\u00e7\u00f5es -> Rede -> Configura\u00e7\u00f5es do dispositivo remoto -> Cancelar registro do dispositivo remoto.", "title": "Autorizar TV Sony Bravia" }, "confirm": { "description": "Quer dar in\u00edcio \u00e0 configura\u00e7\u00e3o?" }, - "reauth_confirm": { - "data": { - "pin": "C\u00f3digo PIN" - } - }, "user": { "data": { "host": "Endere\u00e7o" diff --git a/homeassistant/components/braviatv/translations/ru.json b/homeassistant/components/braviatv/translations/ru.json index 299ad538bc6..12a2bc9ec7c 100644 --- a/homeassistant/components/braviatv/translations/ru.json +++ b/homeassistant/components/braviatv/translations/ru.json @@ -4,8 +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.", "no_ip_control": "\u041d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u043e IP, \u043b\u0438\u0431\u043e \u044d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", "not_bravia_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Bravia.", - "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_unsuccessful": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0439\u0442\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0443\u044e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0435\u0451 \u0441\u043d\u043e\u0432\u0430." + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", @@ -16,21 +15,27 @@ "step": { "authorize": { "data": { - "pin": "PIN-\u043a\u043e\u0434", "use_psk": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c PSK-\u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u044e\u0449\u0438\u0439\u0441\u044f \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 Sony Bravia. \n\n\u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0435 \u0432\u0438\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e Home Assistant \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 -> \u0421\u0435\u0442\u044c -> \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 -> \u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c PSK (Pre-Shared-Key) \u0432\u043c\u0435\u0441\u0442\u043e PIN-\u043a\u043e\u0434\u0430. PSK \u2014 \u044d\u0442\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u043c \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043b\u044e\u0447, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0434\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u043e\u043c. \u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u0431\u043e\u043b\u0435\u0435 \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u0439. \u0427\u0442\u043e\u0431\u044b \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c PSK \u043d\u0430 \u0412\u0430\u0448\u0435\u043c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 - > \u0421\u0435\u0442\u044c - > \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u043c\u0430\u0448\u043d\u0435\u0439 \u0441\u0435\u0442\u0438 - > \u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 IP. \u0417\u0430\u0442\u0435\u043c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 \u0444\u043b\u0430\u0436\u043e\u043a \u00ab\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e PSK\u00bb \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 PSK \u0432\u043c\u0435\u0441\u0442\u043e PIN-\u043a\u043e\u0434\u0430.", + "description": "\u0427\u0442\u043e\u0431\u044b \u0443\u0431\u0435\u0434\u0438\u0442\u044c\u0441\u044f, \u0447\u0442\u043e \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0435 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043d\u0430 \u0412\u0430\u0448\u0435\u043c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432: \n\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 -> \u0421\u0435\u0442\u044c -> \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 -> \u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e. \n\n\u0421\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u0434\u0432\u0430 \u043c\u0435\u0442\u043e\u0434\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438: PIN-\u043a\u043e\u0434 \u0438\u043b\u0438 PSK (Pre-Shared Key). \n\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0447\u0435\u0440\u0435\u0437 PSK \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u0431\u043e\u043b\u0435\u0435 \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u0430\u044f.", "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 Sony Bravia" }, "confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" }, - "reauth_confirm": { + "pin": { "data": { - "pin": "PIN-\u043a\u043e\u0434", - "use_psk": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c PSK-\u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e" + "pin": "PIN-\u043a\u043e\u0434" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u044e\u0449\u0438\u0439\u0441\u044f \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 Sony Bravia. \n\n\u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0435 \u0432\u0438\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e Home Assistant \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 -> \u0421\u0435\u0442\u044c -> \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 -> \u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c PSK (Pre-Shared-Key) \u0432\u043c\u0435\u0441\u0442\u043e PIN-\u043a\u043e\u0434\u0430. PSK \u2014 \u044d\u0442\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c\u044b\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u043c \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u044b\u0439 \u043a\u043b\u044e\u0447, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0434\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u043e\u043c. \u042d\u0442\u043e\u0442 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u0431\u043e\u043b\u0435\u0435 \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u044b\u0439. \u0427\u0442\u043e\u0431\u044b \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c PSK \u043d\u0430 \u0412\u0430\u0448\u0435\u043c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 - > \u0421\u0435\u0442\u044c - > \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u043c\u0430\u0448\u043d\u0435\u0439 \u0441\u0435\u0442\u0438 - > \u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 IP. \u0417\u0430\u0442\u0435\u043c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 \u0444\u043b\u0430\u0436\u043e\u043a \u00ab\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e PSK\u00bb \u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 PSK \u0432\u043c\u0435\u0441\u0442\u043e PIN-\u043a\u043e\u0434\u0430." + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u044e\u0449\u0438\u0439\u0441\u044f \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 Sony Bravia. \n\n\u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0435 \u0432\u0438\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e Home Assistant \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 -> \u0421\u0435\u0442\u044c -> \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 -> \u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u044e \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 Sony Bravia" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "\u0427\u0442\u043e\u0431\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c PSK \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432: \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 -> \u0421\u0435\u0442\u044c -> \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u043c\u0430\u0448\u043d\u0435\u0439 \u0441\u0435\u0442\u0438 -> \u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 IP. \u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0435 \u0434\u043b\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430 \"\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f\" \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \"\u041e\u0431\u044b\u0447\u043d\u0430\u044f \u0438 Pre-Shared Key\" \u0438\u043b\u0438 \"Pre-Shared Key\" \u0438 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u0435 \u0441\u0442\u0440\u043e\u043a\u0443 Pre-Shared-Key (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, sony). \n\n\u0417\u0430\u0442\u0435\u043c \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 PSK \u0437\u0434\u0435\u0441\u044c.", + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 Sony Bravia" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/sk.json b/homeassistant/components/braviatv/translations/sk.json index 133429ed2c2..0bd2cc39bea 100644 --- a/homeassistant/components/braviatv/translations/sk.json +++ b/homeassistant/components/braviatv/translations/sk.json @@ -4,8 +4,7 @@ "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "no_ip_control": "Ovl\u00e1danie IP je na va\u0161om telev\u00edzore vypnut\u00e9 alebo telev\u00edzor nie je podporovan\u00fd.", "not_bravia_device": "Zariadenie nie je telev\u00edzor Bravia.", - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", - "reauth_unsuccessful": "Op\u00e4tovn\u00e1 autentifik\u00e1cia bola ne\u00faspe\u0161n\u00e1, odstr\u00e1\u0148te integr\u00e1ciu a znova ju nastavte." + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { "cannot_connect": "Nepodarilo sa pripoji\u0165", @@ -16,21 +15,27 @@ "step": { "authorize": { "data": { - "pin": "PIN k\u00f3d", "use_psk": "Pou\u017eite autentifik\u00e1ciu PSK" }, - "description": "Zadajte PIN k\u00f3d zobrazen\u00fd na telev\u00edzii Sony Bravia.\n\nPokia\u013e sa PIN k\u00f3d nezobraz\u00ed, je potrebn\u00e9 zru\u0161i\u0165 registr\u00e1ciu Home Assistant na telev\u00edzii, prejdite na: Nastavenia -> Sie\u0165 -> Nastavenie vzdialen\u00e9ho zariadenia -> Zru\u0161i\u0165 registr\u00e1ciu vzdialen\u00e9ho zariadenia.", + "description": "Uistite sa, \u017ee je na va\u0161om telev\u00edzore aktivovan\u00e9 \u00abOvl\u00e1danie na dia\u013eku\u00bb, prejdite na:\n Nastavenia - > Sie\u0165 - > Nastavenia vzdialen\u00e9ho zariadenia - > Ovl\u00e1da\u0165 na dia\u013eku. \n\n Existuj\u00fa dva sp\u00f4soby autoriz\u00e1cie: PIN k\u00f3d alebo PSK (Pre-Shared Key).\n Ako stabilnej\u0161ia sa odpor\u00fa\u010da autoriz\u00e1cia cez PSK.", "title": "Autorizujte telev\u00edzor Sony Bravia" }, "confirm": { "description": "Chcete za\u010da\u0165 nastavova\u0165?" }, - "reauth_confirm": { + "pin": { "data": { - "pin": "PIN k\u00f3d", - "use_psk": "Pou\u017eite autentifik\u00e1ciu PSK" + "pin": "PIN k\u00f3d" }, - "description": "Zadajte PIN k\u00f3d zobrazen\u00fd na telev\u00edzii Sony Bravia.\n\nPokia\u013e sa PIN k\u00f3d nezobraz\u00ed, je potrebn\u00e9 zru\u0161i\u0165 registr\u00e1ciu Home Assistant na telev\u00edzii, prejdite na: Nastavenia -> Sie\u0165 -> Nastavenie vzdialen\u00e9ho zariadenia -> Zru\u0161i\u0165 registr\u00e1ciu vzdialen\u00e9ho zariadenia." + "description": "Zadajte k\u00f3d PIN zobrazen\u00fd na telev\u00edzore Sony Bravia. \n\n Ak sa k\u00f3d PIN nezobrazuje, mus\u00edte zru\u0161i\u0165 registr\u00e1ciu aplik\u00e1cie Home Assistant na telev\u00edzore, prejdite na: Nastavenia - > Sie\u0165 - > Nastavenia vzdialen\u00e9ho zariadenia - > Zru\u0161te registr\u00e1ciu vzdialen\u00e9ho zariadenia.", + "title": "Autorizujte telev\u00edzor Sony Bravia" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "Ak chcete nastavi\u0165 PSK na svojom telev\u00edzore, prejdite na: Nastavenia - > Sie\u0165 - > Nastavenie dom\u00e1cej siete - > Ovl\u00e1danie IP. Nastavte \u00abAuthentication\u00bb na \u00abNormal and Pre-Shared Key\u00bb alebo \u00abPre-Shared Key\u00bb a definujte svoj re\u0165azec pre-Shared-Key (napr. Sony). \n\n Tu zadajte svoje PSK.", + "title": "Autorizujte telev\u00edzor Sony Bravia" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/sv.json b/homeassistant/components/braviatv/translations/sv.json index 9b218f562d2..91af5afaf1b 100644 --- a/homeassistant/components/braviatv/translations/sv.json +++ b/homeassistant/components/braviatv/translations/sv.json @@ -4,8 +4,7 @@ "already_configured": "Den h\u00e4r TV:n \u00e4r redan konfigurerad", "no_ip_control": "IP-kontroll \u00e4r inaktiverat p\u00e5 din TV eller s\u00e5 st\u00f6ds inte TV:n.", "not_bravia_device": "Enheten \u00e4r inte en Bravia TV.", - "reauth_successful": "\u00c5terautentisering lyckades", - "reauth_unsuccessful": "\u00c5terautentiseringen misslyckades. Ta bort integrationen och konfigurera den igen." + "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { "cannot_connect": "Det gick inte att ansluta.", @@ -16,7 +15,6 @@ "step": { "authorize": { "data": { - "pin": "Pin-kod", "use_psk": "Anv\u00e4nd PSK-autentisering" }, "description": "Ange PIN-koden som visas p\u00e5 Sony Bravia TV. \n\n Om PIN-koden inte visas m\u00e5ste du avregistrera Home Assistant p\u00e5 din TV, g\u00e5 till: Inst\u00e4llningar - > N\u00e4tverk - > Inst\u00e4llningar f\u00f6r fj\u00e4rrenhet - > Avregistrera fj\u00e4rrenhet.", @@ -25,12 +23,11 @@ "confirm": { "description": "Vill du starta konfigurationen?" }, - "reauth_confirm": { + "psk": { "data": { - "pin": "Pin-kod", - "use_psk": "Anv\u00e4nd PSK-autentisering" + "pin": "PSK" }, - "description": "Ange PIN-koden som visas p\u00e5 Sony Bravia TV. \n\n Om PIN-koden inte visas m\u00e5ste du avregistrera Home Assistant p\u00e5 din TV, g\u00e5 till: Inst\u00e4llningar - > N\u00e4tverk - > Inst\u00e4llningar f\u00f6r fj\u00e4rrenhet - > Avregistrera fj\u00e4rrenhet. \n\n Du kan anv\u00e4nda PSK (Pre-Shared-Key) ist\u00e4llet f\u00f6r PIN. PSK \u00e4r en anv\u00e4ndardefinierad hemlig nyckel som anv\u00e4nds f\u00f6r \u00e5tkomstkontroll. Denna autentiseringsmetod rekommenderas eftersom den \u00e4r mer stabil. F\u00f6r att aktivera PSK p\u00e5 din TV, g\u00e5 till: Inst\u00e4llningar - > N\u00e4tverk - > Hemn\u00e4tverksinst\u00e4llningar - > IP-kontroll. Markera sedan rutan \u00abAnv\u00e4nd PSK-autentisering\u00bb och ange din PSK ist\u00e4llet f\u00f6r PIN-kod." + "title": "Auktorisera Sony Bravia TV" }, "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/tr.json b/homeassistant/components/braviatv/translations/tr.json index 939f8e71b7b..5c1365a5b8d 100644 --- a/homeassistant/components/braviatv/translations/tr.json +++ b/homeassistant/components/braviatv/translations/tr.json @@ -4,8 +4,7 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "no_ip_control": "TV'nizde IP Kontrol\u00fc devre d\u0131\u015f\u0131 veya TV desteklenmiyor.", "not_bravia_device": "Cihaz bir Bravia TV de\u011fildir.", - "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", - "reauth_unsuccessful": "Yeniden kimlik do\u011frulama ba\u015far\u0131s\u0131z oldu, l\u00fctfen entegrasyonu kald\u0131r\u0131n ve yeniden kurun." + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", @@ -16,21 +15,27 @@ "step": { "authorize": { "data": { - "pin": "PIN Kodu", "use_psk": "PSK kimlik do\u011frulamas\u0131n\u0131 kullan\u0131n" }, - "description": "Sony Bravia TV'de g\u00f6sterilen PIN kodunu girin. \n\n PIN kodu g\u00f6r\u00fcnt\u00fclenmezse, TV'nizde Home Assistant kayd\u0131n\u0131 iptal etmeniz gerekir, \u015furaya gidin: Ayarlar - > A\u011f - > Uzak cihaz ayarlar\u0131 - > Uzak cihaz\u0131n kayd\u0131n\u0131 sil. \n\n PIN yerine PSK (\u00d6n Payla\u015f\u0131ml\u0131 Anahtar) kullanabilirsiniz. PSK, eri\u015fim kontrol\u00fc i\u00e7in kullan\u0131lan kullan\u0131c\u0131 tan\u0131ml\u0131 bir gizli anahtard\u0131r. Bu kimlik do\u011frulama y\u00f6nteminin daha kararl\u0131 olmas\u0131 \u00f6nerilir. TV'nizde PSK'y\u0131 etkinle\u015ftirmek i\u00e7in \u015furaya gidin: Ayarlar - > A\u011f - > Ev A\u011f\u0131 Kurulumu - > IP Kontrol\u00fc. Ard\u0131ndan \u00abPSK kimlik do\u011frulamas\u0131n\u0131 kullan\u00bb kutusunu i\u015faretleyin ve PIN yerine PSK'n\u0131z\u0131 girin.", + "description": "TV'nizde \u00abUzaktan kontrol\u00bb \u00f6zelli\u011finin etkinle\u015ftirildi\u011finden emin olun, \u015fu adrese gidin:\n Ayarlar - > A\u011f - > Uzak cihaz ayarlar\u0131 - > Uzaktan kontrol. \n\n \u0130ki yetkilendirme y\u00f6ntemi vard\u0131r: PIN kodu veya PSK (\u00d6n Payla\u015f\u0131ml\u0131 Anahtar).\n Daha kararl\u0131 olmas\u0131 i\u00e7in PSK arac\u0131l\u0131\u011f\u0131yla yetkilendirme \u00f6nerilir.", "title": "Sony Bravia TV'yi yetkilendirin" }, "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" }, - "reauth_confirm": { + "pin": { "data": { - "pin": "PIN Kodu", - "use_psk": "PSK kimlik do\u011frulamas\u0131n\u0131 kullan\u0131n" + "pin": "PIN Kodu" }, - "description": "Sony Bravia TV'de g\u00f6sterilen PIN kodunu girin. \n\n PIN kodu g\u00f6r\u00fcnt\u00fclenmezse, TV'nizde Home Assistant kayd\u0131n\u0131 iptal etmeniz gerekir, \u015furaya gidin: Ayarlar - > A\u011f - > Uzak cihaz ayarlar\u0131 - > Uzak cihaz\u0131n kayd\u0131n\u0131 sil. \n\n PIN yerine PSK (\u00d6n Payla\u015f\u0131ml\u0131 Anahtar) kullanabilirsiniz. PSK, eri\u015fim kontrol\u00fc i\u00e7in kullan\u0131lan kullan\u0131c\u0131 tan\u0131ml\u0131 bir gizli anahtard\u0131r. Bu kimlik do\u011frulama y\u00f6nteminin daha kararl\u0131 olmas\u0131 \u00f6nerilir. TV'nizde PSK'y\u0131 etkinle\u015ftirmek i\u00e7in \u015furaya gidin: Ayarlar - > A\u011f - > Ev A\u011f\u0131 Kurulumu - > IP Kontrol\u00fc. Ard\u0131ndan \u00abPSK kimlik do\u011frulamas\u0131n\u0131 kullan\u00bb kutusunu i\u015faretleyin ve PIN yerine PSK'n\u0131z\u0131 girin." + "description": "Sony Bravia TV'de g\u00f6sterilen PIN kodunu girin. \n\n PIN kodu g\u00f6sterilmiyorsa TV'nizdeki Home Assistant kayd\u0131n\u0131 iptal etmeniz gerekir, \u015furaya gidin: Ayarlar - > A\u011f - > Uzak cihaz ayarlar\u0131 - > Uzak cihaz\u0131n kayd\u0131n\u0131 sil.", + "title": "Sony Bravia TV'yi yetkilendirin" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "TV'nizde PSK'yi kurmak i\u00e7in \u015furaya gidin: Ayarlar - > A\u011f - > Ev A\u011f\u0131 Kurulumu - > IP Kontrol\u00fc. \u00abKimlik Do\u011frulama\u00bb \u00f6\u011fesini \u00abNormal ve \u00d6n Payla\u015f\u0131ml\u0131 Anahtar\u00bb veya \u00ab\u00d6n Payla\u015f\u0131ml\u0131 Anahtar\u00bb olarak ayarlay\u0131n ve \u00d6n Payla\u015f\u0131ml\u0131 Anahtar dizinizi (\u00f6rn. sony) tan\u0131mlay\u0131n. \n\n Ard\u0131ndan PSK'n\u0131z\u0131 buraya girin.", + "title": "Sony Bravia TV'yi yetkilendirin" }, "user": { "data": { @@ -41,6 +46,9 @@ } }, "options": { + "abort": { + "failed_update": "Kaynak listesi g\u00fcncellenirken bir hata olu\u015ftu. \n\n Ayarlamaya \u00e7al\u0131\u015fmadan \u00f6nce TV'nizin a\u00e7\u0131k oldu\u011fundan emin olun." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/braviatv/translations/uk.json b/homeassistant/components/braviatv/translations/uk.json index 5d9e22e59de..acc8fa70f30 100644 --- a/homeassistant/components/braviatv/translations/uk.json +++ b/homeassistant/components/braviatv/translations/uk.json @@ -11,12 +11,14 @@ }, "step": { "authorize": { - "data": { - "pin": "PIN-\u043a\u043e\u0434" - }, "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434, \u044f\u043a\u0438\u0439 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Sony Bravia. \n\n\u042f\u043a\u0449\u043e \u0412\u0438 \u043d\u0435 \u0431\u0430\u0447\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0441\u043a\u0430\u0441\u0443\u0432\u0430\u0442\u0438 \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u044e Home Assistant \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0456. \u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 - > \u041c\u0435\u0440\u0435\u0436\u0430 - > \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e - > \u0421\u043a\u0430\u0441\u0443\u0432\u0430\u0442\u0438 \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u044e \u0432\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e.", "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Sony Bravia" }, + "pin": { + "data": { + "pin": "PIN \u043a\u043e\u0434" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/braviatv/translations/zh-Hans.json b/homeassistant/components/braviatv/translations/zh-Hans.json index 6f115e243ac..20449fcb3d1 100644 --- a/homeassistant/components/braviatv/translations/zh-Hans.json +++ b/homeassistant/components/braviatv/translations/zh-Hans.json @@ -1,24 +1,13 @@ { "config": { "abort": { - "reauth_successful": "\u91cd\u65b0\u8ba4\u8bc1\u6210\u529f", - "reauth_unsuccessful": "\u91cd\u65b0\u9a8c\u8bc1\u5931\u8d25\uff0c\u8bf7\u79fb\u9664\u96c6\u6210\u5e76\u91cd\u65b0\u8bbe\u7f6e\u3002" + "reauth_successful": "\u91cd\u65b0\u8ba4\u8bc1\u6210\u529f" }, "step": { "authorize": { - "data": { - "pin": "PIN \u7801" - }, "description": "\u8f93\u5165\u5728 Sony Bravia \u7535\u89c6\u4e0a\u663e\u793a\u7684 PIN \u7801\u3002 \n\n\u5982\u679c\u672a\u663e\u793a PIN \u7801\uff0c\u60a8\u9700\u8981\u5728\u7535\u89c6\u4e0a\u53d6\u6d88\u6ce8\u518c Home Assistant\uff0c\u8bf7\u8f6c\u5230\uff1a\u8bbe\u7f6e - >\u7f51\u7edc - >\u8fdc\u7a0b\u8bbe\u5907\u8bbe\u7f6e - >\u53d6\u6d88\u6ce8\u518c\u8fdc\u7a0b\u8bbe\u5907\u3002", "title": "\u6388\u6743 Sony Bravia \u7535\u89c6" }, - "reauth_confirm": { - "data": { - "pin": "PIN\u7801", - "use_psk": "\u4f7f\u7528 PSK \u8ba4\u8bc1" - }, - "description": "\u8f93\u5165 Sony Bravia \u7535\u89c6\u4e0a\u663e\u793a\u7684 PIN \u7801\u3002 \n\n\u5982\u679c PIN \u7801\u672a\u663e\u793a\uff0c\u60a8\u5fc5\u987b\u5728\u7535\u89c6\u4e0a\u53d6\u6d88\u6ce8\u518c Home Assistant\uff0c\u524d\u5f80\uff1a\u8bbe\u7f6e - >\u7f51\u7edc - >\u8fdc\u7a0b\u8bbe\u5907\u8bbe\u7f6e - >\u53d6\u6d88\u6ce8\u518c\u8fdc\u7a0b\u8bbe\u5907\u3002 \n\n\u60a8\u53ef\u4ee5\u4f7f\u7528 PSK\uff08\u9884\u5171\u4eab\u5bc6\u94a5\uff09\u4ee3\u66ff PIN\u3002 PSK \u662f\u7528\u4e8e\u8bbf\u95ee\u63a7\u5236\u7684\u7528\u6237\u5b9a\u4e49\u7684\u5bc6\u94a5\u3002\u63a8\u8350\u4f7f\u7528\u8fd9\u79cd\u8eab\u4efd\u9a8c\u8bc1\u65b9\u6cd5\uff0c\u56e0\u4e3a\u5b83\u66f4\u7a33\u5b9a\u3002\u8981\u5728\u7535\u89c6\u4e0a\u542f\u7528 PSK\uff0c\u8bf7\u8f6c\u5230\uff1a\u8bbe\u7f6e - >\u7f51\u7edc - >\u5bb6\u5ead\u7f51\u7edc\u8bbe\u7f6e - > IP \u63a7\u5236\u3002\u7136\u540e\u9009\u4e2d\u00ab\u4f7f\u7528 PSK \u8eab\u4efd\u9a8c\u8bc1\u00bb\u6846\u5e76\u8f93\u5165\u60a8\u7684 PSK \u800c\u4e0d\u662f PIN\u3002" - }, "user": { "description": "\u8bbe\u7f6e Sony Bravia \u7535\u89c6\u96c6\u6210\u3002\u5982\u679c\u60a8\u5728\u914d\u7f6e\u65b9\u9762\u9047\u5230\u95ee\u9898\uff0c\u8bf7\u8bbf\u95ee\uff1ahttps://www.home-assistant.io/integrations/braviatv\n\u786e\u4fdd\u7535\u89c6\u5df2\u6253\u5f00\u3002" } diff --git a/homeassistant/components/braviatv/translations/zh-Hant.json b/homeassistant/components/braviatv/translations/zh-Hant.json index c66ba705db1..6918cef5406 100644 --- a/homeassistant/components/braviatv/translations/zh-Hant.json +++ b/homeassistant/components/braviatv/translations/zh-Hant.json @@ -4,8 +4,7 @@ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_ip_control": "\u96fb\u8996\u4e0a\u7684 IP \u5df2\u95dc\u9589\u6216\u4e0d\u652f\u63f4\u6b64\u6b3e\u96fb\u8996\u3002", "not_bravia_device": "\u88dd\u7f6e\u4e26\u975e Bravia TV\u3002", - "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", - "reauth_unsuccessful": "\u91cd\u65b0\u9a57\u8b49\u5931\u6557\uff0c\u8acb\u79fb\u9664\u88dd\u7f6e\u4e26\u91cd\u65b0\u8a2d\u5b9a\u3002" + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -16,21 +15,27 @@ "step": { "authorize": { "data": { - "pin": "PIN \u78bc", "use_psk": "\u4f7f\u7528 PSK \u9a57\u8b49" }, - "description": "\u8f38\u5165 Sony Bravia \u96fb\u8996\u6240\u986f\u793a\u4e4b PIN \u78bc\u3002\n\n\u5047\u5982 PIN \u78bc\u672a\u986f\u793a\uff0c\u5fc5\u9808\u5148\u65bc\u96fb\u8996\u89e3\u9664 Home Assistant \u8a3b\u518a\uff0c\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u9060\u7aef\u88dd\u7f6e\u8a2d\u5b9a -> \u89e3\u9664\u9060\u7aef\u88dd\u7f6e\u8a3b\u518a\u3002\n\n\u53ef\u4f7f\u7528 PSK (Pre-Shared-Key) \u53d6\u4ee3 PIN \u78bc\u3002PSK \u70ba\u4f7f\u7528\u8005\u81ea\u5b9a\u5bc6\u9470\u7528\u4ee5\u5b58\u53d6\u63a7\u5236\u3002\u5efa\u8b70\u63a1\u7528\u6b64\u8a8d\u8b49\u65b9\u5f0f\u66f4\u70ba\u7a69\u5b9a\u3002\u6b32\u65bc\u96fb\u8996\u555f\u7528 PSK\u3002\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u5bb6\u5ead\u7db2\u8def\u8a2d\u5b9a -> IP \u63a7\u5236\u3002\u7136\u5f8c\u52fe\u9078 \u00ab\u4f7f\u7528 PSK \u8a8d\u8b49\u00bb \u4e26\u8f38\u5165 PSK \u78bc\u3002", + "description": "\u7f3a\u5b9a\u96fb\u8996\u4e0a\u7684 \u00ab\u9060\u7aef\u63a7\u5236\u00bb \u70ba\u958b\u555f\u72c0\u6cc1\u3002\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u9060\u7aef\u88dd\u7f6e\u8a2d\u5b9a -> \u9060\u7aef\u63a7\u5236\u3002 \n\n\u5171\u6709\u5169\u7a2e\u8a8d\u8b49\u65b9\u5f0f\uff1aPIN \u78bc\u6216 PSK\uff08\u9810\u7f6e\u5171\u4eab\u91d1\u9470\uff09\u3002 \n\u5efa\u8b70\u900f\u904e PSK \u8a8d\u8b49\u3001\u8f03\u70ba\u7a69\u5b9a\u3002", "title": "\u8a8d\u8b49 Sony Bravia \u96fb\u8996" }, "confirm": { "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" }, - "reauth_confirm": { + "pin": { "data": { - "pin": "PIN \u78bc", - "use_psk": "\u4f7f\u7528 PSK \u9a57\u8b49" + "pin": "PIN \u78bc" }, - "description": "\u8f38\u5165 Sony Bravia \u96fb\u8996\u6240\u986f\u793a\u4e4b PIN \u78bc\u3002\n\n\u5047\u5982 PIN \u78bc\u672a\u986f\u793a\uff0c\u5fc5\u9808\u5148\u65bc\u96fb\u8996\u89e3\u9664 Home Assistant \u8a3b\u518a\uff0c\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u9060\u7aef\u88dd\u7f6e\u8a2d\u5b9a -> \u89e3\u9664\u9060\u7aef\u88dd\u7f6e\u8a3b\u518a\u3002\n\n\u53ef\u4f7f\u7528 PSK (Pre-Shared-Key) \u53d6\u4ee3 PIN \u78bc\u3002PSK \u70ba\u4f7f\u7528\u8005\u81ea\u5b9a\u5bc6\u9470\u7528\u4ee5\u5b58\u53d6\u63a7\u5236\u3002\u5efa\u8b70\u63a1\u7528\u6b64\u8a8d\u8b49\u65b9\u5f0f\u66f4\u70ba\u7a69\u5b9a\u3002\u6b32\u65bc\u96fb\u8996\u555f\u7528 PSK\u3002\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u5bb6\u5ead\u7db2\u8def\u8a2d\u5b9a -> IP \u63a7\u5236\u3002\u7136\u5f8c\u52fe\u9078 \u00ab\u4f7f\u7528 PSK \u8a8d\u8b49\u00bb \u4e26\u8f38\u5165 PSK \u78bc\u3002" + "description": "\u8f38\u5165 Sony Bravia \u96fb\u8996\u6240\u986f\u793a\u4e4b PIN \u78bc\u3002\n\n\u5047\u5982 PIN \u78bc\u672a\u986f\u793a\uff0c\u5fc5\u9808\u5148\u65bc\u96fb\u8996\u89e3\u9664 Home Assistant \u8a3b\u518a\uff0c\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u9060\u7aef\u88dd\u7f6e\u8a2d\u5b9a -> \u89e3\u9664\u9060\u7aef\u88dd\u7f6e\u8a3b\u518a\u3002", + "title": "\u8a8d\u8b49 Sony Bravia \u96fb\u8996" + }, + "psk": { + "data": { + "pin": "PSK" + }, + "description": "\u6b32\u8a2d\u5b9a\u96fb\u8996 PSK\u3002\u6b65\u9a5f\u70ba\uff1a\u8a2d\u5b9a -> \u7db2\u8def -> \u5bb6\u5ead\u7db2\u8def\u8a2d\u5b9a -> IP \u63a7\u5236\u3002\u5c07 \u00ab\u8a8d\u8b49\u00bb \u8a2d\u5b9a\u70ba \u00ab\u4e00\u822c\u53ca\u9810\u7f6e\u5171\u4eab\u91d1\u9470\u00bb \u6216 \u00ab\u9810\u7f6e\u5171\u4eab\u91d1\u9470\u00bb \u4e26\u5b9a\u7fa9\u9810\u7f6e\u5171\u4eab\u91d1\u9470\u5b57\u4e32\uff08\u4f8b\u5982 Sony\uff09\u3002\n\n\u63a5\u8457\u65bc\u6b64\u8f38\u5165 PSK\u3002", + "title": "\u8a8d\u8b49 Sony Bravia \u96fb\u8996" }, "user": { "data": { diff --git a/homeassistant/components/broadlink/updater.py b/homeassistant/components/broadlink/updater.py index 2b98b757fbd..f3837c73263 100644 --- a/homeassistant/components/broadlink/updater.py +++ b/homeassistant/components/broadlink/updater.py @@ -76,17 +76,16 @@ class BroadlinkUpdateManager(ABC): ) raise UpdateFailed(err) from err - else: - if self.available is False: - _LOGGER.warning( - "Connected to %s (%s at %s)", - self.device.name, - self.device.api.model, - self.device.api.host[0], - ) - self.available = True - self.last_update = dt.utcnow() - return data + if self.available is False: + _LOGGER.warning( + "Connected to %s (%s at %s)", + self.device.name, + self.device.api.model, + self.device.api.host[0], + ) + self.available = True + self.last_update = dt.utcnow() + return data @abstractmethod async def async_fetch_data(self): diff --git a/homeassistant/components/brother/diagnostics.py b/homeassistant/components/brother/diagnostics.py index 239d4916d6b..4e2b64b4f56 100644 --- a/homeassistant/components/brother/diagnostics.py +++ b/homeassistant/components/brother/diagnostics.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import asdict +from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -12,7 +13,7 @@ from .const import DATA_CONFIG_ENTRY, DOMAIN async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: BrotherDataUpdateCoordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ config_entry.entry_id diff --git a/homeassistant/components/brother/translations/el.json b/homeassistant/components/brother/translations/el.json index e8c9e4ea8e7..4b24a921277 100644 --- a/homeassistant/components/brother/translations/el.json +++ b/homeassistant/components/brother/translations/el.json @@ -21,7 +21,7 @@ "data": { "type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c4\u03bf\u03c5 \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae" }, - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae Brother {model} \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\u03bd \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae {model} \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 \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae\u03c2 Brother" } } diff --git a/homeassistant/components/brother/translations/he.json b/homeassistant/components/brother/translations/he.json index af3f5750ddb..88d8630c57e 100644 --- a/homeassistant/components/brother/translations/he.json +++ b/homeassistant/components/brother/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", + "unsupported_model": "\u05d3\u05d2\u05dd \u05de\u05d3\u05e4\u05e1\u05ea \u05d6\u05d4 \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da." }, "error": { "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4", @@ -11,7 +12,8 @@ "step": { "user": { "data": { - "host": "\u05de\u05d0\u05e8\u05d7" + "host": "\u05de\u05d0\u05e8\u05d7", + "type": "\u05e1\u05d5\u05d2 \u05d4\u05de\u05d3\u05e4\u05e1\u05ea" } } } diff --git a/homeassistant/components/brother/translations/lv.json b/homeassistant/components/brother/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/brother/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/browser/__init__.py b/homeassistant/components/browser/__init__.py index f8c1278fbd9..954621ed66f 100644 --- a/homeassistant/components/browser/__init__.py +++ b/homeassistant/components/browser/__init__.py @@ -15,7 +15,7 @@ SERVICE_BROWSE_URL = "browse_url" SERVICE_BROWSE_URL_SCHEMA = vol.Schema( { - # pylint: disable=no-value-for-parameter + # pylint: disable-next=no-value-for-parameter vol.Required(ATTR_URL, default=ATTR_URL_DEFAULT): vol.Url() } ) diff --git a/homeassistant/components/brunt/__init__.py b/homeassistant/components/brunt/__init__.py index f189be63920..979b3f5b005 100644 --- a/homeassistant/components/brunt/__init__.py +++ b/homeassistant/components/brunt/__init__.py @@ -5,7 +5,7 @@ import logging from aiohttp.client_exceptions import ClientResponseError, ServerDisconnectedError import async_timeout -from brunt import BruntClientAsync +from brunt import BruntClientAsync, Thing from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"Brunt could not connect with username: {entry.data[CONF_USERNAME]}." ) from exc - async def async_update_data(): + async def async_update_data() -> dict[str | None, Thing]: """Fetch data from the Brunt endpoint for all Things. Error 403 is the API response for any kind of authentication error (failed password or email) @@ -54,6 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if err.status == 401: _LOGGER.warning("Device not found, will reload Brunt integration") await hass.config_entries.async_reload(entry.entry_id) + raise UpdateFailed from err coordinator = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index 489229622b2..36ee89e0395 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -42,7 +42,9 @@ async def async_setup_entry( ) -> None: """Set up the brunt platform.""" bapi: BruntClientAsync = hass.data[DOMAIN][entry.entry_id][DATA_BAPI] - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][DATA_COOR] + coordinator: DataUpdateCoordinator[dict[str | None, Thing]] = hass.data[DOMAIN][ + entry.entry_id + ][DATA_COOR] async_add_entities( BruntDevice(coordinator, serial, thing, bapi, entry.entry_id) @@ -50,7 +52,9 @@ async def async_setup_entry( ) -class BruntDevice(CoordinatorEntity, CoverEntity): +class BruntDevice( + CoordinatorEntity[DataUpdateCoordinator[dict[str | None, Thing]]], CoverEntity +): """ Representation of a Brunt cover device. @@ -65,8 +69,8 @@ class BruntDevice(CoordinatorEntity, CoverEntity): def __init__( self, - coordinator: DataUpdateCoordinator, - serial: str, + coordinator: DataUpdateCoordinator[dict[str | None, Thing]], + serial: str | None, thing: Thing, bapi: BruntClientAsync, entry_id: str, @@ -84,7 +88,7 @@ class BruntDevice(CoordinatorEntity, CoverEntity): self._attr_device_class = CoverDeviceClass.BLIND self._attr_attribution = ATTRIBUTION self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._attr_unique_id)}, + identifiers={(DOMAIN, self._attr_unique_id)}, # type: ignore[arg-type] name=self._attr_name, via_device=(DOMAIN, self._entry_id), manufacturer="Brunt", diff --git a/homeassistant/components/brunt/translations/uk.json b/homeassistant/components/brunt/translations/uk.json new file mode 100644 index 00000000000..e84d8bacfde --- /dev/null +++ b/homeassistant/components/brunt/translations/uk.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f: {username}" + }, + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/climate.py b/homeassistant/components/bsblan/climate.py index acf9ee25c57..fcff6a925e5 100644 --- a/homeassistant/components/bsblan/climate.py +++ b/homeassistant/components/bsblan/climate.py @@ -64,10 +64,11 @@ async def async_setup_entry( ) -class BSBLANClimate(BSBLANEntity, CoordinatorEntity, ClimateEntity): +class BSBLANClimate( + BSBLANEntity, CoordinatorEntity[DataUpdateCoordinator[State]], ClimateEntity +): """Defines a BSBLAN climate device.""" - coordinator: DataUpdateCoordinator[State] _attr_has_entity_name = True # Determine preset modes _attr_supported_features = ( @@ -80,7 +81,7 @@ class BSBLANClimate(BSBLANEntity, CoordinatorEntity, ClimateEntity): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[State], client: BSBLAN, device: Device, info: Info, diff --git a/homeassistant/components/bsblan/manifest.json b/homeassistant/components/bsblan/manifest.json index 810e78872f3..994af9dea11 100644 --- a/homeassistant/components/bsblan/manifest.json +++ b/homeassistant/components/bsblan/manifest.json @@ -3,7 +3,7 @@ "name": "BSB-Lan", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/bsblan", - "requirements": ["python-bsblan==0.5.8"], + "requirements": ["python-bsblan==0.5.9"], "codeowners": ["@liudger"], "iot_class": "local_polling", "loggers": ["bsblan"] diff --git a/homeassistant/components/bsblan/translations/lv.json b/homeassistant/components/bsblan/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/bsblan/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/binary_sensor.py b/homeassistant/components/bthome/binary_sensor.py index bbd77f271a4..ad36fe3644e 100644 --- a/homeassistant/components/bthome/binary_sensor.py +++ b/homeassistant/components/bthome/binary_sensor.py @@ -1,8 +1,6 @@ """Support for BTHome binary sensors.""" from __future__ import annotations -from typing import Optional - from bthome_ble import ( BinarySensorDeviceClass as BTHomeBinarySensorDeviceClass, SensorUpdate, @@ -188,7 +186,7 @@ async def async_setup_entry( class BTHomeBluetoothBinarySensorEntity( - PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[Optional[bool]]], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[bool | None]], BinarySensorEntity, ): """Representation of a BTHome binary sensor.""" diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index 1be63f5f486..8ca8f464b64 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -17,8 +17,8 @@ "service_data_uuid": "0000fcd2-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["bthome-ble==2.4.1"], - "dependencies": ["bluetooth"], + "requirements": ["bthome-ble==2.5.1"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@Ernst79"], "iot_class": "local_push" } diff --git a/homeassistant/components/bthome/sensor.py b/homeassistant/components/bthome/sensor.py index 219ce17c081..8cc8b10a67c 100644 --- a/homeassistant/components/bthome/sensor.py +++ b/homeassistant/components/bthome/sensor.py @@ -1,8 +1,6 @@ """Support for BTHome sensors.""" from __future__ import annotations -from typing import Optional, Union - from bthome_ble import SensorDeviceClass as BTHomeSensorDeviceClass, SensorUpdate, Units from homeassistant import config_entries @@ -332,9 +330,7 @@ async def async_setup_entry( class BTHomeBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a BTHome BLE sensor.""" diff --git a/homeassistant/components/bthome/translations/hu.json b/homeassistant/components/bthome/translations/hu.json index 11a8592dbe5..f4f028eeeae 100644 --- a/homeassistant/components/bthome/translations/hu.json +++ b/homeassistant/components/bthome/translations/hu.json @@ -7,7 +7,7 @@ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { - "decryption_failed": "A megadott kulcs nem m\u0171k\u00f6d\u00f6tt, az \u00e9rz\u00e9kel\u0151adatokat nem lehetett kiolvasni. K\u00e9rj\u00fck, ellen\u0151rizze \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", + "decryption_failed": "A megadott kulcs nem m\u0171k\u00f6d\u00f6tt, az \u00e9rz\u00e9kel\u0151adatokat nem lehetett kiolvasni. K\u00e9rem, ellen\u0151rizze \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", "expected_32_characters": "32 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g." }, "flow_title": "{name}", diff --git a/homeassistant/components/bthome/translations/lv.json b/homeassistant/components/bthome/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/bthome/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bthome/translations/tr.json b/homeassistant/components/bthome/translations/tr.json index 48b91fe6932..8804e2658b0 100644 --- a/homeassistant/components/bthome/translations/tr.json +++ b/homeassistant/components/bthome/translations/tr.json @@ -13,7 +13,7 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "get_encryption_key": { "data": { @@ -25,7 +25,7 @@ "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 3fe5aab59c8..ab3c47b9690 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import date, datetime, timedelta +from functools import partial import logging import re @@ -172,7 +173,13 @@ class WebDavCalendarData: """Get all events in a specific time frame.""" # Get event list from the current calendar vevent_list = await hass.async_add_executor_job( - self.calendar.date_search, start_date, end_date + partial( + self.calendar.search, + start=start_date, + end=end_date, + event=True, + expand=True, + ) ) event_list = [] for event in vevent_list: @@ -202,7 +209,12 @@ class WebDavCalendarData: # We have to retrieve the results for the whole day as the server # won't return events that have already started - results = self.calendar.date_search(start_of_today, start_of_tomorrow) + results = self.calendar.search( + start=start_of_today, + end=start_of_tomorrow, + event=True, + expand=True, + ) # Create new events for each recurrence of an event that happens today. # For recurring events, some servers return the original event with recurrence rules diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index dc34542dffa..9dbb2289f54 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -2,7 +2,7 @@ "domain": "caldav", "name": "CalDAV", "documentation": "https://www.home-assistant.io/integrations/caldav", - "requirements": ["caldav==0.9.1"], + "requirements": ["caldav==1.0.1"], "codeowners": [], "iot_class": "cloud_polling", "loggers": ["caldav", "vobject"] diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index 01c8d4fd5ed..876b90eac9b 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -19,7 +19,7 @@ from homeassistant.components.websocket_api import ERR_NOT_FOUND, ERR_NOT_SUPPOR from homeassistant.components.websocket_api.connection import ActiveConnection from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 @@ -37,15 +37,26 @@ from .const import ( CONF_EVENT, EVENT_DESCRIPTION, EVENT_END, + EVENT_END_DATE, + EVENT_END_DATETIME, + EVENT_IN, + EVENT_IN_DAYS, + EVENT_IN_WEEKS, EVENT_RECURRENCE_ID, EVENT_RECURRENCE_RANGE, EVENT_RRULE, EVENT_START, + EVENT_START_DATE, + EVENT_START_DATETIME, EVENT_SUMMARY, + EVENT_TIME_FIELDS, + EVENT_TYPES, EVENT_UID, CalendarEntityFeature, ) +# mypy: disallow-any-generics + _LOGGER = logging.getLogger(__name__) DOMAIN = "calendar" @@ -55,8 +66,39 @@ SCAN_INTERVAL = datetime.timedelta(seconds=60) # Don't support rrules more often than daily VALID_FREQS = {"DAILY", "WEEKLY", "MONTHLY", "YEARLY"} - -# mypy: disallow-any-generics +CREATE_EVENT_SERVICE = "create_event" +CREATE_EVENT_SCHEMA = vol.All( + cv.has_at_least_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN), + cv.has_at_most_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN), + cv.make_entity_service_schema( + { + vol.Required(EVENT_SUMMARY): cv.string, + vol.Optional(EVENT_DESCRIPTION, default=""): cv.string, + vol.Inclusive( + EVENT_START_DATE, "dates", "Start and end dates must both be specified" + ): cv.date, + vol.Inclusive( + EVENT_END_DATE, "dates", "Start and end dates must both be specified" + ): cv.date, + vol.Inclusive( + EVENT_START_DATETIME, + "datetimes", + "Start and end datetimes must both be specified", + ): cv.datetime, + vol.Inclusive( + EVENT_END_DATETIME, + "datetimes", + "Start and end datetimes must both be specified", + ): cv.datetime, + vol.Optional(EVENT_IN): vol.Schema( + { + vol.Exclusive(EVENT_IN_DAYS, EVENT_TYPES): cv.positive_int, + vol.Exclusive(EVENT_IN_WEEKS, EVENT_TYPES): cv.positive_int, + } + ), + }, + ), +) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -76,6 +118,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: websocket_api.async_register_command(hass, handle_calendar_event_delete) websocket_api.async_register_command(hass, handle_calendar_event_update) + component.async_register_entity_service( + CREATE_EVENT_SERVICE, + CREATE_EVENT_SCHEMA, + async_create_event, + required_features=[CalendarEntityFeature.CREATE_EVENT], + ) + await component.async_setup(config) return True @@ -569,3 +618,43 @@ async def handle_calendar_event_update( connection.send_error(msg["id"], "failed", str(ex)) else: connection.send_result(msg["id"]) + + +def _validate_timespan( + values: dict[str, Any] +) -> tuple[datetime.datetime | datetime.date, datetime.datetime | datetime.date]: + """Parse a create event service call and convert the args ofr a create event entity call. + + This converts the input service arguments into a `start` and `end` date or date time. This + exists because service calls use `start_date` and `start_date_time` whereas the + normal entity methods can take either a `datetim` or `date` as a single `start` argument. + It also handles the other service call variations like "in days" as well. + """ + + if event_in := values.get(EVENT_IN): + days = event_in.get(EVENT_IN_DAYS, 7 * event_in.get(EVENT_IN_WEEKS, 0)) + today = datetime.date.today() + return ( + today + datetime.timedelta(days=days), + today + datetime.timedelta(days=days + 1), + ) + + if EVENT_START_DATE in values and EVENT_END_DATE in values: + return (values[EVENT_START_DATE], values[EVENT_END_DATE]) + + if EVENT_START_DATETIME in values and EVENT_END_DATETIME in values: + return (values[EVENT_START_DATETIME], values[EVENT_END_DATETIME]) + + raise ValueError("Missing required fields to set start or end date/datetime") + + +async def async_create_event(entity: CalendarEntity, call: ServiceCall) -> None: + """Add a new event to calendar.""" + # Convert parameters to format used by async_create_event + (start, end) = _validate_timespan(call.data) + params = { + **{k: v for k, v in call.data.items() if k not in EVENT_TIME_FIELDS}, + EVENT_START: start, + EVENT_END: end, + } + await entity.async_create_event(**params) diff --git a/homeassistant/components/calendar/const.py b/homeassistant/components/calendar/const.py index 4a29a28d71d..aa47cb3592e 100644 --- a/homeassistant/components/calendar/const.py +++ b/homeassistant/components/calendar/const.py @@ -23,3 +23,20 @@ EVENT_LOCATION = "location" EVENT_RECURRENCE_ID = "recurrence_id" EVENT_RECURRENCE_RANGE = "recurrence_range" EVENT_RRULE = "rrule" + +# Service call fields +EVENT_START_DATE = "start_date" +EVENT_END_DATE = "end_date" +EVENT_START_DATETIME = "start_date_time" +EVENT_END_DATETIME = "end_date_time" +EVENT_IN = "in" +EVENT_IN_DAYS = "days" +EVENT_IN_WEEKS = "weeks" +EVENT_TIME_FIELDS = { + EVENT_START_DATE, + EVENT_END_DATE, + EVENT_START_DATETIME, + EVENT_END_DATETIME, + EVENT_IN, +} +EVENT_TYPES = "event_types" diff --git a/homeassistant/components/calendar/services.yaml b/homeassistant/components/calendar/services.yaml index 8e2958f7370..61a6ae1e0c8 100644 --- a/homeassistant/components/calendar/services.yaml +++ b/homeassistant/components/calendar/services.yaml @@ -1 +1,48 @@ -# Describes the format for available calendar services +create_event: + name: Create event + description: Add a new calendar event. + target: + entity: + domain: calendar + fields: + summary: + name: Summary + description: Defines the short summary or subject for the event + required: true + example: "Department Party" + selector: + text: + description: + name: Description + description: A more complete description of the event than that provided by the summary. + example: "Meeting to provide technical review for 'Phoenix' design." + selector: + text: + start_date_time: + name: Start time + description: The date and time the event should start. + example: "2022-03-22 20:00:00" + selector: + datetime: + end_date_time: + name: End time + description: The date and time the event should end. + example: "2022-03-22 22:00:00" + selector: + datetime: + start_date: + name: Start date + description: The date the all-day event should start. + example: "2022-03-22" + selector: + date: + end_date: + name: End date + description: The date the all-day event should end (exclusive). + example: "2022-03-23" + selector: + date: + in: + name: In + description: Days or weeks that you want to create the event in. + example: '"days": 2 or "weeks": 2' diff --git a/homeassistant/components/calendar/trigger.py b/homeassistant/components/calendar/trigger.py index 0fdb7259c9d..1e51c746e18 100644 --- a/homeassistant/components/calendar/trigger.py +++ b/homeassistant/components/calendar/trigger.py @@ -84,9 +84,10 @@ class CalendarEventListener: async def _fetch_events(self, last_endtime: datetime.datetime) -> None: """Update the set of eligible events.""" - # Use a sliding window for selecting in scope events in the next interval. The event - # search range is offset, then the fire time of the returned events are offset again below. - # Event time ranges are exclusive so the end time is expanded by 1sec + # Use a sliding window for selecting in scope events in the next interval. + # The event search range is offset, then the fire time of the returned events + # are offset again below. Event time ranges are exclusive so the end time + # is expanded by 1sec. start_time = last_endtime - self._offset end_time = start_time + UPDATE_INTERVAL + datetime.timedelta(seconds=1) _LOGGER.debug( diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index f329be16f1d..11e75c50cfc 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -12,7 +12,7 @@ from functools import partial import logging import os from random import SystemRandom -from typing import Any, Final, Optional, cast, final +from typing import Any, Final, cast, final from aiohttp import hdrs, web import async_timeout @@ -294,7 +294,7 @@ def _get_camera_from_entity_id(hass: HomeAssistant, entity_id: str) -> Camera: # stream_id: A unique id for the stream, used to update an existing source # The output is the SDP answer, or None if the source or offer is not eligible. # The Callable may throw HomeAssistantError on failure. -RtspToWebRtcProviderType = Callable[[str, str, str], Awaitable[Optional[str]]] +RtspToWebRtcProviderType = Callable[[str, str, str], Awaitable[str | None]] def async_register_rtsp_to_web_rtc_provider( @@ -747,8 +747,8 @@ class CameraImageView(CameraView): ) except (HomeAssistantError, ValueError) as ex: raise web.HTTPInternalServerError() from ex - else: - return web.Response(body=image.content, content_type=image.content_type) + + return web.Response(body=image.content, content_type=image.content_type) class CameraMjpegStream(CameraView): diff --git a/homeassistant/components/camera/img_util.py b/homeassistant/components/camera/img_util.py index 3aadc5c454c..cfa8399c4d5 100644 --- a/homeassistant/components/camera/img_util.py +++ b/homeassistant/components/camera/img_util.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Literal, cast SUPPORTED_SCALING_FACTORS = [(7, 8), (3, 4), (5, 8), (1, 2), (3, 8), (1, 4), (1, 8)] @@ -38,7 +38,10 @@ def find_supported_scaling_factor( def scale_jpeg_camera_image(cam_image: Image, width: int, height: int) -> bytes: - """Scale a camera image as close as possible to one of the supported scaling factors.""" + """Scale a camera image. + + Scale as close as possible to one of the supported scaling factors. + """ turbo_jpeg = TurboJPEGSingleton.instance() if not turbo_jpeg: return cam_image.content @@ -75,10 +78,10 @@ class TurboJPEGSingleton: seconds. """ - __instance = None + __instance: TurboJPEG | Literal[False] | None = None @staticmethod - def instance() -> TurboJPEG: + def instance() -> TurboJPEG | Literal[False] | None: """Singleton for TurboJPEG.""" if TurboJPEGSingleton.__instance is None: TurboJPEGSingleton() diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index f1bb0a0a840..28e4e1eeacb 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import asdict, dataclass -from typing import Final, Union, cast +from typing import Final, cast from homeassistant.components.stream import Orientation from homeassistant.core import HomeAssistant @@ -33,7 +33,7 @@ class CameraPreferences: self._hass = hass # The orientation prefs are stored in in the entity registry options # The preload_stream prefs are stored in this Store - self._store = Store[dict[str, dict[str, Union[bool, Orientation]]]]( + self._store = Store[dict[str, dict[str, bool | Orientation]]]( hass, STORAGE_VERSION, STORAGE_KEY ) self._dynamic_stream_settings_by_entity_id: dict[ diff --git a/homeassistant/components/camera/translations/lt.json b/homeassistant/components/camera/translations/lt.json index 1687427f7a1..8dc67be912f 100644 --- a/homeassistant/components/camera/translations/lt.json +++ b/homeassistant/components/camera/translations/lt.json @@ -1,9 +1,10 @@ { "state": { "_": { - "idle": "Laukimo re\u017eimas", + "idle": "Laukiama", "recording": "\u012era\u0161oma", "streaming": "Transliuojama" } - } + }, + "title": "Kamera" } \ No newline at end of file diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 0b77815ce42..90cb20a6c6c 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -1,7 +1,7 @@ """Support for Canary sensors.""" from __future__ import annotations -from typing import Final, Optional +from typing import Final from canary.model import Device, Location, SensorType @@ -20,9 +20,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER from .coordinator import CanaryDataUpdateCoordinator -SensorTypeItem = tuple[ - str, Optional[str], Optional[str], Optional[SensorDeviceClass], list[str] -] +SensorTypeItem = tuple[str, str | None, str | None, SensorDeviceClass | None, list[str]] SENSOR_VALUE_PRECISION: Final = 2 ATTR_AIR_QUALITY: Final = "air_quality" diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 467678ba82b..4d1c00f967b 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -57,7 +57,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Cast from a config entry.""" await home_assistant_cast.async_setup_ha_cast(hass, entry) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) hass.data[DOMAIN] = {"cast_platform": {}, "unknown_models": {}} await async_process_integration_platforms(hass, DOMAIN, _register_cast_platform) return True @@ -80,7 +80,8 @@ class CastProtocol(Protocol): ) -> BrowseMedia | None: """Browse media. - Return a BrowseMedia object or None if the media does not belong to this platform. + Return a BrowseMedia object or None if the media does not belong to + this platform. """ async def async_play_media( diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index 26759ca9606..c6a92c21fb4 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -5,7 +5,7 @@ import asyncio import configparser from dataclasses import dataclass import logging -from typing import Optional +from typing import TYPE_CHECKING from urllib.parse import urlparse import aiohttp @@ -20,6 +20,10 @@ from homeassistant.helpers import aiohttp_client from .const import DOMAIN +if TYPE_CHECKING: + from homeassistant.components import zeroconf + + _LOGGER = logging.getLogger(__name__) _PLS_SECTION_PLAYLIST = "playlist" @@ -33,7 +37,7 @@ class ChromecastInfo: """ cast_info: CastInfo = attr.ib() - is_dynamic_group = attr.ib(type=Optional[bool], default=None) + is_dynamic_group = attr.ib(type=bool | None, default=None) @property def friendly_name(self) -> str: @@ -59,7 +63,8 @@ class ChromecastInfo: if self.cast_info.cast_type is None or self.cast_info.manufacturer is None: unknown_models = hass.data[DOMAIN]["unknown_models"] if self.cast_info.model_name not in unknown_models: - # Manufacturer and cast type is not available in mDNS data, get it over http + # Manufacturer and cast type is not available in mDNS data, + # get it over HTTP cast_info = dial.get_cast_type( cast_info, zconf=ChromeCastZeroconf.get_zeroconf(), @@ -124,15 +129,15 @@ class ChromecastInfo: class ChromeCastZeroconf: """Class to hold a zeroconf instance.""" - __zconf = None + __zconf: zeroconf.HaZeroconf | None = None @classmethod - def set_zeroconf(cls, zconf): + def set_zeroconf(cls, zconf: zeroconf.HaZeroconf) -> None: """Set zeroconf.""" cls.__zconf = zconf @classmethod - def get_zeroconf(cls): + def get_zeroconf(cls) -> zeroconf.HaZeroconf | None: """Get zeroconf.""" return cls.__zconf diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index 14aa454c2e0..5eec2a28908 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -16,6 +16,7 @@ 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" +CAST_USER_NAME = "Home Assistant Cast" 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" @@ -34,7 +35,7 @@ async def async_setup_ha_cast( if user is None: user = await hass.auth.async_create_system_user( - "Home Assistant Cast", group_ids=[auth.const.GROUP_ID_ADMIN] + CAST_USER_NAME, group_ids=[auth.const.GROUP_ID_ADMIN] ) hass.config_entries.async_update_entry( entry, data={**entry.data, "user_id": user.id} @@ -55,7 +56,8 @@ async def async_setup_ha_cast( hass_uuid = await instance_id.async_get(hass) controller = HomeAssistantController( - # If you are developing Home Assistant Cast, uncomment and set to your dev app id. + # If you are developing Home Assistant Cast, uncomment and set to + # your dev app id. # app_id="5FE44367", hass_url=hass_url, hass_uuid=hass_uuid, diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index b18c2ccb133..5791fb6b8b9 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -375,7 +375,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): tts_base_url = None url_description = "" if "tts" in self.hass.config.components: - # pylint: disable=[import-outside-toplevel] + # pylint: disable-next=[import-outside-toplevel] from homeassistant.components import tts with suppress(KeyError): # base_url not configured diff --git a/homeassistant/components/cast/translations/tr.json b/homeassistant/components/cast/translations/tr.json index 3a2609c302a..6e121da0ab0 100644 --- a/homeassistant/components/cast/translations/tr.json +++ b/homeassistant/components/cast/translations/tr.json @@ -15,7 +15,7 @@ "title": "Google Cast yap\u0131land\u0131rmas\u0131" }, "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } }, diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 85f73532fed..5f6152b7bc7 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import datetime, timedelta import logging -from typing import Optional from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -60,7 +59,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) -class CertExpiryDataUpdateCoordinator(DataUpdateCoordinator[Optional[datetime]]): +class CertExpiryDataUpdateCoordinator(DataUpdateCoordinator[datetime | None]): """Class to manage fetching Cert Expiry data from single endpoint.""" def __init__(self, hass, host, port): diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 0c1b6116cdd..56bcf07a3bb 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -63,7 +63,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Add cert-expiry entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: CertExpiryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] sensors = [ SSLCertificateTimestamp(coordinator), @@ -72,7 +72,7 @@ async def async_setup_entry( async_add_entities(sensors, True) -class CertExpiryEntity(CoordinatorEntity): +class CertExpiryEntity(CoordinatorEntity[CertExpiryDataUpdateCoordinator]): """Defines a base Cert Expiry entity.""" _attr_icon = "mdi:certificate" diff --git a/homeassistant/components/climacell/translations/sk.json b/homeassistant/components/climacell/translations/sk.json index 954b33e8139..61beb048dd1 100644 --- a/homeassistant/components/climacell/translations/sk.json +++ b/homeassistant/components/climacell/translations/sk.json @@ -5,7 +5,7 @@ "data": { "timestep": "Min. Medzi predpove\u010fami NowCast" }, - "description": "Ak sa rozhodnete povoli\u0165 entitu progn\u00f3zy \u201enowcast\u201c, m\u00f4\u017eete nakonfigurova\u0165 po\u010det min\u00fat medzi jednotliv\u00fdmi progn\u00f3zami. Po\u010det poskytnut\u00fdch predpoved\u00ed z\u00e1vis\u00ed od po\u010dtu min\u00fat vybrat\u00fdch medzi predpove\u010fami.", + "description": "Ak sa rozhodnete povoli\u0165 entitu progn\u00f3zy `nowcast`, m\u00f4\u017eete nakonfigurova\u0165 po\u010det min\u00fat medzi jednotliv\u00fdmi progn\u00f3zami. Po\u010det poskytnut\u00fdch predpoved\u00ed z\u00e1vis\u00ed od po\u010dtu min\u00fat vybrat\u00fdch medzi predpove\u010fami.", "title": "Aktualizujte mo\u017enosti ClimaCell" } } diff --git a/homeassistant/components/climate/translations/bg.json b/homeassistant/components/climate/translations/bg.json index a798e05aa07..20cd4ddafe5 100644 --- a/homeassistant/components/climate/translations/bg.json +++ b/homeassistant/components/climate/translations/bg.json @@ -48,6 +48,9 @@ "fan_modes": { "name": "\u0420\u0435\u0436\u0438\u043c\u0438 \u043d\u0430 \u0432\u0435\u043d\u0442\u0438\u043b\u0430\u0442\u043e\u0440\u0430" }, + "humidity": { + "name": "\u0416\u0435\u043b\u0430\u043d\u0430 \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442" + }, "hvac_action": { "state": { "cooling": "\u041e\u0445\u043b\u0430\u0436\u0434\u0430\u043d\u0435", @@ -60,6 +63,18 @@ "hvac_modes": { "name": "HVAC \u0440\u0435\u0436\u0438\u043c\u0438" }, + "max_humidity": { + "name": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u043d\u0430 \u0436\u0435\u043b\u0430\u043d\u0430 \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442" + }, + "max_temp": { + "name": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u043d\u0430 \u0436\u0435\u043b\u0430\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430" + }, + "min_humidity": { + "name": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u043d\u0430 \u0436\u0435\u043b\u0430\u043d\u0430 \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442" + }, + "min_temp": { + "name": "\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u043d\u0430 \u0436\u0435\u043b\u0430\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430" + }, "swing_mode": { "name": "\u0420\u0435\u0436\u0438\u043c \u043d\u0430 \u043b\u044e\u043b\u0435\u0435\u043d\u0435", "state": { @@ -72,6 +87,18 @@ }, "swing_modes": { "name": "\u0420\u0435\u0436\u0438\u043c\u0438 \u043d\u0430 \u043b\u044e\u043b\u0435\u0435\u043d\u0435" + }, + "target_temp_high": { + "name": "\u0413\u043e\u0440\u043d\u0430 \u0436\u0435\u043b\u0430\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430" + }, + "target_temp_low": { + "name": "\u0414\u043e\u043b\u043d\u0430 \u0436\u0435\u043b\u0430\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430" + }, + "target_temp_step": { + "name": "\u0421\u0442\u044a\u043f\u043a\u0430 \u043d\u0430 \u0436\u0435\u043b\u0430\u043d\u0430\u0442\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430" + }, + "temperature": { + "name": "\u0416\u0435\u043b\u0430\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430" } } }, diff --git a/homeassistant/components/climate/translations/ca.json b/homeassistant/components/climate/translations/ca.json index 8a8e6d074b2..71377678bbb 100644 --- a/homeassistant/components/climate/translations/ca.json +++ b/homeassistant/components/climate/translations/ca.json @@ -41,7 +41,7 @@ "state": { "auto": "Autom\u00e0tic", "diffuse": "Dif\u00fas", - "focus": "Enfocament", + "focus": "Enfocat", "high": "Alt", "low": "Baix", "medium": "Mitj\u00e0", @@ -65,17 +65,17 @@ "fan": "Ventilador", "heating": "Escalfant", "idle": "Inactiu", - "off": "OFF" + "off": "Apagat" } }, "hvac_modes": { "name": "Modes HVAC" }, "max_humidity": { - "name": "Humitat m\u00e0xima objectiu" + "name": "Humitat objectiu m\u00e0xima" }, "max_temp": { - "name": "Temperatura m\u00e0xima objectiu" + "name": "Temperatura objectiu m\u00e0xima" }, "min_humidity": { "name": "Humitat objectiu m\u00ednima" diff --git a/homeassistant/components/climate/translations/cs.json b/homeassistant/components/climate/translations/cs.json index 276ef1a7d70..cf264e627e2 100644 --- a/homeassistant/components/climate/translations/cs.json +++ b/homeassistant/components/climate/translations/cs.json @@ -2,11 +2,11 @@ "device_automation": { "action_type": { "set_hvac_mode": "Zm\u011bnit re\u017eim HVAC na {entity_name}", - "set_preset_mode": "Zm\u011bnit p\u0159ednastaven\u00fd re\u017eim na {entity_name}" + "set_preset_mode": "Zm\u011bnit p\u0159edvolbu na {entity_name}" }, "condition_type": { "is_hvac_mode": "{entity_name} je nastaveno na ur\u010dit\u00fd re\u017eim HVAC", - "is_preset_mode": "{entity_name} je nastaveno na p\u0159ednastaven\u00fd re\u017eim" + "is_preset_mode": "{entity_name} je nastaveno na p\u0159edvolbu" }, "trigger_type": { "current_humidity_changed": "M\u011b\u0159en\u00e1 vlhkost na {entity_name} zm\u011bn\u011bna", @@ -25,5 +25,96 @@ "off": "Vypnuto" } }, + "state_attributes": { + "_": { + "current_humidity": { + "name": "Aktu\u00e1ln\u00ed vlhkost" + }, + "current_temperature": { + "name": "Aktu\u00e1ln\u00ed teplota" + }, + "fan_mode": { + "name": "Re\u017eim ventil\u00e1toru", + "state": { + "auto": "Auto", + "off": "Vypnuto", + "on": "Zapnuto" + } + }, + "fan_modes": { + "name": "Re\u017eimy ventil\u00e1toru" + }, + "humidity": { + "name": "C\u00edlov\u00e1 vlhkost" + }, + "hvac_action": { + "name": "Aktu\u00e1ln\u00ed akce", + "state": { + "cooling": "Chlazen\u00ed", + "drying": "Vysou\u0161en\u00ed", + "fan": "V\u011btr\u00e1n\u00ed", + "heating": "Vyt\u00e1p\u011bn\u00ed", + "idle": "Ne\u010dinn\u00fd", + "off": "Vypnuto" + } + }, + "hvac_modes": { + "name": "Re\u017eimy HVAC" + }, + "max_humidity": { + "name": "Maxim\u00e1ln\u00ed c\u00edlov\u00e1 vlhkost" + }, + "max_temp": { + "name": "Maxim\u00e1ln\u00ed c\u00edlov\u00e1 teplota" + }, + "min_humidity": { + "name": "Minim\u00e1ln\u00ed c\u00edlov\u00e1 vlhkost" + }, + "min_temp": { + "name": "Minim\u00e1ln\u00ed c\u00edlov\u00e1 teplota" + }, + "preset_mode": { + "name": "P\u0159edvolba", + "state": { + "activity": "Aktivita", + "away": "Pry\u010d", + "boost": "Boost", + "comfort": "Komfort", + "eco": "Eko", + "home": "Doma", + "none": "\u017d\u00e1dn\u00e1", + "sleep": "Sp\u00e1nek" + } + }, + "preset_modes": { + "name": "P\u0159edvolby" + }, + "swing_mode": { + "name": "Re\u017eim kmit\u00e1n\u00ed", + "state": { + "both": "Oba", + "horizontal": "Horizont\u00e1ln\u00ed", + "off": "Vypnuto", + "on": "Zapnuto", + "vertical": "Vertik\u00e1ln\u00ed" + } + }, + "swing_modes": { + "name": "Re\u017eimy kmit\u00e1n\u00ed" + }, + "target_temp_high": { + "name": "Horn\u00ed c\u00edlov\u00e1 teplota" + }, + "target_temp_low": { + "name": "Doln\u00ed c\u00edlov\u00e1 teplota" + }, + "target_temp_step": { + "name": "Krok c\u00edlov\u00e9 teploty" + }, + "temperature": { + "name": "C\u00edlov\u00e1 teplota" + } + } + }, "title": "Klima" } \ No newline at end of file diff --git a/homeassistant/components/climate/translations/fr.json b/homeassistant/components/climate/translations/fr.json index 58de5fc4e76..7370a246823 100644 --- a/homeassistant/components/climate/translations/fr.json +++ b/homeassistant/components/climate/translations/fr.json @@ -25,5 +25,22 @@ "off": "D\u00e9sactiv\u00e9" } }, + "state_attributes": { + "_": { + "hvac_action": { + "state": { + "heating": "Chauffe", + "idle": "Inactif", + "off": "Arr\u00eat" + } + }, + "preset_mode": { + "state": { + "away": "Absent", + "comfort": "Confort" + } + } + } + }, "title": "Thermostat" } \ No newline at end of file diff --git a/homeassistant/components/climate/translations/it.json b/homeassistant/components/climate/translations/it.json index 1abe9345a43..fb2edb203f0 100644 --- a/homeassistant/components/climate/translations/it.json +++ b/homeassistant/components/climate/translations/it.json @@ -2,7 +2,7 @@ "device_automation": { "action_type": { "set_hvac_mode": "Cambia modalit\u00e0 HVAC su {entity_name}", - "set_preset_mode": "Modifica preimpostazione su {entity_name}" + "set_preset_mode": "Modifica modalit\u00e0 su {entity_name}" }, "condition_type": { "is_hvac_mode": "{entity_name} \u00e8 impostato su una modalit\u00e0 HVAC specifica", @@ -97,7 +97,7 @@ } }, "preset_modes": { - "name": "Preimpostazioni" + "name": "Modalit\u00e0" }, "swing_mode": { "name": "Modalit\u00e0 di oscillazione", @@ -122,7 +122,7 @@ "name": "passo di temperatura obiettivo" }, "temperature": { - "name": "Temperatura obiettivo" + "name": "Temperatura desiderata" } } }, diff --git a/homeassistant/components/climate/translations/lt.json b/homeassistant/components/climate/translations/lt.json index d9a5d057d40..fe09cfc5642 100644 --- a/homeassistant/components/climate/translations/lt.json +++ b/homeassistant/components/climate/translations/lt.json @@ -1,11 +1,114 @@ { "state": { "_": { + "auto": "Auto", "cool": "V\u0117sina", "dry": "D\u017eiovina", + "fan_only": "Tik ventiliatorius", "heat": "\u0160ildo", "heat_cool": "\u0160ildo/V\u0117sina", "off": "I\u0161jungta" } - } + }, + "state_attributes": { + "_": { + "aux_heat": { + "name": "I\u0161orinis \u0161ildymas" + }, + "current_humidity": { + "name": "Dabartin\u0117 dr\u0117gm\u0117" + }, + "current_temperature": { + "name": "Dabartin\u0117 temperat\u016bra" + }, + "fan_mode": { + "name": "Ventiliatoriaus re\u017eimas", + "state": { + "auto": "Auto", + "diffuse": "Difuzinis", + "focus": "Fokusuotas", + "high": "Auk\u0161tas", + "low": "\u017demas", + "medium": "Vidutinis", + "middle": "Vidurinis", + "off": "I\u0161jungta", + "on": "\u012ejungta", + "top": "Vir\u0161uje" + } + }, + "fan_modes": { + "name": "Ventiliatoriaus re\u017eimai" + }, + "humidity": { + "name": "Tikslin\u0117 dr\u0117gm\u0117" + }, + "hvac_action": { + "name": "Dabartinis veiksmas", + "state": { + "drying": "D\u017eiovinama", + "fan": "Ventiliatorius", + "heating": "\u0160ildoma", + "idle": "Laukiama", + "off": "I\u0161jungta" + } + }, + "hvac_modes": { + "name": "\u0160VOK re\u017eimai" + }, + "max_humidity": { + "name": "Maksimali tikslin\u0117 dr\u0117gm\u0117" + }, + "max_temp": { + "name": "Maksimali tikslin\u0117 temperat\u016bra" + }, + "min_humidity": { + "name": "Minimali tikslin\u0117 dr\u0117gm\u0117" + }, + "min_temp": { + "name": "Minimali tikslin\u0117 temperat\u016bra" + }, + "preset_mode": { + "name": "I\u0161 anksto nustatytas", + "state": { + "activity": "Veikla", + "away": "I\u0161vyk\u0119s", + "boost": "Laikinai padidinti", + "comfort": "Komfortas", + "eco": "Eko", + "home": "Namuose", + "none": "Joks", + "sleep": "Miego" + } + }, + "preset_modes": { + "name": "I\u0161 anksto nustatyti" + }, + "swing_mode": { + "name": "Sukiojimo re\u017eimas", + "state": { + "both": "Abu", + "horizontal": "Horizontali", + "off": "I\u0161jungta", + "on": "\u012ejungta", + "vertical": "Vertikalus" + } + }, + "swing_modes": { + "name": "P\u016btimo re\u017eimai" + }, + "target_temp_high": { + "name": "Auk\u0161\u010diausia tikslin\u0117 temperat\u016bra" + }, + "target_temp_low": { + "name": "\u017demutin\u0117 tikslin\u0117 temperat\u016bra" + }, + "target_temp_step": { + "name": "Tikslin\u0117s temperat\u016bros \u017eingsnis" + }, + "temperature": { + "name": "Tikslin\u0117 temperat\u016bra" + } + } + }, + "title": "Termostatas" } \ No newline at end of file diff --git a/homeassistant/components/climate/translations/tr.json b/homeassistant/components/climate/translations/tr.json index 3e175e6f598..c982014127c 100644 --- a/homeassistant/components/climate/translations/tr.json +++ b/homeassistant/components/climate/translations/tr.json @@ -25,5 +25,106 @@ "off": "Kapal\u0131" } }, + "state_attributes": { + "_": { + "aux_heat": { + "name": "Yard\u0131mc\u0131 \u0131s\u0131" + }, + "current_humidity": { + "name": "Mevcut nem" + }, + "current_temperature": { + "name": "Mevcut s\u0131cakl\u0131k" + }, + "fan_mode": { + "name": "Fan modu", + "state": { + "auto": "Otomatik", + "diffuse": "Da\u011f\u0131n\u0131k", + "focus": "Odak", + "high": "Y\u00fcksek", + "low": "D\u00fc\u015f\u00fck", + "medium": "Orta", + "middle": "Orta", + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k", + "top": "Yukar\u0131" + } + }, + "fan_modes": { + "name": "Fan modlar\u0131" + }, + "humidity": { + "name": "Hedef nem" + }, + "hvac_action": { + "name": "Mevcut eylem", + "state": { + "cooling": "So\u011futuluyor", + "drying": "Kurutuluyor", + "fan": "Fan", + "heating": "Is\u0131t\u0131l\u0131yor", + "idle": "Bo\u015fta", + "off": "Kapal\u0131" + } + }, + "hvac_modes": { + "name": "HVAC modlar\u0131" + }, + "max_humidity": { + "name": "Maksimum hedef nem" + }, + "max_temp": { + "name": "Maksimum hedef s\u0131cakl\u0131k" + }, + "min_humidity": { + "name": "Minimum hedef nem" + }, + "min_temp": { + "name": "Minimum hedef s\u0131cakl\u0131k" + }, + "preset_mode": { + "name": "\u00d6n ayar", + "state": { + "activity": "Aktivite", + "away": "D\u0131\u015far\u0131da", + "boost": "G\u00fc\u00e7l\u00fc", + "comfort": "Konfor", + "eco": "Eko", + "home": "Evde", + "none": "Hi\u00e7biri", + "sleep": "Uyku" + } + }, + "preset_modes": { + "name": "\u00d6n Ayarlar" + }, + "swing_mode": { + "name": "Sal\u0131n\u0131m modu", + "state": { + "both": "\u00c7ift", + "horizontal": "Yatay", + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k", + "vertical": "Dikey" + } + }, + "swing_modes": { + "name": "Sal\u0131n\u0131m modlar\u0131" + }, + "target_temp_high": { + "name": "\u00dcst hedef s\u0131cakl\u0131k" + }, + "target_temp_low": { + "name": "Daha d\u00fc\u015f\u00fck hedef s\u0131cakl\u0131k" + }, + "target_temp_step": { + "name": "Hedef s\u0131cakl\u0131k ad\u0131m\u0131" + }, + "temperature": { + "name": "Hedef s\u0131cakl\u0131k" + } + } + }, "title": "\u0130klimlendirme" } \ No newline at end of file diff --git a/homeassistant/components/climate/translations/uk.json b/homeassistant/components/climate/translations/uk.json index de6baff021c..6d9e0485bc5 100644 --- a/homeassistant/components/climate/translations/uk.json +++ b/homeassistant/components/climate/translations/uk.json @@ -25,5 +25,14 @@ "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e" } }, + "state_attributes": { + "_": { + "hvac_action": { + "state": { + "idle": "\u0411\u0435\u0437\u0434\u0456\u044f\u043b\u044c\u043d\u0456\u0441\u0442\u044c" + } + } + } + }, "title": "\u041a\u043b\u0456\u043c\u0430\u0442" } \ No newline at end of file diff --git a/homeassistant/components/climate/translations/zh-Hans.json b/homeassistant/components/climate/translations/zh-Hans.json index a93125525e1..eb3e4bb1799 100644 --- a/homeassistant/components/climate/translations/zh-Hans.json +++ b/homeassistant/components/climate/translations/zh-Hans.json @@ -25,5 +25,67 @@ "off": "\u5173" } }, + "state_attributes": { + "_": { + "aux_heat": { + "name": "\u8f85\u70ed" + }, + "current_humidity": { + "name": "\u5f53\u524d\u6e7f\u5ea6" + }, + "current_temperature": { + "name": "\u5f53\u524d\u6e29\u5ea6" + }, + "fan_mode": { + "name": "\u98ce\u901f", + "state": { + "auto": "\u81ea\u52a8", + "high": "\u9ad8", + "low": "\u4f4e", + "medium": "\u4e2d", + "middle": "\u4e2d\u95f4", + "off": "\u5173", + "on": "\u5f00" + } + }, + "fan_modes": { + "name": "\u98ce\u901f\u5217\u8868" + }, + "humidity": { + "name": "\u8bbe\u5b9a\u6e7f\u5ea6" + }, + "hvac_action": { + "name": "\u5f53\u524d\u52a8\u4f5c", + "state": { + "cooling": "\u5236\u51b7\u4e2d", + "drying": "\u9664\u6e7f\u4e2d", + "fan": "\u9001\u98ce\u4e2d", + "heating": "\u5236\u70ed\u4e2d", + "idle": "\u5f85\u673a", + "off": "\u5173\u95ed" + } + }, + "hvac_modes": { + "name": "\u7a7a\u8c03\u6a21\u5f0f" + }, + "preset_mode": { + "name": "\u9884\u8bbe\u6a21\u5f0f", + "state": { + "away": "\u79bb\u5f00", + "boost": "\u5f3a\u52b2", + "comfort": "\u8212\u9002", + "eco": "\u8282\u80fd", + "none": "\u65e0", + "sleep": "\u7761\u7720" + } + }, + "preset_modes": { + "name": "\u9884\u8bbe\u5217\u8868" + }, + "swing_mode": { + "name": "\u626b\u98ce\u6a21\u5f0f" + } + } + }, "title": "\u7a7a\u8c03" } \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/hu.json b/homeassistant/components/cloud/translations/hu.json index 83d1e4915d1..53886f8ac2d 100644 --- a/homeassistant/components/cloud/translations/hu.json +++ b/homeassistant/components/cloud/translations/hu.json @@ -3,7 +3,7 @@ "legacy_subscription": { "fix_flow": { "abort": { - "operation_took_too_long": "A m\u0171velet t\u00fal sok\u00e1ig tartott. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra." + "operation_took_too_long": "A m\u0171velet t\u00fal sok\u00e1ig tartott. K\u00e9rem, pr\u00f3b\u00e1lja meg k\u00e9s\u0151bb \u00fajra." }, "step": { "confirm_change_plan": { diff --git a/homeassistant/components/cloud/translations/tr.json b/homeassistant/components/cloud/translations/tr.json index 5f3edb5d3f1..d29f337ba10 100644 --- a/homeassistant/components/cloud/translations/tr.json +++ b/homeassistant/components/cloud/translations/tr.json @@ -1,4 +1,19 @@ { + "issues": { + "legacy_subscription": { + "fix_flow": { + "abort": { + "operation_took_too_long": "Operasyon \u00e7ok uzun s\u00fcrd\u00fc. L\u00fctfen daha sonra tekrar deneyiniz." + }, + "step": { + "confirm_change_plan": { + "description": "Yak\u0131n zamanda abonelik sistemimizi g\u00fcncelledik. Home Assistant Cloud'u kullanmaya devam etmek i\u00e7in PayPal'da de\u011fi\u015fikli\u011fi bir defaya mahsus onaylaman\u0131z gerekir. \n\n Bu i\u015flem 1 dakika s\u00fcrer ve fiyat\u0131 art\u0131rmaz." + } + } + }, + "title": "Eski abonelik alg\u0131land\u0131" + } + }, "system_health": { "info": { "alexa_enabled": "Alexa Etkin", diff --git a/homeassistant/components/co2signal/__init__.py b/homeassistant/components/co2signal/__init__.py index a5bae23332d..721a26e147f 100644 --- a/homeassistant/components/co2signal/__init__.py +++ b/homeassistant/components/co2signal/__init__.py @@ -131,12 +131,11 @@ def get_data(hass: HomeAssistant, config: Mapping[str, Any]) -> CO2SignalRespons _LOGGER.exception("Unexpected exception") raise UnknownError from err - else: - if "error" in data: - raise UnknownError(data["error"]) + if "error" in data: + raise UnknownError(data["error"]) - if data.get("status") != "ok": - _LOGGER.exception("Unexpected response: %s", data) - raise UnknownError + if data.get("status") != "ok": + _LOGGER.exception("Unexpected response: %s", data) + raise UnknownError return cast(CO2SignalResponse, data) diff --git a/homeassistant/components/co2signal/diagnostics.py b/homeassistant/components/co2signal/diagnostics.py index 24f47f96da0..8ab09b8cb75 100644 --- a/homeassistant/components/co2signal/diagnostics.py +++ b/homeassistant/components/co2signal/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for CO2Signal.""" 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_API_KEY @@ -13,7 +15,7 @@ TO_REDACT = {CONF_API_KEY} async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: CO2SignalCoordinator = hass.data[DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/co2signal/translations/lv.json b/homeassistant/components/co2signal/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/co2signal/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coinbase/diagnostics.py b/homeassistant/components/coinbase/diagnostics.py index 54d4168776c..674ce9dca28 100644 --- a/homeassistant/components/coinbase/diagnostics.py +++ b/homeassistant/components/coinbase/diagnostics.py @@ -1,5 +1,7 @@ """Diagnostics support for Coinbase.""" +from typing import Any + from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_ID @@ -20,7 +22,7 @@ TO_REDACT = { async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" instance: CoinbaseData = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/coinbase/translations/lv.json b/homeassistant/components/coinbase/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/coinbase/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/comfoconnect/__init__.py b/homeassistant/components/comfoconnect/__init__.py index 9c9b4aad711..5ff34526cc0 100644 --- a/homeassistant/components/comfoconnect/__init__.py +++ b/homeassistant/components/comfoconnect/__init__.py @@ -31,8 +31,6 @@ DEFAULT_PIN = 0 DEFAULT_TOKEN = "00000000000000000000000000000001" DEFAULT_USER_AGENT = "Home Assistant" -DEVICE = None - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index 5341a5f6925..3f00a9b59f0 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -63,7 +63,7 @@ class ComfoConnectFan(FanEntity): _attr_should_poll = False _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE _attr_preset_modes = PRESET_MODES - current_speed = None + current_speed: float | None = None def __init__(self, ccb: ComfoConnectBridge) -> None: """Initialize the ComfoConnect fan.""" diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 42d2386977f..74c15da2f00 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -1,22 +1,17 @@ """HTTP views to interact with the device registry.""" from __future__ import annotations -from typing import Any +from typing import Any, cast import voluptuous as vol from homeassistant import loader from homeassistant.components import websocket_api from homeassistant.components.websocket_api.decorators import require_admin -from homeassistant.components.websocket_api.messages import ( - IDEN_JSON_TEMPLATE, - IDEN_TEMPLATE, - message_to_json, -) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import ( - EVENT_DEVICE_REGISTRY_UPDATED, + DeviceEntry, DeviceEntryDisabler, async_get, ) @@ -25,44 +20,6 @@ from homeassistant.helpers.device_registry import ( async def async_setup(hass): """Enable the Device Registry views.""" - cached_list_devices: str | None = None - - @callback - def _async_clear_list_device_cache(event: Event) -> None: - nonlocal cached_list_devices - cached_list_devices = None - - @callback - @websocket_api.websocket_command( - { - vol.Required("type"): "config/device_registry/list", - } - ) - def websocket_list_devices( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], - ) -> None: - """Handle list devices command.""" - nonlocal cached_list_devices - if not cached_list_devices: - registry = async_get(hass) - cached_list_devices = message_to_json( - websocket_api.result_message( - IDEN_TEMPLATE, # type: ignore[arg-type] - [_entry_dict(entry) for entry in registry.devices.values()], - ) - ) - connection.send_message( - cached_list_devices.replace(IDEN_JSON_TEMPLATE, str(msg["id"]), 1) - ) - - hass.bus.async_listen( - EVENT_DEVICE_REGISTRY_UPDATED, - _async_clear_list_device_cache, - run_immediately=True, - ) - websocket_api.async_register_command(hass, websocket_list_devices) websocket_api.async_register_command(hass, websocket_update_device) websocket_api.async_register_command( @@ -71,6 +28,37 @@ async def async_setup(hass): return True +@callback +@websocket_api.websocket_command( + { + vol.Required("type"): "config/device_registry/list", + } +) +def websocket_list_devices( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Handle list devices command.""" + registry = async_get(hass) + # Build start of response message + msg_json_prefix = ( + f'{{"id":{msg["id"]},"type": "{websocket_api.const.TYPE_RESULT}",' + f'"success":true,"result": [' + ) + # Concatenate cached entity registry item JSON serializations + msg_json = ( + msg_json_prefix + + ",".join( + entry.json_repr + for entry in registry.devices.values() + if entry.json_repr is not None + ) + + "]}" + ) + connection.send_message(msg_json) + + @require_admin @websocket_api.websocket_command( { @@ -98,9 +86,9 @@ def websocket_update_device( if msg.get("disabled_by") is not None: msg["disabled_by"] = DeviceEntryDisabler(msg["disabled_by"]) - entry = registry.async_update_device(**msg) + entry = cast(DeviceEntry, registry.async_update_device(**msg)) - connection.send_message(websocket_api.result_message(msg_id, _entry_dict(entry))) + connection.send_message(websocket_api.result_message(msg_id, entry.dict_repr)) @websocket_api.require_admin @@ -151,28 +139,6 @@ async def websocket_remove_config_entry_from_device( device_id, remove_config_entry_id=config_entry_id ) - entry_as_dict = _entry_dict(entry) if entry else None + entry_as_dict = entry.dict_repr 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.""" - return { - "area_id": entry.area_id, - "configuration_url": entry.configuration_url, - "config_entries": list(entry.config_entries), - "connections": list(entry.connections), - "disabled_by": entry.disabled_by, - "entry_type": entry.entry_type, - "hw_version": entry.hw_version, - "id": entry.id, - "identifiers": list(entry.identifiers), - "manufacturer": entry.manufacturer, - "model": entry.model, - "name_by_user": entry.name_by_user, - "name": entry.name, - "sw_version": entry.sw_version, - "via_device_id": entry.via_device_id, - } diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index dffb44c8153..43e433a4f36 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -9,12 +9,7 @@ from homeassistant import config_entries from homeassistant.components import websocket_api from homeassistant.components.websocket_api import ERR_NOT_FOUND from homeassistant.components.websocket_api.decorators import require_admin -from homeassistant.components.websocket_api.messages import ( - IDEN_JSON_TEMPLATE, - IDEN_TEMPLATE, - message_to_json, -) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( config_validation as cv, device_registry as dr, @@ -25,48 +20,41 @@ from homeassistant.helpers import ( async def async_setup(hass: HomeAssistant) -> bool: """Enable the Entity Registry views.""" - cached_list_entities: str | None = None - - @callback - def _async_clear_list_entities_cache(event: Event) -> None: - nonlocal cached_list_entities - cached_list_entities = None - - @websocket_api.websocket_command( - {vol.Required("type"): "config/entity_registry/list"} - ) - @callback - def websocket_list_entities( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], - ) -> None: - """Handle list registry entries command.""" - nonlocal cached_list_entities - if not cached_list_entities: - registry = er.async_get(hass) - cached_list_entities = message_to_json( - websocket_api.result_message( - IDEN_TEMPLATE, # type: ignore[arg-type] - [_entry_dict(entry) for entry in registry.entities.values()], - ) - ) - connection.send_message( - cached_list_entities.replace(IDEN_JSON_TEMPLATE, str(msg["id"]), 1) - ) - - hass.bus.async_listen( - er.EVENT_ENTITY_REGISTRY_UPDATED, - _async_clear_list_entities_cache, - run_immediately=True, - ) websocket_api.async_register_command(hass, websocket_list_entities) websocket_api.async_register_command(hass, websocket_get_entity) + websocket_api.async_register_command(hass, websocket_get_entities) websocket_api.async_register_command(hass, websocket_update_entity) websocket_api.async_register_command(hass, websocket_remove_entity) return True +@websocket_api.websocket_command({vol.Required("type"): "config/entity_registry/list"}) +@callback +def websocket_list_entities( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Handle list registry entries command.""" + registry = er.async_get(hass) + # Build start of response message + msg_json_prefix = ( + f'{{"id":{msg["id"]},"type": "{websocket_api.const.TYPE_RESULT}",' + f'"success":true,"result": [' + ) + # Concatenate cached entity registry item JSON serializations + msg_json = ( + msg_json_prefix + + ",".join( + entry.partial_json_repr + for entry in registry.entities.values() + if entry.partial_json_repr is not None + ) + + "]}" + ) + connection.send_message(msg_json) + + @websocket_api.websocket_command( { vol.Required("type"): "config/entity_registry/get", @@ -96,6 +84,33 @@ def websocket_get_entity( ) +@websocket_api.websocket_command( + { + vol.Required("type"): "config/entity_registry/get_entries", + vol.Required("entity_ids"): cv.entity_ids, + } +) +@callback +def websocket_get_entities( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Handle get entity registry entries command. + + Async friendly. + """ + registry = er.async_get(hass) + + entity_ids = msg["entity_ids"] + entries: dict[str, dict[str, Any] | None] = {} + for entity_id in entity_ids: + entry = registry.entities.get(entity_id) + entries[entity_id] = _entry_ext_dict(entry) if entry else None + + connection.send_message(websocket_api.result_message(msg["id"], entries)) + + @require_admin @websocket_api.websocket_command( { @@ -244,32 +259,10 @@ def websocket_remove_entity( connection.send_message(websocket_api.result_message(msg["id"])) -@callback -def _entry_dict(entry: er.RegistryEntry) -> dict[str, Any]: - """Convert entry to API format.""" - return { - "area_id": entry.area_id, - "config_entry_id": entry.config_entry_id, - "device_id": entry.device_id, - "disabled_by": entry.disabled_by, - "entity_category": entry.entity_category, - "entity_id": entry.entity_id, - "has_entity_name": entry.has_entity_name, - "hidden_by": entry.hidden_by, - "icon": entry.icon, - "id": entry.id, - "name": entry.name, - "original_name": entry.original_name, - "platform": entry.platform, - "translation_key": entry.translation_key, - "unique_id": entry.unique_id, - } - - @callback def _entry_ext_dict(entry: er.RegistryEntry) -> dict[str, Any]: """Convert entry to API format.""" - data = _entry_dict(entry) + data = entry.as_partial_dict data["aliases"] = entry.aliases data["capabilities"] = entry.capabilities data["device_class"] = entry.device_class diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index 5de212d03a5..be4151c3d80 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -219,8 +219,7 @@ class Configurator: if not self._validate_request_id(request_id): return - # pylint: disable=unused-variable - entity_id, fields, callback = self._requests[request_id] + _, _, callback = self._requests[request_id] # field validation goes here? if callback: diff --git a/homeassistant/components/control4/translations/lt.json b/homeassistant/components/control4/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/control4/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index b95b9361624..a9356ab8b7e 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -10,13 +10,14 @@ import voluptuous as vol from homeassistant import core from homeassistant.components import http, websocket_api from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, intent from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass -from .agent import AbstractConversationAgent, ConversationResult -from .default_agent import DefaultAgent, async_register +from .agent import AbstractConversationAgent, ConversationInput, ConversationResult +from .default_agent import DefaultAgent _LOGGER = logging.getLogger(__name__) @@ -30,14 +31,25 @@ DATA_AGENT = "conversation_agent" DATA_CONFIG = "conversation_config" SERVICE_PROCESS = "process" +SERVICE_RELOAD = "reload" SERVICE_PROCESS_SCHEMA = vol.Schema( - {vol.Required(ATTR_TEXT): cv.string, vol.Optional(ATTR_LANGUAGE): cv.string} + { + vol.Required(ATTR_TEXT): cv.string, + vol.Optional(ATTR_LANGUAGE): cv.string, + } +) + + +SERVICE_RELOAD_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_LANGUAGE): cv.string, + } ) CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( + vol.Optional(DOMAIN): vol.Schema( { vol.Optional("intents"): vol.Schema( {cv.string: vol.All(cv.ensure_list, [cv.string])} @@ -48,47 +60,73 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -async_register = bind_hass(async_register) - @core.callback @bind_hass -def async_set_agent(hass: core.HomeAssistant, agent: AbstractConversationAgent | None): +def async_set_agent( + hass: core.HomeAssistant, + config_entry: ConfigEntry, + agent: AbstractConversationAgent, +): """Set the agent to handle the conversations.""" hass.data[DATA_AGENT] = agent +@core.callback +@bind_hass +def async_unset_agent( + hass: core.HomeAssistant, + config_entry: ConfigEntry, +): + """Set the agent to handle the conversations.""" + hass.data[DATA_AGENT] = None + + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Register the process service.""" - hass.data[DATA_CONFIG] = config + if config_intents := config.get(DOMAIN, {}).get("intents"): + hass.data[DATA_CONFIG] = config_intents - async def handle_service(service: core.ServiceCall) -> None: + async def handle_process(service: core.ServiceCall) -> None: """Parse text into commands.""" text = service.data[ATTR_TEXT] _LOGGER.debug("Processing: <%s>", text) agent = await _get_agent(hass) try: await agent.async_process( - text, service.context, language=service.data.get(ATTR_LANGUAGE) + ConversationInput( + text=text, + context=service.context, + conversation_id=None, + language=service.data.get(ATTR_LANGUAGE, hass.config.language), + ) ) except intent.IntentHandleError as err: _LOGGER.error("Error processing %s: %s", text, err) + async def handle_reload(service: core.ServiceCall) -> None: + """Reload intents.""" + agent = await _get_agent(hass) + await agent.async_reload(language=service.data.get(ATTR_LANGUAGE)) + hass.services.async_register( - DOMAIN, SERVICE_PROCESS, handle_service, schema=SERVICE_PROCESS_SCHEMA + DOMAIN, SERVICE_PROCESS, handle_process, schema=SERVICE_PROCESS_SCHEMA + ) + hass.services.async_register( + DOMAIN, SERVICE_RELOAD, handle_reload, schema=SERVICE_RELOAD_SCHEMA ) hass.http.register_view(ConversationProcessView()) websocket_api.async_register_command(hass, websocket_process) + websocket_api.async_register_command(hass, websocket_prepare) websocket_api.async_register_command(hass, websocket_get_agent_info) - websocket_api.async_register_command(hass, websocket_set_onboarding) return True @websocket_api.websocket_command( { - "type": "conversation/process", - "text": str, + vol.Required("type"): "conversation/process", + vol.Required("text"): str, vol.Optional("conversation_id"): vol.Any(str, None), vol.Optional("language"): str, } @@ -100,7 +138,7 @@ async def websocket_process( msg: dict[str, Any], ) -> None: """Process text.""" - result = await _async_converse( + result = await async_converse( hass, msg["text"], msg.get("conversation_id"), @@ -110,43 +148,46 @@ async def websocket_process( connection.send_result(msg["id"], result.as_dict()) -@websocket_api.websocket_command({"type": "conversation/agent/info"}) +@websocket_api.websocket_command( + { + "type": "conversation/prepare", + vol.Optional("language"): str, + } +) +@websocket_api.async_response +async def websocket_prepare( + hass: HomeAssistant, + connection: websocket_api.ActiveConnection, + msg: dict[str, Any], +) -> None: + """Reload intents.""" + agent = await _get_agent(hass) + await agent.async_prepare(msg.get("language")) + connection.send_result(msg["id"]) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "conversation/agent/info", + } +) @websocket_api.async_response async def websocket_get_agent_info( hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any], ) -> None: - """Do we need onboarding.""" + """Info about the agent in use.""" agent = await _get_agent(hass) connection.send_result( msg["id"], { - "onboarding": await agent.async_get_onboarding(), "attribution": agent.attribution, }, ) -@websocket_api.websocket_command({"type": "conversation/onboarding/set", "shown": bool}) -@websocket_api.async_response -async def websocket_set_onboarding( - hass: HomeAssistant, - connection: websocket_api.ActiveConnection, - msg: dict[str, Any], -) -> None: - """Set onboarding status.""" - agent = await _get_agent(hass) - - success = await agent.async_set_onboarding(msg["shown"]) - - if success: - connection.send_result(msg["id"]) - else: - connection.send_error(msg["id"], "error", "Failed to set onboarding") - - class ConversationProcessView(http.HomeAssistantView): """View to process text.""" @@ -165,7 +206,7 @@ class ConversationProcessView(http.HomeAssistantView): async def post(self, request, data): """Send a request for processing.""" hass = request.app["hass"] - result = await _async_converse( + result = await async_converse( hass, text=data["text"], conversation_id=data.get("conversation_id"), @@ -184,7 +225,7 @@ async def _get_agent(hass: core.HomeAssistant) -> AbstractConversationAgent: return agent -async def _async_converse( +async def async_converse( hass: core.HomeAssistant, text: str, conversation_id: str | None, @@ -196,44 +237,12 @@ async def _async_converse( if language is None: language = hass.config.language - result: ConversationResult | None = None - intent_response: intent.IntentResponse | None = None - - try: - result = await agent.async_process(text, context, conversation_id, language) - except intent.IntentHandleError as err: - # Match was successful, but target(s) were invalid - intent_response = intent.IntentResponse(language=language) - intent_response.async_set_error( - intent.IntentResponseErrorCode.NO_VALID_TARGETS, - str(err), + result = await agent.async_process( + ConversationInput( + text=text, + context=context, + conversation_id=conversation_id, + language=language, ) - except intent.IntentUnexpectedError as err: - # Match was successful, but an error occurred while handling intent - intent_response = intent.IntentResponse(language=language) - intent_response.async_set_error( - intent.IntentResponseErrorCode.FAILED_TO_HANDLE, - str(err), - ) - except intent.IntentError as err: - # Unknown error - intent_response = intent.IntentResponse(language=language) - intent_response.async_set_error( - intent.IntentResponseErrorCode.UNKNOWN, - str(err), - ) - - if result is None: - if intent_response is None: - # Match was not successful - intent_response = intent.IntentResponse(language=language) - intent_response.async_set_error( - intent.IntentResponseErrorCode.NO_INTENT_MATCH, - "Sorry, I didn't understand that", - ) - - result = ConversationResult( - response=intent_response, conversation_id=conversation_id - ) - + ) return result diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py index 0bd3f018589..2b2c307f824 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/agent.py @@ -3,12 +3,22 @@ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Any +from typing import Any, TypedDict from homeassistant.core import Context from homeassistant.helpers import intent +@dataclass +class ConversationInput: + """User input to be processed.""" + + text: str + context: Context + conversation_id: str | None + language: str + + @dataclass class ConversationResult: """Result of async_process.""" @@ -24,28 +34,27 @@ class ConversationResult: } +class Attribution(TypedDict): + """Attribution for a conversation agent.""" + + name: str + url: str + + class AbstractConversationAgent(ABC): """Abstract conversation agent.""" @property - def attribution(self): + def attribution(self) -> Attribution | None: """Return the attribution.""" return None - async def async_get_onboarding(self): - """Get onboard data.""" - return None - - async def async_set_onboarding(self, shown): - """Set onboard data.""" - return True - @abstractmethod - async def async_process( - self, - text: str, - context: Context, - conversation_id: str | None = None, - language: str | None = None, - ) -> ConversationResult | None: + async def async_process(self, user_input: ConversationInput) -> ConversationResult: """Process a sentence.""" + + async def async_reload(self, language: str | None = None) -> None: + """Clear cached intents for a language.""" + + async def async_prepare(self, language: str | None = None) -> None: + """Load intents for a language.""" diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 2e8e78d6f38..2756998b3a6 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -1,53 +1,63 @@ """Standard conversation implementation for Home Assistant.""" from __future__ import annotations +import asyncio +from collections import defaultdict +from collections.abc import Iterable +from dataclasses import dataclass +import logging +from pathlib import Path import re +from typing import IO, Any + +from hassil.intents import Intents, ResponseType, SlotList, TextSlotList +from hassil.recognize import RecognizeResult, recognize_all +from hassil.util import merge_dict +from home_assistant_intents import get_intents +import yaml from homeassistant import core, setup -from homeassistant.components.cover.intent import INTENT_CLOSE_COVER, INTENT_OPEN_COVER -from homeassistant.components.shopping_list.intent import ( - INTENT_ADD_ITEM, - INTENT_LAST_ITEMS, -) -from homeassistant.const import EVENT_COMPONENT_LOADED -from homeassistant.core import callback -from homeassistant.helpers import intent -from homeassistant.setup import ATTR_COMPONENT +from homeassistant.helpers import area_registry, entity_registry, intent, template +from homeassistant.helpers.json import json_loads -from .agent import AbstractConversationAgent, ConversationResult +from .agent import AbstractConversationAgent, ConversationInput, ConversationResult from .const import DOMAIN -from .util import create_matcher -REGEX_TURN_COMMAND = re.compile(r"turn (?P(?: |\w)+) (?P\w+)") +_LOGGER = logging.getLogger(__name__) +_DEFAULT_ERROR_TEXT = "Sorry, I couldn't understand that" + REGEX_TYPE = type(re.compile("")) -UTTERANCES = { - "cover": { - INTENT_OPEN_COVER: ["Open [the] [a] [an] {name}[s]"], - INTENT_CLOSE_COVER: ["Close [the] [a] [an] {name}[s]"], - }, - "shopping_list": { - INTENT_ADD_ITEM: ["Add [the] [a] [an] {item} to my shopping list"], - INTENT_LAST_ITEMS: ["What is on my shopping list"], - }, -} + +def json_load(fp: IO[str]) -> dict[str, Any]: + """Wrap json_loads for get_intents.""" + return json_loads(fp.read()) -@core.callback -def async_register(hass, intent_type, utterances): - """Register utterances and any custom intents for the default agent. +@dataclass +class LanguageIntents: + """Loaded intents for a language.""" - Registrations don't require conversations to be loaded. They will become - active once the conversation component is loaded. - """ - intents = hass.data.setdefault(DOMAIN, {}) - conf = intents.setdefault(intent_type, []) + intents: Intents + intents_dict: dict[str, Any] + intent_responses: dict[str, Any] + error_responses: dict[str, Any] + loaded_components: set[str] - for utterance in utterances: - if isinstance(utterance, REGEX_TYPE): - conf.append(utterance) - else: - conf.append(create_matcher(utterance)) + +def _get_language_variations(language: str) -> Iterable[str]: + """Generate language codes with and without region.""" + yield language + + parts = re.split(r"([-_])", language) + if len(parts) == 3: + lang, sep, region = parts + if sep == "_": + # en_US -> en-US + yield f"{lang}-{region}" + + # en-US -> en + yield lang class DefaultAgent(AbstractConversationAgent): @@ -56,87 +66,378 @@ class DefaultAgent(AbstractConversationAgent): def __init__(self, hass: core.HomeAssistant) -> None: """Initialize the default agent.""" self.hass = hass + self._lang_intents: dict[str, LanguageIntents] = {} + self._lang_lock: dict[str, asyncio.Lock] = defaultdict(asyncio.Lock) - async def async_initialize(self, config): + # intent -> [sentences] + self._config_intents: dict[str, Any] = {} + self._areas_list: TextSlotList | None = None + self._names_list: TextSlotList | None = None + + async def async_initialize(self, config_intents): """Initialize the default agent.""" if "intent" not in self.hass.config.components: await setup.async_setup_component(self.hass, "intent", {}) - config = config.get(DOMAIN, {}) - intents = self.hass.data.setdefault(DOMAIN, {}) + # Intents from config may only contains sentences for HA config's language + if config_intents: + self._config_intents = config_intents - for intent_type, utterances in config.get("intents", {}).items(): - if (conf := intents.get(intent_type)) is None: - conf = intents[intent_type] = [] - - conf.extend(create_matcher(utterance) for utterance in utterances) - - # We strip trailing 's' from name because our state matcher will fail - # if a letter is not there. By removing 's' we can match singular and - # plural names. - - async_register( - self.hass, - intent.INTENT_TURN_ON, - ["Turn [the] [a] {name}[s] on", "Turn on [the] [a] [an] {name}[s]"], + self.hass.bus.async_listen( + area_registry.EVENT_AREA_REGISTRY_UPDATED, + self._async_handle_area_registry_changed, + run_immediately=True, ) - async_register( - self.hass, - intent.INTENT_TURN_OFF, - ["Turn [the] [a] [an] {name}[s] off", "Turn off [the] [a] [an] {name}[s]"], + self.hass.bus.async_listen( + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, + self._async_handle_entity_registry_changed, + run_immediately=True, ) - async_register( - self.hass, - intent.INTENT_TOGGLE, - ["Toggle [the] [a] [an] {name}[s]", "[the] [a] [an] {name}[s] toggle"], + self.hass.bus.async_listen( + core.EVENT_STATE_CHANGED, + self._async_handle_state_changed, + run_immediately=True, ) - @callback - def component_loaded(event): - """Handle a new component loaded.""" - self.register_utterances(event.data[ATTR_COMPONENT]) - - self.hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded) - - # Check already loaded components. - for component in self.hass.config.components: - self.register_utterances(component) - - @callback - def register_utterances(self, component): - """Register utterances for a component.""" - if component not in UTTERANCES: - return - for intent_type, sentences in UTTERANCES[component].items(): - async_register(self.hass, intent_type, sentences) - - async def async_process( - self, - text: str, - context: core.Context, - conversation_id: str | None = None, - language: str | None = None, - ) -> ConversationResult | None: + async def async_process(self, user_input: ConversationInput) -> ConversationResult: """Process a sentence.""" - intents = self.hass.data[DOMAIN] + language = user_input.language or self.hass.config.language + lang_intents = self._lang_intents.get(language) + conversation_id = None # Not supported - for intent_type, matchers in intents.items(): - for matcher in matchers: - if not (match := matcher.match(text)): + # Reload intents if missing or new components + if lang_intents is None or ( + lang_intents.loaded_components - self.hass.config.components + ): + # Load intents in executor + lang_intents = await self.async_get_or_load_intents(language) + + if lang_intents is None: + # No intents loaded + _LOGGER.warning("No intents were loaded for language: %s", language) + return _make_error_result( + language, + intent.IntentResponseErrorCode.NO_INTENT_MATCH, + _DEFAULT_ERROR_TEXT, + conversation_id, + ) + + slot_lists: dict[str, SlotList] = { + "area": self._make_areas_list(), + "name": self._make_names_list(), + } + + result = await self.hass.async_add_executor_job( + self._recognize, + user_input, + lang_intents, + slot_lists, + ) + if result is None: + _LOGGER.debug("No intent was matched for '%s'", user_input.text) + return _make_error_result( + language, + intent.IntentResponseErrorCode.NO_INTENT_MATCH, + self._get_error_text(ResponseType.NO_INTENT, lang_intents), + conversation_id, + ) + + try: + intent_response = await intent.async_handle( + self.hass, + DOMAIN, + result.intent.name, + { + entity.name: {"value": entity.value} + for entity in result.entities_list + }, + user_input.text, + user_input.context, + language, + ) + except intent.IntentHandleError: + _LOGGER.exception("Intent handling error") + return _make_error_result( + language, + intent.IntentResponseErrorCode.FAILED_TO_HANDLE, + self._get_error_text(ResponseType.HANDLE_ERROR, lang_intents), + conversation_id, + ) + except intent.IntentUnexpectedError: + _LOGGER.exception("Unexpected intent error") + return _make_error_result( + language, + intent.IntentResponseErrorCode.UNKNOWN, + self._get_error_text(ResponseType.HANDLE_ERROR, lang_intents), + conversation_id, + ) + + if ( + (not intent_response.speech) + and (intent_response.intent is not None) + and (response_key := result.response) + ): + # Use response template, if available + response_str = lang_intents.intent_responses.get( + result.intent.name, {} + ).get(response_key) + if response_str: + response_template = template.Template(response_str, self.hass) + speech = response_template.async_render( + { + "slots": { + entity_name: entity_value.text or entity_value.value + for entity_name, entity_value in result.entities.items() + } + } + ) + + # Normalize whitespace + speech = " ".join(speech.strip().split()) + intent_response.async_set_speech(speech) + + return ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + + def _recognize( + self, + user_input: ConversationInput, + lang_intents: LanguageIntents, + slot_lists: dict[str, SlotList], + ) -> RecognizeResult | None: + """Search intents for a match to user input.""" + # Prioritize matches with entity names above area names + maybe_result: RecognizeResult | None = None + for result in recognize_all( + user_input.text, lang_intents.intents, slot_lists=slot_lists + ): + if "name" in result.entities: + return result + + # Keep looking in case an entity has the same name + maybe_result = result + + return maybe_result + + async def async_reload(self, language: str | None = None): + """Clear cached intents for a language.""" + if language is None: + language = self.hass.config.language + + self._lang_intents.pop(language, None) + _LOGGER.debug("Cleared intents for language: %s", language) + + async def async_prepare(self, language: str | None = None): + """Load intents for a language.""" + if language is None: + language = self.hass.config.language + + lang_intents = await self.async_get_or_load_intents(language) + + if lang_intents is None: + # No intents loaded + _LOGGER.warning("No intents were loaded for language: %s", language) + + async def async_get_or_load_intents(self, language: str) -> LanguageIntents | None: + """Load all intents of a language with lock.""" + hass_components = set(self.hass.config.components) + async with self._lang_lock[language]: + return await self.hass.async_add_executor_job( + self._get_or_load_intents, language, hass_components + ) + + def _get_or_load_intents( + self, language: str, hass_components: set[str] + ) -> LanguageIntents | None: + """Load all intents for language (run inside executor).""" + lang_intents = self._lang_intents.get(language) + + if lang_intents is None: + intents_dict: dict[str, Any] = {} + loaded_components: set[str] = set() + else: + intents_dict = lang_intents.intents_dict + loaded_components = lang_intents.loaded_components + + # Check if any new components have been loaded + intents_changed = False + for component in hass_components: + if component in loaded_components: + continue + + # Don't check component again + loaded_components.add(component) + + # Check for intents for this component with the target language. + # Try en-US, en, etc. + for language_variation in _get_language_variations(language): + component_intents = get_intents( + component, language_variation, json_load=json_load + ) + if component_intents: + # Merge sentences into existing dictionary + merge_dict(intents_dict, component_intents) + + # Will need to recreate graph + intents_changed = True + _LOGGER.debug( + "Loaded intents component=%s, language=%s", component, language + ) + break + + # Check for custom sentences in /custom_sentences// + if lang_intents is None: + # Only load custom sentences once, otherwise they will be re-loaded + # when components change. + custom_sentences_dir = Path( + self.hass.config.path("custom_sentences", language) + ) + if custom_sentences_dir.is_dir(): + for custom_sentences_path in custom_sentences_dir.rglob("*.yaml"): + with custom_sentences_path.open( + encoding="utf-8" + ) as custom_sentences_file: + # Merge custom sentences + merge_dict(intents_dict, yaml.safe_load(custom_sentences_file)) + + # Will need to recreate graph + intents_changed = True + _LOGGER.debug( + "Loaded custom sentences language=%s, path=%s", + language, + custom_sentences_path, + ) + + # Load sentences from HA config for default language only + if self._config_intents and (language == self.hass.config.language): + merge_dict( + intents_dict, + { + "intents": { + intent_name: {"data": [{"sentences": sentences}]} + for intent_name, sentences in self._config_intents.items() + } + }, + ) + intents_changed = True + _LOGGER.debug( + "Loaded intents from configuration.yaml", + ) + + if not intents_dict: + return None + + if not intents_changed and lang_intents is not None: + return lang_intents + + # This can be made faster by not re-parsing existing sentences. + # But it will likely only be called once anyways, unless new + # components with sentences are often being loaded. + intents = Intents.from_dict(intents_dict) + + # Load responses + responses_dict = intents_dict.get("responses", {}) + intent_responses = responses_dict.get("intents", {}) + error_responses = responses_dict.get("errors", {}) + + if lang_intents is None: + lang_intents = LanguageIntents( + intents, + intents_dict, + intent_responses, + error_responses, + loaded_components, + ) + self._lang_intents[language] = lang_intents + else: + lang_intents.intents = intents + lang_intents.intent_responses = intent_responses + lang_intents.error_responses = error_responses + + return lang_intents + + @core.callback + def _async_handle_area_registry_changed(self, event: core.Event) -> None: + """Clear area area cache when the area registry has changed.""" + self._areas_list = None + + @core.callback + def _async_handle_entity_registry_changed(self, event: core.Event) -> None: + """Clear names list cache when an entity changes aliases.""" + if event.data["action"] == "update" and "aliases" not in event.data["changes"]: + return + self._names_list = None + + @core.callback + def _async_handle_state_changed(self, event: core.Event) -> None: + """Clear names list cache when a state is added or removed from the state machine.""" + if event.data.get("old_state") and event.data.get("new_state"): + return + self._names_list = None + + def _make_areas_list(self) -> TextSlotList: + """Create slot list mapping area names/aliases to area ids.""" + if self._areas_list is not None: + return self._areas_list + registry = area_registry.async_get(self.hass) + areas = [] + for entry in registry.async_list_areas(): + areas.append((entry.name, entry.id)) + if entry.aliases: + for alias in entry.aliases: + areas.append((alias, entry.id)) + + self._areas_list = TextSlotList.from_tuples(areas, allow_template=False) + return self._areas_list + + def _make_names_list(self) -> TextSlotList: + """Create slot list mapping entity names/aliases to entity ids.""" + if self._names_list is not None: + return self._names_list + states = self.hass.states.async_all() + entities = entity_registry.async_get(self.hass) + names = [] + for state in states: + context = {"domain": state.domain} + + entity = entities.async_get(state.entity_id) + if entity is not None: + if entity.entity_category or entity.hidden: + # Skip configuration/diagnostic/hidden entities continue - intent_response = await intent.async_handle( - self.hass, - DOMAIN, - intent_type, - {key: {"value": value} for key, value in match.groupdict().items()}, - text, - context, - language, - ) + if entity.aliases: + for alias in entity.aliases: + names.append((alias, state.entity_id, context)) - return ConversationResult( - response=intent_response, conversation_id=conversation_id - ) + # Default name + names.append((state.name, state.entity_id, context)) - return None + else: + # Default name + names.append((state.name, state.entity_id, context)) + + self._names_list = TextSlotList.from_tuples(names, allow_template=False) + return self._names_list + + def _get_error_text( + self, response_type: ResponseType, lang_intents: LanguageIntents + ) -> str: + """Get response error text by type.""" + response_key = response_type.value + response_str = lang_intents.error_responses.get(response_key) + return response_str or _DEFAULT_ERROR_TEXT + + +def _make_error_result( + language: str, + error_code: intent.IntentResponseErrorCode, + response_text: str, + conversation_id: str | None = None, +) -> ConversationResult: + """Create conversation result with error code and text.""" + response = intent.IntentResponse(language=language) + response.async_set_error(error_code, response_text) + + return ConversationResult(response, conversation_id) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 54265bfcb83..f44bcda8f03 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -2,6 +2,7 @@ "domain": "conversation", "name": "Conversation", "documentation": "https://www.home-assistant.io/integrations/conversation", + "requirements": ["hassil==0.2.6", "home-assistant-intents==2023.1.31"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index ef2b5328f96..129797c356f 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -10,18 +10,20 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DATA_COORDINATOR, DATA_INFO, DOMAIN +from .const import CONF_SWING_SUPPORT, DATA_COORDINATOR, DATA_INFO, DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.CLIMATE] +PLATFORMS = [Platform.CLIMATE, Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Coolmaster from a config entry.""" host = entry.data[CONF_HOST] port = entry.data[CONF_PORT] - coolmaster = CoolMasterNet(host, port) + coolmaster = CoolMasterNet( + host, port, swing_support=entry.data.get(CONF_SWING_SUPPORT, False) + ) try: info = await coolmaster.info() if not info: diff --git a/homeassistant/components/coolmaster/binary_sensor.py b/homeassistant/components/coolmaster/binary_sensor.py new file mode 100644 index 00000000000..6d25d7ababf --- /dev/null +++ b/homeassistant/components/coolmaster/binary_sensor.py @@ -0,0 +1,47 @@ +"""Binary Sensor platform for CoolMasterNet integration.""" +from __future__ import annotations + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DATA_COORDINATOR, DATA_INFO, DOMAIN +from .entity import CoolmasterEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the CoolMasterNet binary_sensor platform.""" + info = hass.data[DOMAIN][config_entry.entry_id][DATA_INFO] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + async_add_entities( + CoolmasterCleanFilter(coordinator, unit_id, info) + for unit_id in coordinator.data + ) + + +class CoolmasterCleanFilter(CoolmasterEntity, BinarySensorEntity): + """Representation of a unit's filter state (true means need to be cleaned).""" + + _attr_has_entity_name = True + entity_description = BinarySensorEntityDescription( + key="clean_filter", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + name="Clean filter", + icon="mdi:air-filter", + ) + + @property + def is_on(self) -> bool | None: + """Return true if the binary sensor is on.""" + return self._unit.clean_filter diff --git a/homeassistant/components/coolmaster/button.py b/homeassistant/components/coolmaster/button.py new file mode 100644 index 00000000000..6f9a31576a2 --- /dev/null +++ b/homeassistant/components/coolmaster/button.py @@ -0,0 +1,42 @@ +"""Button platform for CoolMasterNet integration.""" +from __future__ import annotations + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DATA_COORDINATOR, DATA_INFO, DOMAIN +from .entity import CoolmasterEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the CoolMasterNet button platform.""" + info = hass.data[DOMAIN][config_entry.entry_id][DATA_INFO] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + async_add_entities( + CoolmasterResetFilter(coordinator, unit_id, info) + for unit_id in coordinator.data + ) + + +class CoolmasterResetFilter(CoolmasterEntity, ButtonEntity): + """Reset the clean filter timer (once filter was cleaned).""" + + _attr_has_entity_name = True + entity_description = ButtonEntityDescription( + key="reset_filter", + entity_category=EntityCategory.CONFIG, + name="Reset filter", + icon="mdi:air-filter", + ) + + async def async_press(self) -> None: + """Press the button.""" + await self._unit.reset_filter() + await self.coordinator.async_refresh() diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 933072aac9d..d27f776c655 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -1,7 +1,11 @@ """CoolMasterNet platform to control of CoolMasterNet Climate Devices.""" +from __future__ import annotations + import logging from typing import Any +from pycoolmasternet_async import SWING_MODES + from homeassistant.components.climate import ( ClimateEntity, ClimateEntityFeature, @@ -9,12 +13,12 @@ from homeassistant.components.climate import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import DeviceInfo +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 .const import CONF_SUPPORTED_MODES, DATA_COORDINATOR, DATA_INFO, DOMAIN +from .entity import CoolmasterEntity CM_TO_HA_STATE = { "heat": HVACMode.HEAT, @@ -31,60 +35,28 @@ FAN_MODES = ["low", "med", "high", "auto"] _LOGGER = logging.getLogger(__name__) -def _build_entity(coordinator, unit_id, unit, supported_modes, info): - _LOGGER.debug("Found device %s", unit_id) - return CoolmasterClimate(coordinator, unit_id, unit, supported_modes, info) - - async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_devices: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the CoolMasterNet climate platform.""" - supported_modes = config_entry.data.get(CONF_SUPPORTED_MODES) info = hass.data[DOMAIN][config_entry.entry_id][DATA_INFO] - coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] - - all_devices = [ - _build_entity(coordinator, unit_id, unit, supported_modes, info) - for (unit_id, unit) in coordinator.data.items() - ] - - async_add_devices(all_devices) - - -class CoolmasterClimate(CoordinatorEntity, ClimateEntity): - """Representation of a coolmaster climate device.""" - - _attr_supported_features = ( - ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + supported_modes = config_entry.data.get(CONF_SUPPORTED_MODES) + async_add_entities( + CoolmasterClimate(coordinator, unit_id, info, supported_modes) + for unit_id in coordinator.data ) - def __init__(self, coordinator, unit_id, unit, supported_modes, info): + +class CoolmasterClimate(CoolmasterEntity, ClimateEntity): + """Representation of a coolmaster climate device.""" + + def __init__(self, coordinator, unit_id, info, supported_modes): """Initialize the climate device.""" - super().__init__(coordinator) - self._unit_id = unit_id - self._unit = unit + super().__init__(coordinator, unit_id, info) self._hvac_modes = supported_modes - self._info = info - - @callback - def _handle_coordinator_update(self): - self._unit = self.coordinator.data[self._unit_id] - super()._handle_coordinator_update() - - @property - def device_info(self) -> DeviceInfo: - """Return device info for this device.""" - return DeviceInfo( - identifiers={(DOMAIN, self.unique_id)}, - manufacturer="CoolAutomation", - model="CoolMasterNet", - name=self.name, - sw_version=self._info["version"], - ) @property def unique_id(self): @@ -96,6 +68,16 @@ class CoolmasterClimate(CoordinatorEntity, ClimateEntity): """Return the name of the climate device.""" return self.unique_id + @property + def supported_features(self) -> ClimateEntityFeature: + """Return the list of supported features.""" + supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ) + if self.swing_mode: + supported_features |= ClimateEntityFeature.SWING_MODE + return supported_features + @property def temperature_unit(self) -> str: """Return the unit of measurement.""" @@ -138,6 +120,16 @@ class CoolmasterClimate(CoordinatorEntity, ClimateEntity): """Return the list of available fan modes.""" return FAN_MODES + @property + def swing_mode(self) -> str | None: + """Return the swing mode setting.""" + return self._unit.swing + + @property + def swing_modes(self) -> list[str] | None: + """Return swing modes if supported.""" + return SWING_MODES if self.swing_mode is not None else None + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is not None: @@ -151,6 +143,15 @@ class CoolmasterClimate(CoordinatorEntity, ClimateEntity): self._unit = await self._unit.set_fan_speed(fan_mode) self.async_write_ha_state() + async def async_set_swing_mode(self, swing_mode: str) -> None: + """Set new swing mode.""" + _LOGGER.debug("Setting swing mode of %s to %s", self.unique_id, swing_mode) + try: + self._unit = await self._unit.set_swing(swing_mode) + except ValueError as error: + raise HomeAssistantError(error) from error + self.async_write_ha_state() + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new operation mode.""" _LOGGER.debug("Setting operation mode of %s to %s", self.unique_id, hvac_mode) diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py index 9ad88d36574..ad3817b77ce 100644 --- a/homeassistant/components/coolmaster/config_flow.py +++ b/homeassistant/components/coolmaster/config_flow.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from .const import CONF_SUPPORTED_MODES, DEFAULT_PORT, DOMAIN +from .const import CONF_SUPPORTED_MODES, CONF_SWING_SUPPORT, DEFAULT_PORT, DOMAIN AVAILABLE_MODES = [ HVACMode.OFF.value, @@ -25,7 +25,13 @@ AVAILABLE_MODES = [ MODES_SCHEMA = {vol.Required(mode, default=True): bool for mode in AVAILABLE_MODES} -DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, **MODES_SCHEMA}) +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + **MODES_SCHEMA, + vol.Required(CONF_SWING_SUPPORT, default=False): bool, + } +) async def _validate_connection(host: str) -> bool: @@ -50,6 +56,7 @@ class CoolmasterConfigFlow(ConfigFlow, domain=DOMAIN): CONF_HOST: data[CONF_HOST], CONF_PORT: DEFAULT_PORT, CONF_SUPPORTED_MODES: supported_modes, + CONF_SWING_SUPPORT: data[CONF_SWING_SUPPORT], }, ) diff --git a/homeassistant/components/coolmaster/const.py b/homeassistant/components/coolmaster/const.py index e5aa1f1b93d..1fa46e20ee9 100644 --- a/homeassistant/components/coolmaster/const.py +++ b/homeassistant/components/coolmaster/const.py @@ -8,3 +8,4 @@ DOMAIN = "coolmaster" DEFAULT_PORT = 10102 CONF_SUPPORTED_MODES = "supported_modes" +CONF_SWING_SUPPORT = "swing_support" diff --git a/homeassistant/components/coolmaster/entity.py b/homeassistant/components/coolmaster/entity.py new file mode 100644 index 00000000000..65f21b77534 --- /dev/null +++ b/homeassistant/components/coolmaster/entity.py @@ -0,0 +1,38 @@ +"""Base entity for Coolmaster integration.""" +from pycoolmasternet_async.coolmasternet import CoolMasterNetUnit + +from homeassistant.core import callback +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import CoolmasterDataUpdateCoordinator +from .const import DOMAIN + + +class CoolmasterEntity(CoordinatorEntity[CoolmasterDataUpdateCoordinator]): + """Representation of a Coolmaster entity.""" + + def __init__( + self, + coordinator: CoolmasterDataUpdateCoordinator, + unit_id: str, + info: dict[str, str], + ) -> None: + """Initiate CoolmasterEntity.""" + super().__init__(coordinator) + self._unit_id: str = unit_id + self._unit: CoolMasterNetUnit = coordinator.data[self._unit_id] + self._attr_device_info: DeviceInfo = DeviceInfo( + identifiers={(DOMAIN, unit_id)}, + manufacturer="CoolAutomation", + model="CoolMasterNet", + name=unit_id, + sw_version=info["version"], + ) + if hasattr(self, "entity_description"): + self._attr_unique_id: str = f"{unit_id}-{self.entity_description.key}" + + @callback + def _handle_coordinator_update(self) -> None: + self._unit = self.coordinator.data[self._unit_id] + super()._handle_coordinator_update() diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index a56a97f272e..8980850ca49 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -3,7 +3,7 @@ "name": "CoolMasterNet", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coolmaster", - "requirements": ["pycoolmasternet-async==0.1.2"], + "requirements": ["pycoolmasternet-async==0.1.5"], "codeowners": ["@OnFreund"], "iot_class": "local_polling", "loggers": ["pycoolmasternet_async"] diff --git a/homeassistant/components/coolmaster/sensor.py b/homeassistant/components/coolmaster/sensor.py new file mode 100644 index 00000000000..ef550360f84 --- /dev/null +++ b/homeassistant/components/coolmaster/sensor.py @@ -0,0 +1,42 @@ +"""Sensor platform for CoolMasterNet integration.""" +from __future__ import annotations + +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DATA_COORDINATOR, DATA_INFO, DOMAIN +from .entity import CoolmasterEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the CoolMasterNet sensor platform.""" + info = hass.data[DOMAIN][config_entry.entry_id][DATA_INFO] + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + async_add_entities( + CoolmasterCleanFilter(coordinator, unit_id, info) + for unit_id in coordinator.data + ) + + +class CoolmasterCleanFilter(CoolmasterEntity, SensorEntity): + """Representation of a unit's error code.""" + + _attr_has_entity_name = True + entity_description = SensorEntityDescription( + key="error_code", + entity_category=EntityCategory.DIAGNOSTIC, + name="Error code", + icon="mdi:alert", + ) + + @property + def native_value(self) -> str: + """Return the error code or OK.""" + return self._unit.error_code or "OK" diff --git a/homeassistant/components/coolmaster/strings.json b/homeassistant/components/coolmaster/strings.json index 970660e6e63..6bba26b6bc9 100644 --- a/homeassistant/components/coolmaster/strings.json +++ b/homeassistant/components/coolmaster/strings.json @@ -10,7 +10,8 @@ "cool": "Support cool mode", "heat_cool": "Support automatic heat/cool mode", "dry": "Support dry mode", - "fan_only": "Support fan only mode" + "fan_only": "Support fan only mode", + "swing_support": "Control swing mode" } } }, diff --git a/homeassistant/components/coolmaster/translations/bg.json b/homeassistant/components/coolmaster/translations/bg.json index 72b8df6634d..aa3a5f5a8b5 100644 --- a/homeassistant/components/coolmaster/translations/bg.json +++ b/homeassistant/components/coolmaster/translations/bg.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435(", "no_units": "\u041d\u0435 \u0431\u044f\u0445\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u043a\u043b\u0438\u043c\u0430\u0442\u0438\u0447\u043d\u0438/\u0432\u0435\u043d\u0442\u0438\u043b\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u0438 \u043d\u0430 \u0437\u0430\u0434\u0430\u0434\u0435\u043d\u0438\u044f CoolMasterNet \u0430\u0434\u0440\u0435\u0441." }, "step": { @@ -12,8 +12,9 @@ "fan_only": "\u041f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430 \u043d\u0430 \u0440\u0435\u0436\u0438\u043c \u0432\u0435\u043d\u0442\u0438\u043b\u0430\u0442\u043e\u0440", "heat": "\u041f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430 \u043d\u0430 \u0440\u0435\u0436\u0438\u043c \u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435", "heat_cool": "\u041f\u043e\u0434\u0434\u0440\u044a\u0436\u043a\u0430 \u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435/\u043e\u0445\u043b\u0430\u0436\u0434\u0430\u043d\u0435", - "host": "\u0410\u0434\u0440\u0435\u0441", - "off": "\u041c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d" + "host": "\u0425\u043e\u0441\u0442", + "off": "\u041c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", + "swing_support": "\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u0430 \u0440\u0435\u0436\u0438\u043c\u0430 \u043d\u0430 \u043b\u044e\u043b\u0435\u0435\u043d\u0435" }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0432\u043e\u0438\u0442\u0435 \u0434\u0430\u043d\u043d\u0438 \u0437\u0430 \u0432\u0440\u044a\u0437\u043a\u0430 \u0441 CoolMasterNet." } diff --git a/homeassistant/components/coolmaster/translations/ca.json b/homeassistant/components/coolmaster/translations/ca.json index 6db6a6d72d0..b991fdcd4bd 100644 --- a/homeassistant/components/coolmaster/translations/ca.json +++ b/homeassistant/components/coolmaster/translations/ca.json @@ -13,7 +13,8 @@ "heat": "Suporta mode escalfar", "heat_cool": "Suporta mode escalfar/refredar autom\u00e0tic", "host": "Amfitri\u00f3", - "off": "Es pot apagar" + "off": "Es pot apagar", + "swing_support": "Controla el mode d'oscil\u00b7laci\u00f3" }, "title": "Configuraci\u00f3 de la connexi\u00f3 amb CoolMasterNet." } diff --git a/homeassistant/components/coolmaster/translations/de.json b/homeassistant/components/coolmaster/translations/de.json index 4e58b1ed964..f6c7fb03ab9 100644 --- a/homeassistant/components/coolmaster/translations/de.json +++ b/homeassistant/components/coolmaster/translations/de.json @@ -13,7 +13,8 @@ "heat": "Unterst\u00fctzt Heiz-Modus", "heat_cool": "Unterst\u00fctzung automatische Heiz-/K\u00fchlmodus", "host": "Host", - "off": "Kann ausgeschaltet werden" + "off": "Kann ausgeschaltet werden", + "swing_support": "Swing-Modus steuern" }, "title": "Richte deine CoolMasterNet-Verbindungsdaten ein." } diff --git a/homeassistant/components/coolmaster/translations/el.json b/homeassistant/components/coolmaster/translations/el.json index 9cdc9fe0054..e038f1096b9 100644 --- a/homeassistant/components/coolmaster/translations/el.json +++ b/homeassistant/components/coolmaster/translations/el.json @@ -13,9 +13,10 @@ "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" + "off": "\u039c\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af", + "swing_support": "\u0388\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b1\u03b9\u03ce\u03c1\u03b7\u03c3\u03b7\u03c2" }, - "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." + "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 CoolMasterNet." } } } diff --git a/homeassistant/components/coolmaster/translations/en.json b/homeassistant/components/coolmaster/translations/en.json index 9f12a4ebb30..57cea971c66 100644 --- a/homeassistant/components/coolmaster/translations/en.json +++ b/homeassistant/components/coolmaster/translations/en.json @@ -13,7 +13,8 @@ "heat": "Support heat mode", "heat_cool": "Support automatic heat/cool mode", "host": "Host", - "off": "Can be turned off" + "off": "Can be turned off", + "swing_support": "Control swing mode" }, "title": "Set up your CoolMasterNet connection details." } diff --git a/homeassistant/components/coolmaster/translations/es.json b/homeassistant/components/coolmaster/translations/es.json index 14e9179f122..2226623413e 100644 --- a/homeassistant/components/coolmaster/translations/es.json +++ b/homeassistant/components/coolmaster/translations/es.json @@ -13,7 +13,8 @@ "heat": "Admite modo de calor", "heat_cool": "Admite modo autom\u00e1tico de calor/fr\u00edo", "host": "Host", - "off": "Se puede apagar" + "off": "Se puede apagar", + "swing_support": "Modo de oscilaci\u00f3n de control" }, "title": "Configura los detalles de tu conexi\u00f3n CoolMasterNet." } diff --git a/homeassistant/components/coolmaster/translations/et.json b/homeassistant/components/coolmaster/translations/et.json index 6fb29910387..42b11887d51 100644 --- a/homeassistant/components/coolmaster/translations/et.json +++ b/homeassistant/components/coolmaster/translations/et.json @@ -13,7 +13,8 @@ "heat": "Kasuta k\u00fcttere\u017eiimi", "heat_cool": "Kasuta automaatset k\u00fctte / jahutuse re\u017eiimi", "host": "", - "off": "Saab v\u00e4lja l\u00fclitada" + "off": "Saab v\u00e4lja l\u00fclitada", + "swing_support": "Kontrolli \u00f5\u00f5tsumise re\u017eiimi" }, "title": "Seadista oma CoolMasterNet'i \u00fchenduse \u00fcksikasjad." } diff --git a/homeassistant/components/coolmaster/translations/hu.json b/homeassistant/components/coolmaster/translations/hu.json index c5fb4be995b..e55d5a74d4b 100644 --- a/homeassistant/components/coolmaster/translations/hu.json +++ b/homeassistant/components/coolmaster/translations/hu.json @@ -13,7 +13,8 @@ "heat": "T\u00e1mogatott f\u0171t\u00e9si m\u00f3d(ok)", "heat_cool": "T\u00e1mogatott f\u0171t\u00e9si/h\u0171t\u00e9si m\u00f3d(ok)", "host": "C\u00edm", - "off": "Ki lehet kapcsolni" + "off": "Ki lehet kapcsolni", + "swing_support": "Forg\u00e1si m\u00f3d be\u00e1ll\u00edt\u00e1sa" }, "title": "\u00c1ll\u00edtsa be a CoolMasterNet kapcsolat r\u00e9szleteit." } diff --git a/homeassistant/components/coolmaster/translations/id.json b/homeassistant/components/coolmaster/translations/id.json index d12c10da25a..c6640587092 100644 --- a/homeassistant/components/coolmaster/translations/id.json +++ b/homeassistant/components/coolmaster/translations/id.json @@ -13,7 +13,8 @@ "heat": "Mendukung mode panas", "heat_cool": "Mendukung mode panas/dingin otomatis", "host": "Host", - "off": "Bisa dimatikan" + "off": "Bisa dimatikan", + "swing_support": "Kontrol mode ayunan" }, "title": "Siapkan detail koneksi CoolMasterNet Anda." } diff --git a/homeassistant/components/coolmaster/translations/it.json b/homeassistant/components/coolmaster/translations/it.json index 2d3cf61e6d6..a57dbf58497 100644 --- a/homeassistant/components/coolmaster/translations/it.json +++ b/homeassistant/components/coolmaster/translations/it.json @@ -13,7 +13,8 @@ "heat": "Supporta la modalit\u00e0 di riscaldamento", "heat_cool": "Supporta la modalit\u00e0 di riscaldamento/raffreddamento automatica", "host": "Host", - "off": "Pu\u00f2 essere spento" + "off": "Pu\u00f2 essere spento", + "swing_support": "Controlla la modalit\u00e0 di oscillazione" }, "title": "Configura i dettagli della tua connessione CoolMasterNet." } diff --git a/homeassistant/components/coolmaster/translations/no.json b/homeassistant/components/coolmaster/translations/no.json index 8e7384bfad1..c8390b67f65 100644 --- a/homeassistant/components/coolmaster/translations/no.json +++ b/homeassistant/components/coolmaster/translations/no.json @@ -13,7 +13,8 @@ "heat": "St\u00f8tt varmemodus", "heat_cool": "St\u00f8tter automatisk varme/kj\u00f8l-modus", "host": "Vert", - "off": "Kan sl\u00e5s av" + "off": "Kan sl\u00e5s av", + "swing_support": "Kontroller svingmodus" }, "title": "Sett opp tilkoblingsdetaljer for CoolMasterNet." } diff --git a/homeassistant/components/coolmaster/translations/pl.json b/homeassistant/components/coolmaster/translations/pl.json index 81e8aa3e5a9..ad89ec5e888 100644 --- a/homeassistant/components/coolmaster/translations/pl.json +++ b/homeassistant/components/coolmaster/translations/pl.json @@ -13,7 +13,8 @@ "heat": "Obs\u0142uga trybu grzania", "heat_cool": "Obs\u0142uga automatycznego trybu grzanie/ch\u0142odzenie", "host": "Nazwa hosta lub adres IP", - "off": "Mo\u017ce by\u0107 wy\u0142\u0105czone" + "off": "Mo\u017ce by\u0107 wy\u0142\u0105czone", + "swing_support": "Sterowanie trybem obrotu" }, "title": "Konfiguracja po\u0142\u0105czenia CoolMasterNet." } diff --git a/homeassistant/components/coolmaster/translations/pt-BR.json b/homeassistant/components/coolmaster/translations/pt-BR.json index 4bb7d51e464..0056578401e 100644 --- a/homeassistant/components/coolmaster/translations/pt-BR.json +++ b/homeassistant/components/coolmaster/translations/pt-BR.json @@ -13,7 +13,8 @@ "heat": "Suporta o modo de aquecimento", "heat_cool": "Suporta o modo de aquecimento/resfriamento autom\u00e1tico", "host": "Nome do host", - "off": "Pode ser desligado" + "off": "Pode ser desligado", + "swing_support": "Modo de balan\u00e7o de controle" }, "title": "Configure seus detalhes de conex\u00e3o CoolMasterNet." } diff --git a/homeassistant/components/coolmaster/translations/ru.json b/homeassistant/components/coolmaster/translations/ru.json index 25b060aa65f..7c766e1237c 100644 --- a/homeassistant/components/coolmaster/translations/ru.json +++ b/homeassistant/components/coolmaster/translations/ru.json @@ -13,7 +13,8 @@ "heat": "\u0420\u0435\u0436\u0438\u043c \u043e\u0431\u043e\u0433\u0440\u0435\u0432\u0430", "heat_cool": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", "host": "\u0425\u043e\u0441\u0442", - "off": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435" + "off": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "swing_support": "\u0423\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0440\u0435\u0436\u0438\u043c\u043e\u043c \u043a\u0430\u0447\u0430\u043d\u0438\u044f" }, "title": "CoolMasterNet" } diff --git a/homeassistant/components/coolmaster/translations/sk.json b/homeassistant/components/coolmaster/translations/sk.json index 4151e5afd04..7496371c627 100644 --- a/homeassistant/components/coolmaster/translations/sk.json +++ b/homeassistant/components/coolmaster/translations/sk.json @@ -13,7 +13,8 @@ "heat": "Podpora re\u017eimu vykurovania", "heat_cool": "Podpora automatick\u00e9ho re\u017eimu vykurovania/chladenia", "host": "Hostite\u013e", - "off": "Mo\u017en\u00e9 vypn\u00fa\u0165" + "off": "Mo\u017en\u00e9 vypn\u00fa\u0165", + "swing_support": "Re\u017eim ovl\u00e1dania v\u00fdkyvu" }, "title": "Nastavte podrobnosti pripojenia CoolMasterNet." } diff --git a/homeassistant/components/coolmaster/translations/tr.json b/homeassistant/components/coolmaster/translations/tr.json index 8950dfa0220..25e214524af 100644 --- a/homeassistant/components/coolmaster/translations/tr.json +++ b/homeassistant/components/coolmaster/translations/tr.json @@ -13,7 +13,8 @@ "heat": "Is\u0131tma modunu destekler", "heat_cool": "Otomatik \u0131s\u0131tma/so\u011futma modunu destekler", "host": "Sunucu", - "off": "Kapat\u0131labilir" + "off": "Kapat\u0131labilir", + "swing_support": "Sal\u0131n\u0131m modunu kontrol et" }, "title": "CoolMasterNet ba\u011flant\u0131 ayr\u0131nt\u0131lar\u0131n\u0131z\u0131 ayarlay\u0131n." } diff --git a/homeassistant/components/coolmaster/translations/zh-Hant.json b/homeassistant/components/coolmaster/translations/zh-Hant.json index 42278561d58..0203e8f53dd 100644 --- a/homeassistant/components/coolmaster/translations/zh-Hant.json +++ b/homeassistant/components/coolmaster/translations/zh-Hant.json @@ -13,7 +13,8 @@ "heat": "\u652f\u63f4\u4fdd\u6696\u6a21\u5f0f", "heat_cool": "\u652f\u63f4\u81ea\u52d5\u4fdd\u6696/\u5236\u51b7\u6a21\u5f0f", "host": "\u4e3b\u6a5f\u7aef", - "off": "\u53ef\u4ee5\u95dc\u9589" + "off": "\u53ef\u4ee5\u95dc\u9589", + "swing_support": "\u63a7\u5236\u64fa\u52d5\u6a21\u5f0f" }, "title": "\u8a2d\u5b9a CoolMasterNet \u9023\u7dda\u8cc7\u8a0a\u3002" } diff --git a/homeassistant/components/coronavirus/config_flow.py b/homeassistant/components/coronavirus/config_flow.py index a5e086c90e0..81e4f06f57f 100644 --- a/homeassistant/components/coronavirus/config_flow.py +++ b/homeassistant/components/coronavirus/config_flow.py @@ -17,7 +17,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - _options = None + _options: dict[str, Any] | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 98bb2f4909f..a3965552b16 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -7,9 +7,8 @@ from datetime import timedelta from enum import IntFlag import functools as ft import logging -from typing import Any, TypeVar, final +from typing import Any, ParamSpec, TypeVar, final -from typing_extensions import ParamSpec import voluptuous as vol from homeassistant.backports.enum import StrEnum diff --git a/homeassistant/components/cpuspeed/translations/tr.json b/homeassistant/components/cpuspeed/translations/tr.json index f5689f2f3b4..141d90c039d 100644 --- a/homeassistant/components/cpuspeed/translations/tr.json +++ b/homeassistant/components/cpuspeed/translations/tr.json @@ -6,7 +6,7 @@ }, "step": { "user": { - "description": "Kuruluma ba\u015flamak ister misiniz?", + "description": "Kurulumu ba\u015flatmak istiyor musunuz?", "title": "\u0130\u015flemci h\u0131z\u0131" } } diff --git a/homeassistant/components/crownstone/translations/el.json b/homeassistant/components/crownstone/translations/el.json index b682dedcb1b..b3aaf2f62a2 100644 --- a/homeassistant/components/crownstone/translations/el.json +++ b/homeassistant/components/crownstone/translations/el.json @@ -15,7 +15,7 @@ "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.", + "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 \u03ae \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \"Don't use USB\" \u03b5\u03ac\u03bd \u03b4\u03b5\u03bd \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\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": { diff --git a/homeassistant/components/crownstone/translations/sk.json b/homeassistant/components/crownstone/translations/sk.json index cea3ff01a26..7d548d3c521 100644 --- a/homeassistant/components/crownstone/translations/sk.json +++ b/homeassistant/components/crownstone/translations/sk.json @@ -15,7 +15,7 @@ "data": { "usb_path": "Cesta k zariadeniu USB" }, - "description": "Vyberte s\u00e9riov\u00fd port Crownstone USB dongle alebo zvo\u013ete \u201eNepou\u017e\u00edva\u0165 USB\u201c, ak nechcete nastavova\u0165 USB dongle. \n\n H\u013eadajte zariadenie s VID 10C4 a PID EA60.", + "description": "Vyberte s\u00e9riov\u00fd port Crownstone USB dongle alebo zvo\u013ete `Nepou\u017e\u00edva\u0165 USB`, ak nechcete nastavova\u0165 USB dongle. \n\n H\u013eadajte zariadenie s VID 10C4 a PID EA60.", "title": "Konfigur\u00e1cia hardv\u00e9rov\u00e9ho k\u013e\u00fa\u010da Crownstone USB" }, "usb_manual_config": { diff --git a/homeassistant/components/crownstone/translations/tr.json b/homeassistant/components/crownstone/translations/tr.json index d244ed239a3..73362f0ba2a 100644 --- a/homeassistant/components/crownstone/translations/tr.json +++ b/homeassistant/components/crownstone/translations/tr.json @@ -15,7 +15,7 @@ "data": { "usb_path": "USB Cihaz Yolu" }, - "description": "Crownstone USB donan\u0131m kilidinin seri ba\u011flant\u0131 noktas\u0131n\u0131 se\u00e7in veya bir USB donan\u0131m kilidi kurmak istemiyorsan\u0131z 'USB kullanma' se\u00e7ene\u011fini se\u00e7in. \n\n VID 10C4 ve PID EA60'a sahip bir cihaz aray\u0131n.", + "description": "Crownstone USB donan\u0131m kilidinin seri ba\u011flant\u0131 noktas\u0131n\u0131 se\u00e7in veya bir USB donan\u0131m kilidi kurmak istemiyorsan\u0131z 'USB kullanma' \u00f6\u011fesini se\u00e7in. \n\n VID 10C4 ve PID EA60'a sahip bir cihaz aray\u0131n.", "title": "Crownstone USB dongle yap\u0131land\u0131rmas\u0131" }, "usb_manual_config": { diff --git a/homeassistant/components/crownstone/translations/uk.json b/homeassistant/components/crownstone/translations/uk.json new file mode 100644 index 00000000000..5c722c2a338 --- /dev/null +++ b/homeassistant/components/crownstone/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/lt.json b/homeassistant/components/daikin/translations/lt.json new file mode 100644 index 00000000000..b8710e82a4c --- /dev/null +++ b/homeassistant/components/daikin/translations/lt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u012erenginys jau sukonfig\u016bruotas" + }, + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/lv.json b/homeassistant/components/daikin/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/daikin/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index 3d24903971e..a1832c6d08a 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -2,7 +2,7 @@ "domain": "debugpy", "name": "Remote Python Debugger", "documentation": "https://www.home-assistant.io/integrations/debugpy", - "requirements": ["debugpy==1.6.4"], + "requirements": ["debugpy==1.6.6"], "codeowners": ["@frenck"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 6163db0dc65..7b0c9383cb3 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Generic, TypeVar, Union +from typing import Generic, TypeVar from pydeconz.models.deconz_device import DeconzDevice as PydeconzDevice from pydeconz.models.group import Group as PydeconzGroup @@ -21,12 +21,7 @@ from .util import serial_from_unique_id _DeviceT = TypeVar( "_DeviceT", - bound=Union[ - PydeconzGroup, - PydeconzLightBase, - PydeconzSensorBase, - PydeconzScene, - ], + bound=PydeconzGroup | PydeconzLightBase | PydeconzSensorBase | PydeconzScene, ) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 590c0795e65..9f8011e3431 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -1,7 +1,7 @@ """Support for deCONZ lights.""" from __future__ import annotations -from typing import Any, TypedDict, TypeVar, Union +from typing import Any, TypedDict, TypeVar from pydeconz.interfaces.groups import GroupHandler from pydeconz.interfaces.lights import LightHandler @@ -47,7 +47,7 @@ DECONZ_TO_COLOR_MODE = { LightColorMode.XY: ColorMode.XY, } -_LightDeviceT = TypeVar("_LightDeviceT", bound=Union[Group, Light]) +_LightDeviceT = TypeVar("_LightDeviceT", bound=Group | Light) class SetStateAttributes(TypedDict, total=False): diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index 9c4c5b43dbe..7afde4ada11 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Union +from typing import Any from pydeconz.models.event import EventType from pydeconz.models.light.lock import Lock @@ -50,7 +50,7 @@ async def async_setup_entry( ) -class DeconzLock(DeconzDevice[Union[DoorLock, Lock]], LockEntity): +class DeconzLock(DeconzDevice[DoorLock | Lock], LockEntity): """Representation of a deCONZ lock.""" TYPE = DOMAIN diff --git a/homeassistant/components/deconz/logbook.py b/homeassistant/components/deconz/logbook.py index 67801b84344..39fe7e98a56 100644 --- a/homeassistant/components/deconz/logbook.py +++ b/homeassistant/components/deconz/logbook.py @@ -17,6 +17,10 @@ from .device_trigger import ( CONF_BUTTON_2, CONF_BUTTON_3, CONF_BUTTON_4, + CONF_BUTTON_5, + CONF_BUTTON_6, + CONF_BUTTON_7, + CONF_BUTTON_8, CONF_CLOSE, CONF_DIM_DOWN, CONF_DIM_UP, @@ -95,6 +99,10 @@ INTERFACES = { CONF_BUTTON_2: "Button 2", CONF_BUTTON_3: "Button 3", CONF_BUTTON_4: "Button 4", + CONF_BUTTON_5: "Button 5", + CONF_BUTTON_6: "Button 6", + CONF_BUTTON_7: "Button 7", + CONF_BUTTON_8: "Button 8", CONF_SIDE_1: "Side 1", CONF_SIDE_2: "Side 2", CONF_SIDE_3: "Side 3", diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 31a8a244b1a..ee27beaa8e2 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -112,7 +112,6 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = ( update_key="airquality", value_fn=lambda device: device.air_quality, instance_check=AirQuality, - state_class=SensorStateClass.MEASUREMENT, ), DeconzSensorDescription[AirQuality]( key="air_quality_ppb", diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 55bb86d03f6..45a19b0466d 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -49,14 +49,14 @@ }, "device_automation": { "trigger_type": { - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", - "remote_button_long_press": "\"{subtype}\" button continuously pressed", - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_double_press": "\"{subtype}\" button double clicked", - "remote_button_triple_press": "\"{subtype}\" button triple clicked", - "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", - "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "remote_button_short_press": "\"{subtype}\" pressed", + "remote_button_short_release": "\"{subtype}\" released", + "remote_button_long_press": "\"{subtype}\" continuously pressed", + "remote_button_long_release": "\"{subtype}\" released after long press", + "remote_button_double_press": "\"{subtype}\" double clicked", + "remote_button_triple_press": "\"{subtype}\" triple clicked", + "remote_button_quadruple_press": "\"{subtype}\" quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" quintuple clicked", "remote_button_rotated": "Button rotated \"{subtype}\"", "remote_button_rotated_fast": "Button rotated fast \"{subtype}\"", "remote_button_rotation_stopped": "Button rotation \"{subtype}\" stopped", diff --git a/homeassistant/components/deconz/translations/ca.json b/homeassistant/components/deconz/translations/ca.json index c9f599429b4..8f57a4499e7 100644 --- a/homeassistant/components/deconz/translations/ca.json +++ b/homeassistant/components/deconz/translations/ca.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "Dispositiu despertat", - "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades", - "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut cont\u00ednuament", - "remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", - "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades", - "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades", + "remote_button_double_press": "\"{subtype}\" clicat dues vegades", + "remote_button_long_press": "\"{subtype}\" premut cont\u00ednuament", + "remote_button_long_release": "\"{subtype}\" alliberat despr\u00e9s d'una estona premut", + "remote_button_quadruple_press": "\"{subtype}\" clicat quatre vegades", + "remote_button_quintuple_press": "\"{subtype}\" clicat cinc vegades", "remote_button_rotated": "Bot\u00f3 \"{subtype}\" girat", "remote_button_rotated_fast": "Bot\u00f3 \"{subtype}\" girat r\u00e0pidament", "remote_button_rotation_stopped": "La rotaci\u00f3 del bot\u00f3 \"{subtype}\" s'ha aturat", - "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", - "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", - "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades", + "remote_button_short_press": "\"{subtype}\" premut", + "remote_button_short_release": "\"{subtype}\" alliberat", + "remote_button_triple_press": "\"{subtype}\" clicat tres vegades", "remote_double_tap": "Dispositiu \"{subtype}\" tocat dues vegades", "remote_double_tap_any_side": "Dispositiu tocat dues vegades a alguna cara", "remote_falling": "Dispositiu en caiguda lliure", diff --git a/homeassistant/components/deconz/translations/de.json b/homeassistant/components/deconz/translations/de.json index 626ccf613cc..374d2a991c5 100644 --- a/homeassistant/components/deconz/translations/de.json +++ b/homeassistant/components/deconz/translations/de.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "Ger\u00e4t aufgeweckt", - "remote_button_double_press": "\"{subtype}\" Taste doppelt angedr\u00fcckt", - "remote_button_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt", - "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen", - "remote_button_quadruple_press": "\"{subtype}\" Taste vierfach gedr\u00fcckt", - "remote_button_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach gedr\u00fcckt", + "remote_button_double_press": "\"{subtype}\" doppelt angedr\u00fcckt", + "remote_button_long_press": "\"{subtype}\" kontinuierlich gedr\u00fcckt", + "remote_button_long_release": "\"{subtype}\" nach langem Dr\u00fccken losgelassen", + "remote_button_quadruple_press": "\"{subtype}\" vierfach gedr\u00fcckt", + "remote_button_quintuple_press": "\"{subtype}\" f\u00fcnffach gedr\u00fcckt", "remote_button_rotated": "Button gedreht \"{subtype}\".", "remote_button_rotated_fast": "Button schnell gedreht \"{subtype}\"", "remote_button_rotation_stopped": "Die Tastendrehung \"{subtype}\" wurde gestoppt", - "remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt", - "remote_button_short_release": "\"{subtype}\" Taste losgelassen", - "remote_button_triple_press": "\"{subtype}\" Taste dreimal gedr\u00fcckt", + "remote_button_short_press": "\"{subtype}\" gedr\u00fcckt", + "remote_button_short_release": "\"{subtype}\" losgelassen", + "remote_button_triple_press": "\"{subtype}\" dreimal gedr\u00fcckt", "remote_double_tap": "Ger\u00e4t \"{subtype}\" doppelt getippt", "remote_double_tap_any_side": "Ger\u00e4t auf beliebiger Seite doppelt angetippt", "remote_falling": "Ger\u00e4t im freien Fall", diff --git a/homeassistant/components/deconz/translations/en.json b/homeassistant/components/deconz/translations/en.json index af73a3b43be..319fe2ca99e 100644 --- a/homeassistant/components/deconz/translations/en.json +++ b/homeassistant/components/deconz/translations/en.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "Device awakened", - "remote_button_double_press": "\"{subtype}\" button double clicked", - "remote_button_long_press": "\"{subtype}\" button continuously pressed", - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", - "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "remote_button_double_press": "\"{subtype}\" double clicked", + "remote_button_long_press": "\"{subtype}\" continuously pressed", + "remote_button_long_release": "\"{subtype}\" released after long press", + "remote_button_quadruple_press": "\"{subtype}\" quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" quintuple clicked", "remote_button_rotated": "Button rotated \"{subtype}\"", "remote_button_rotated_fast": "Button rotated fast \"{subtype}\"", "remote_button_rotation_stopped": "Button rotation \"{subtype}\" stopped", - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", - "remote_button_triple_press": "\"{subtype}\" button triple clicked", + "remote_button_short_press": "\"{subtype}\" pressed", + "remote_button_short_release": "\"{subtype}\" released", + "remote_button_triple_press": "\"{subtype}\" triple clicked", "remote_double_tap": "Device \"{subtype}\" double tapped", "remote_double_tap_any_side": "Device double tapped on any side", "remote_falling": "Device in free fall", diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index 4e0d9ee96fc..8b5c1969b65 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "Dispositivo despertado", - "remote_button_double_press": "Bot\u00f3n \"{subtype}\" pulsado dos veces", - "remote_button_long_press": "Bot\u00f3n \"{subtype}\" pulsado continuamente", - "remote_button_long_release": "Bot\u00f3n \"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", - "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces", - "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces", + "remote_button_double_press": "\"{subtype}\" pulsado dos veces", + "remote_button_long_press": "\"{subtype}\" pulsado continuamente", + "remote_button_long_release": "\"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", + "remote_button_quadruple_press": "\"{subtype}\" pulsado cuatro veces", + "remote_button_quintuple_press": "\"{subtype}\" pulsado cinco veces", "remote_button_rotated": "Bot\u00f3n \"{subtype}\" girado", "remote_button_rotated_fast": "Bot\u00f3n \"{subtype}\" girado r\u00e1pido", "remote_button_rotation_stopped": "Se detuvo la rotaci\u00f3n del bot\u00f3n \"{subtype}\"", - "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", - "remote_button_short_release": "Bot\u00f3n \"{subtype}\" soltado", - "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado tres veces", + "remote_button_short_press": "\"{subtype}\" pulsado", + "remote_button_short_release": "\"{subtype}\" soltado", + "remote_button_triple_press": "\"{subtype}\" pulsado tres veces", "remote_double_tap": "Doble toque en dispositivo \"{subtype}\"", "remote_double_tap_any_side": "Dispositivo con doble toque en cualquier lado", "remote_falling": "Dispositivo en ca\u00edda libre", diff --git a/homeassistant/components/deconz/translations/hu.json b/homeassistant/components/deconz/translations/hu.json index 7bf6d021634..26471d32418 100644 --- a/homeassistant/components/deconz/translations/hu.json +++ b/homeassistant/components/deconz/translations/hu.json @@ -18,7 +18,7 @@ "title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 Home Assistant b\u0151v\u00edtm\u00e9nnyel" }, "link": { - "description": "Enged\u00e9lyezze fel a deCONZ \u00e1tj\u00e1r\u00f3ban a Home Assistanthoz val\u00f3 regisztr\u00e1l\u00e1st.\n\n1. V\u00e1lassza ki a deCONZ rendszer be\u00e1ll\u00edt\u00e1sait\n2. Nyomja meg az \"Authenticate app\" gombot", + "description": "Enged\u00e9lyezze a deCONZ \u00e1tj\u00e1r\u00f3ban a Home Assistanthoz val\u00f3 regisztr\u00e1l\u00e1st.\n\n1. V\u00e1lassza ki a deCONZ rendszer be\u00e1ll\u00edt\u00e1sait\n2. Nyomja meg az \"Authenticate app\" gombot", "title": "Kapcsol\u00f3d\u00e1s a deCONZ-hoz" }, "manual_input": { diff --git a/homeassistant/components/deconz/translations/id.json b/homeassistant/components/deconz/translations/id.json index b6d7329758b..5c542ca2418 100644 --- a/homeassistant/components/deconz/translations/id.json +++ b/homeassistant/components/deconz/translations/id.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "Perangkat terbangun", - "remote_button_double_press": "Tombol \"{subtype}\" diklik dua kali", - "remote_button_long_press": "Tombol \"{subtype}\" terus ditekan", - "remote_button_long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama", - "remote_button_quadruple_press": "Tombol \"{subtype}\" diklik empat kali", - "remote_button_quintuple_press": "Tombol \"{subtype}\" diklik lima kali", + "remote_button_double_press": "\"{subtype}\" diklik dua kali", + "remote_button_long_press": "\"{subtype}\" terus ditekan", + "remote_button_long_release": "\"{subtype}\" dilepaskan setelah ditekan lama", + "remote_button_quadruple_press": "\"{subtype}\" diklik empat kali", + "remote_button_quintuple_press": "\"{subtype}\" diklik lima kali", "remote_button_rotated": "Tombol diputar \"{subtype}\"", "remote_button_rotated_fast": "Tombol diputar cepat \"{subtype}\"", "remote_button_rotation_stopped": "Pemutaran tombol \"{subtype}\" berhenti", - "remote_button_short_press": "Tombol \"{subtype}\" ditekan", - "remote_button_short_release": "Tombol \"{subtype}\" dilepaskan", - "remote_button_triple_press": "Tombol \"{subtype}\" diklik tiga kali", + "remote_button_short_press": "\"{subtype}\" ditekan", + "remote_button_short_release": "\"{subtype}\" dilepaskan", + "remote_button_triple_press": "\"{subtype}\" diklik tiga kali", "remote_double_tap": "Perangkat \"{subtype}\" diketuk dua kali", "remote_double_tap_any_side": "Perangkat diketuk dua kali di sisi mana pun", "remote_falling": "Perangkat jatuh bebas", diff --git a/homeassistant/components/deconz/translations/it.json b/homeassistant/components/deconz/translations/it.json index 638b4db4222..d87927d0c12 100644 --- a/homeassistant/components/deconz/translations/it.json +++ b/homeassistant/components/deconz/translations/it.json @@ -47,7 +47,7 @@ "button_7": "Settimo pulsante", "button_8": "Ottavo pulsante", "close": "Chiudere", - "dim_down": "Diminuire luminosit\u00e0", + "dim_down": "Diminuisce luminosit\u00e0", "dim_up": "Aumenta luminosit\u00e0", "left": "Sinistra", "open": "Aperto", @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "Dispositivo risvegliato", - "remote_button_double_press": "Pulsante \"{subtype}\" cliccato due volte", - "remote_button_long_press": "Pulsante \"{subtype}\" premuto continuamente", - "remote_button_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione", - "remote_button_quadruple_press": "Pulsante \"{subtype}\" cliccato quattro volte", - "remote_button_quintuple_press": "Pulsante \"{subtype}\" cliccato cinque volte", + "remote_button_double_press": "\"{subtype}\" cliccato due volte", + "remote_button_long_press": "\"{subtype}\" premuto continuamente", + "remote_button_long_release": "\"{subtype}\" rilasciato dopo una lunga pressione", + "remote_button_quadruple_press": "\"{subtype}\" cliccato quattro volte", + "remote_button_quintuple_press": "\"{subtype}\" cliccato cinque volte", "remote_button_rotated": "Pulsante ruotato \"{subtype}\"", "remote_button_rotated_fast": "Pulsante ruotato velocemente \"{subtype}\"", "remote_button_rotation_stopped": "La rotazione dei pulsanti \"{subtype}\" si \u00e8 arrestata", - "remote_button_short_press": "Pulsante \"{subtype}\" premuto", - "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", - "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte", + "remote_button_short_press": "\"{subtype}\" premuto", + "remote_button_short_release": "\"{subtype}\" rilasciato", + "remote_button_triple_press": "\"{subtype}\" cliccato tre volte", "remote_double_tap": "Dispositivo \"{subtype}\" toccato due volte", "remote_double_tap_any_side": "Dispositivo toccato due volte su qualsiasi lato", "remote_falling": "Dispositivo in caduta libera", diff --git a/homeassistant/components/deconz/translations/no.json b/homeassistant/components/deconz/translations/no.json index 22a294d3242..b1968267e7d 100644 --- a/homeassistant/components/deconz/translations/no.json +++ b/homeassistant/components/deconz/translations/no.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "Enheten ble vekket", - "remote_button_double_press": "\"{subtype}\"-knappen ble dobbeltklikket", - "remote_button_long_press": "\"{subtype}\"-knappen ble kontinuerlig trykket", - "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", - "remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket", - "remote_button_quintuple_press": "\"{subtype}\"-knappen femdobbelt klikket", + "remote_button_double_press": "\" {subtype} \" dobbeltklikket", + "remote_button_long_press": "\" {subtype} \" kontinuerlig trykket", + "remote_button_long_release": "\" {subtype} \" utgitt etter lang trykk", + "remote_button_quadruple_press": "\" {subtype} \" firedoblet klikk", + "remote_button_quintuple_press": "\" {subtype} \" femdobbelt klikket", "remote_button_rotated": "Knappen roterte \"{subtype}\"", "remote_button_rotated_fast": "Knappen roterte raskt \"{subtype}\"", "remote_button_rotation_stopped": "Knapperotasjon \"{subtype}\" stoppet", - "remote_button_short_press": "\"{subtype}\"-knappen ble trykket", - "remote_button_short_release": "\"{subtype}\"-knappen sluppet", - "remote_button_triple_press": "\"{subtype}\"-knappen trippel klikket", + "remote_button_short_press": "\" {subtype} \" trykket", + "remote_button_short_release": "\" {subtype} \" utgitt", + "remote_button_triple_press": "\" {subtype} \" trippelklikket", "remote_double_tap": "Enheten \"{subtype}\" dobbeltklikket", "remote_double_tap_any_side": "Enheten dobbeltklikket p\u00e5 alle sider", "remote_falling": "Enheten er i fritt fall", diff --git a/homeassistant/components/deconz/translations/pl.json b/homeassistant/components/deconz/translations/pl.json index 7894494336e..7e3257cdc3b 100644 --- a/homeassistant/components/deconz/translations/pl.json +++ b/homeassistant/components/deconz/translations/pl.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "urz\u0105dzenie si\u0119 obudzi", - "remote_button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", - "remote_button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", - "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "remote_button_quadruple_press": "przycisk \"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty", - "remote_button_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_double_press": "\"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "\"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", "remote_button_rotated": "przycisk zostanie obr\u00f3cony \"{subtype}\"", "remote_button_rotated_fast": "przycisk zostanie szybko obr\u00f3cony \"{subtype}\"", "remote_button_rotation_stopped": "nast\u0105pi zatrzymanie obrotu przycisku \"{subtype}\"", - "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", - "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", - "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty", + "remote_button_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty", + "remote_button_short_release": "\"{subtype}\" zostanie zwolniony", + "remote_button_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty", "remote_double_tap": "urz\u0105dzenie \"{subtype}\" zostanie dwukrotnie pukni\u0119te", "remote_double_tap_any_side": "urz\u0105dzenie dwukrotnie pukni\u0119te z dowolnej strony", "remote_falling": "urz\u0105dzenie zarejestruje swobodny spadek", diff --git a/homeassistant/components/deconz/translations/pt-BR.json b/homeassistant/components/deconz/translations/pt-BR.json index 785ada6b4c3..aa847366f05 100644 --- a/homeassistant/components/deconz/translations/pt-BR.json +++ b/homeassistant/components/deconz/translations/pt-BR.json @@ -64,17 +64,17 @@ }, "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_double_press": "\"{subtype}\" duplo clique", + "remote_button_long_press": "\"{subtype}\" pressionado continuamente", + "remote_button_long_release": "\"{subtype}\" liberado ap\u00f3s press\u00e3o longa", + "remote_button_quadruple_press": "\"{subtype}\" qu\u00e1druplo clicado", + "remote_button_quintuple_press": "\"{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_button_short_press": "\"{subtype}\" pressionado", + "remote_button_short_release": "\"{subtype}\" liberados", + "remote_button_triple_press": "\"{subtype}\" triplo clique", "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", diff --git a/homeassistant/components/deconz/translations/ru.json b/homeassistant/components/deconz/translations/ru.json index 91c44036331..962d31737af 100644 --- a/homeassistant/components/deconz/translations/ru.json +++ b/homeassistant/components/deconz/translations/ru.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u043b\u0438", - "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", - "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u0434\u043e\u043b\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0430", - "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", - "remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", - "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", + "remote_button_double_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", + "remote_button_long_press": "\"{subtype}\" \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0436\u0430\u0442\u043e\u0439", + "remote_button_long_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_quadruple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", + "remote_button_quintuple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", "remote_button_rotated": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u0430", "remote_button_rotated_fast": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u0430 \u0431\u044b\u0441\u0442\u0440\u043e", "remote_button_rotation_stopped": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u0440\u0435\u043a\u0440\u0430\u0442\u0438\u043b\u0430 \u0432\u0440\u0430\u0449\u0435\u043d\u0438\u0435", - "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", - "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", - "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430", + "remote_button_short_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_short_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_triple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430", "remote_double_tap": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c {subtype} \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \u0434\u0432\u0430\u0436\u0434\u044b", "remote_double_tap_any_side": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \u0434\u0432\u0430\u0436\u0434\u044b", "remote_falling": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432 \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u043e\u043c \u043f\u0430\u0434\u0435\u043d\u0438\u0438", diff --git a/homeassistant/components/deconz/translations/sk.json b/homeassistant/components/deconz/translations/sk.json index 61886b254e8..2e64be75e42 100644 --- a/homeassistant/components/deconz/translations/sk.json +++ b/homeassistant/components/deconz/translations/sk.json @@ -18,7 +18,7 @@ "title": "Br\u00e1na deCONZ Zigbee prostredn\u00edctvom doplnku Home Assistant" }, "link": { - "description": "Odomknite svoju br\u00e1nu deCONZ a zaregistrujte sa v aplik\u00e1cii Home Assistant. \n\n 1. Cho\u010fte do deCONZ Settings - > Gateway - > Advanced\n 2. Stla\u010dte tla\u010didlo \u201eAutentifik\u00e1cia aplik\u00e1cie\u201c.", + "description": "Odomknite svoju br\u00e1nu deCONZ a zaregistrujte sa v aplik\u00e1cii Home Assistant. \n\n 1. Cho\u010fte do deCONZ Settings - > Gateway - > Advanced\n 2. Stla\u010dte tla\u010didlo \"Autentifik\u00e1cia aplik\u00e1cie\".", "title": "Prepojenie s deCONZ" }, "manual_input": { @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "Zariadenie sa prebudilo", - "remote_button_double_press": "dvojklik na tla\u010didlo \u201e{subtype}\u201c", - "remote_button_long_press": "Trvalo stla\u010den\u00e9 tla\u010didlo \"{subtype}\"", - "remote_button_long_release": "Tla\u010didlo \"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed", - "remote_button_quadruple_press": "Tla\u010didlo \"{subtype}\" kliknut\u00e9 \u0161tyrikr\u00e1t", - "remote_button_quintuple_press": "Tla\u010didlo \"{subtype}\" kliknut\u00e9 p\u00e4\u0165kr\u00e1t", - "remote_button_rotated": "Oto\u010den\u00e9 tla\u010didlo \u201e{subtype}\u201c", - "remote_button_rotated_fast": "Tla\u010didlo sa r\u00fdchlo ot\u00e1\u010dalo \u201e{subtype}\u201c", + "remote_button_double_press": "\"{subtype}\" kliknut\u00e9 dvakr\u00e1t", + "remote_button_long_press": "\"{subtype}\" trvalo stla\u010den\u00e9", + "remote_button_long_release": "\"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed", + "remote_button_quadruple_press": "\"{subtype}\" kliknut\u00e9 \u0161tyrikr\u00e1t", + "remote_button_quintuple_press": "\"{subtype}\" kliknut\u00e9 p\u00e4\u0165kr\u00e1t", + "remote_button_rotated": "Oto\u010den\u00e9 tla\u010didlo \"{subtype}\"", + "remote_button_rotated_fast": "Tla\u010didlo sa r\u00fdchlo ot\u00e1\u010dalo \"{subtype}\"", "remote_button_rotation_stopped": "Oto\u010denie tla\u010didla \"{subtype}\" bolo zastaven\u00e9", - "remote_button_short_press": "Stla\u010den\u00e9 tla\u010didlo \"{subtype}\"", - "remote_button_short_release": "Tla\u010didlo \"{subtype}\" bolo uvo\u013enen\u00e9", - "remote_button_triple_press": "Trojklik na tla\u010didlo \"{subtype}\"", + "remote_button_short_press": "\"{subtype}\" stla\u010den\u00e9", + "remote_button_short_release": "\"{subtype}\" bolo uvo\u013enen\u00e9", + "remote_button_triple_press": "\"{subtype}\" trojn\u00e1sobne kliknut\u00e9", "remote_double_tap": "Zariadenie \"{subtype}\" dvojit\u00e9 klepnutie", "remote_double_tap_any_side": "Zariadenie dvakr\u00e1t klepnut\u00e9 na \u013eubovo\u013en\u00fa stranu", "remote_falling": "Zariadenie vo vo\u013enom p\u00e1de", diff --git a/homeassistant/components/deconz/translations/zh-Hant.json b/homeassistant/components/deconz/translations/zh-Hant.json index 8987441fc9a..7c3673f8e4a 100644 --- a/homeassistant/components/deconz/translations/zh-Hant.json +++ b/homeassistant/components/deconz/translations/zh-Hant.json @@ -64,17 +64,17 @@ }, "trigger_type": { "remote_awakened": "\u88dd\u7f6e\u5df2\u559a\u9192", - "remote_button_double_press": "\"{subtype}\" \u6309\u9215\u96d9\u64ca", - "remote_button_long_press": "\"{subtype}\" \u6309\u9215\u6301\u7e8c\u6309\u4e0b", - "remote_button_long_release": "\u9577\u6309\u5f8c\u91cb\u653e \"{subtype}\" \u6309\u9215", - "remote_button_quadruple_press": "\"{subtype}\" \u6309\u9215\u56db\u9023\u9ede\u64ca", - "remote_button_quintuple_press": "\"{subtype}\" \u6309\u9215\u4e94\u9023\u9ede\u64ca", + "remote_button_double_press": "\"{subtype}\" \u96d9\u64ca", + "remote_button_long_press": "\"{subtype}\" \u6301\u7e8c\u6309\u4e0b", + "remote_button_long_release": "\"{subtype}\" \u9577\u6309\u5f8c\u91cb\u653e", + "remote_button_quadruple_press": "\"{subtype}\" \u56db\u9023\u64ca", + "remote_button_quintuple_press": "\"{subtype}\" \u4e94\u9023\u9ede\u64ca", "remote_button_rotated": "\u65cb\u8f49 \"{subtype}\" \u6309\u9215", "remote_button_rotated_fast": "\u5feb\u901f\u65cb\u8f49 \"{subtype}\" \u6309\u9215", "remote_button_rotation_stopped": "\u65cb\u8f49 \"{subtype}\" \u6309\u9215\u5df2\u505c\u6b62", - "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", - "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", - "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u9ede\u64ca", + "remote_button_short_press": "\"{subtype}\" \u5df2\u6309\u4e0b", + "remote_button_short_release": "\"{subtype}\" \u5df2\u91cb\u653e", + "remote_button_triple_press": "\"{subtype}\" \u4e09\u9023\u9ede\u64ca", "remote_double_tap": "\u88dd\u7f6e \"{subtype}\" \u96d9\u6572", "remote_double_tap_any_side": "\u88dd\u7f6e\u4efb\u4e00\u9762\u96d9\u9ede\u9078", "remote_falling": "\u88dd\u7f6e\u81ea\u7531\u843d\u4e0b", diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index c6fae73bc28..b46732178b8 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -6,11 +6,10 @@ import copy from functools import wraps import logging import time -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar from bluepy.btle import BTLEException # pylint: disable=import-error import decora # pylint: disable=import-error -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant import util @@ -82,7 +81,7 @@ def retry( "Decora connect error for device %s. Reconnecting", device.name, ) - # pylint: disable=protected-access + # pylint: disable-next=protected-access device._switch.connect() return wrapper_retry diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 8e1a2f01168..c6568db3fbf 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -7,6 +7,7 @@ "automation", "bluetooth", "cloud", + "conversation", "counter", "dhcp", "energy", diff --git a/homeassistant/components/deluge/coordinator.py b/homeassistant/components/deluge/coordinator.py index 89f9afc31ad..9b0d5907b1a 100644 --- a/homeassistant/components/deluge/coordinator.py +++ b/homeassistant/components/deluge/coordinator.py @@ -4,6 +4,7 @@ from __future__ import annotations from datetime import timedelta import socket from ssl import SSLError +from typing import Any from deluge_client.client import DelugeRPCClient, FailedToReconnectException @@ -16,7 +17,9 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DATA_KEYS, LOGGER -class DelugeDataUpdateCoordinator(DataUpdateCoordinator): +class DelugeDataUpdateCoordinator( + DataUpdateCoordinator[dict[Platform, dict[str, Any]]] +): """Data update coordinator for the Deluge integration.""" config_entry: ConfigEntry @@ -34,7 +37,7 @@ class DelugeDataUpdateCoordinator(DataUpdateCoordinator): self.api = api self.config_entry = entry - async def _async_update_data(self) -> dict[Platform, dict[str, int | str]]: + async def _async_update_data(self) -> dict[Platform, dict[str, Any]]: """Get the latest data from Deluge and updates the state.""" data = {} try: diff --git a/homeassistant/components/deluge/switch.py b/homeassistant/components/deluge/switch.py index c25c1752ee4..5b3989384cd 100644 --- a/homeassistant/components/deluge/switch.py +++ b/homeassistant/components/deluge/switch.py @@ -45,7 +45,7 @@ class DelugeSwitch(DelugeEntity, SwitchEntity): def is_on(self) -> bool: """Return state of the switch.""" if self.coordinator.data: - data: dict = self.coordinator.data[Platform.SWITCH] + data = self.coordinator.data[Platform.SWITCH] for torrent in data.values(): item = torrent.popitem() if not item[1]: diff --git a/homeassistant/components/deluge/translations/uk.json b/homeassistant/components/deluge/translations/uk.json new file mode 100644 index 00000000000..e9180b28e78 --- /dev/null +++ b/homeassistant/components/deluge/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index d4c07cfa730..13e8e135394 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -2,33 +2,20 @@ from __future__ import annotations import asyncio -import datetime -from random import random from homeassistant import config_entries, setup from homeassistant.components import persistent_notification -from homeassistant.components.recorder import get_instance -from homeassistant.components.recorder.models import StatisticData, StatisticMetaData -from homeassistant.components.recorder.statistics import ( - async_add_external_statistics, - get_last_statistics, -) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, Platform, - UnitOfEnergy, UnitOfSoundPressure, - UnitOfTemperature, - UnitOfVolume, ) import homeassistant.core as ha from homeassistant.core import Event, HomeAssistant from homeassistant.helpers.discovery import async_load_platform -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType -import homeassistant.util.dt as dt_util DOMAIN = "demo" @@ -186,192 +173,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass.bus.async_listen(EVENT_HOMEASSISTANT_START, demo_start_listener) - # Create issues - async_create_issue( - hass, - DOMAIN, - "transmogrifier_deprecated", - breaks_in_ha_version="2023.1.1", - is_fixable=False, - learn_more_url="https://en.wiktionary.org/wiki/transmogrifier", - severity=IssueSeverity.WARNING, - translation_key="transmogrifier_deprecated", - ) - - async_create_issue( - hass, - DOMAIN, - "out_of_blinker_fluid", - breaks_in_ha_version="2023.1.1", - is_fixable=True, - learn_more_url="https://www.youtube.com/watch?v=b9rntRxLlbU", - severity=IssueSeverity.CRITICAL, - translation_key="out_of_blinker_fluid", - ) - - async_create_issue( - hass, - DOMAIN, - "unfixable_problem", - is_fixable=False, - learn_more_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ", - severity=IssueSeverity.WARNING, - translation_key="unfixable_problem", - ) - - async_create_issue( - hass, - DOMAIN, - "bad_psu", - is_fixable=True, - learn_more_url="https://www.youtube.com/watch?v=b9rntRxLlbU", - severity=IssueSeverity.CRITICAL, - translation_key="bad_psu", - ) - - async_create_issue( - hass, - DOMAIN, - "cold_tea", - is_fixable=True, - severity=IssueSeverity.WARNING, - translation_key="cold_tea", - ) - return True -def _generate_mean_statistics( - start: datetime.datetime, end: datetime.datetime, init_value: float, max_diff: float -) -> list[StatisticData]: - statistics: list[StatisticData] = [] - mean = init_value - now = start - while now < end: - mean = mean + random() * max_diff - max_diff / 2 - statistics.append( - { - "start": now, - "mean": mean, - "min": mean - random() * max_diff, - "max": mean + random() * max_diff, - } - ) - now = now + datetime.timedelta(hours=1) - - return statistics - - -async def _insert_sum_statistics( - hass: HomeAssistant, - metadata: StatisticMetaData, - start: datetime.datetime, - end: datetime.datetime, - max_diff: float, -) -> None: - statistics: list[StatisticData] = [] - now = start - sum_ = 0.0 - statistic_id = metadata["statistic_id"] - - last_stats = await get_instance(hass).async_add_executor_job( - get_last_statistics, hass, 1, statistic_id, False, {"sum"} - ) - if statistic_id in last_stats: - sum_ = last_stats[statistic_id][0]["sum"] or 0 - while now < end: - sum_ = sum_ + random() * max_diff - statistics.append( - { - "start": now, - "sum": sum_, - } - ) - now = now + datetime.timedelta(hours=1) - - async_add_external_statistics(hass, metadata, statistics) - - -async def _insert_statistics(hass: HomeAssistant) -> None: - """Insert some fake statistics.""" - now = dt_util.now() - yesterday = now - datetime.timedelta(days=1) - yesterday_midnight = yesterday.replace(hour=0, minute=0, second=0, microsecond=0) - today_midnight = yesterday_midnight + datetime.timedelta(days=1) - - # Fake yesterday's temperatures - metadata: StatisticMetaData = { - "source": DOMAIN, - "name": "Outdoor temperature", - "statistic_id": f"{DOMAIN}:temperature_outdoor", - "unit_of_measurement": UnitOfTemperature.CELSIUS, - "has_mean": True, - "has_sum": False, - } - statistics = _generate_mean_statistics(yesterday_midnight, today_midnight, 15, 1) - async_add_external_statistics(hass, metadata, statistics) - - # Add external energy consumption in kWh, ~ 12 kWh / day - # This should be possible to pick for the energy dashboard - metadata = { - "source": DOMAIN, - "name": "Energy consumption 1", - "statistic_id": f"{DOMAIN}:energy_consumption_kwh", - "unit_of_measurement": UnitOfEnergy.KILO_WATT_HOUR, - "has_mean": False, - "has_sum": True, - } - await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 1) - - # Add external energy consumption in MWh, ~ 12 kWh / day - # This should not be possible to pick for the energy dashboard - metadata = { - "source": DOMAIN, - "name": "Energy consumption 2", - "statistic_id": f"{DOMAIN}:energy_consumption_mwh", - "unit_of_measurement": UnitOfEnergy.MEGA_WATT_HOUR, - "has_mean": False, - "has_sum": True, - } - await _insert_sum_statistics( - hass, metadata, yesterday_midnight, today_midnight, 0.001 - ) - - # Add external gas consumption in m³, ~6 m3/day - # This should be possible to pick for the energy dashboard - metadata = { - "source": DOMAIN, - "name": "Gas consumption 1", - "statistic_id": f"{DOMAIN}:gas_consumption_m3", - "unit_of_measurement": UnitOfVolume.CUBIC_METERS, - "has_mean": False, - "has_sum": True, - } - await _insert_sum_statistics( - hass, metadata, yesterday_midnight, today_midnight, 0.5 - ) - - # Add external gas consumption in ft³, ~180 ft3/day - # This should not be possible to pick for the energy dashboard - metadata = { - "source": DOMAIN, - "name": "Gas consumption 2", - "statistic_id": f"{DOMAIN}:gas_consumption_ft3", - "unit_of_measurement": UnitOfVolume.CUBIC_FEET, - "has_mean": False, - "has_sum": True, - } - await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 15) - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set the config entry up.""" # Set up demo platforms with config entry await hass.config_entries.async_forward_entry_setups( config_entry, COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM ) - if "recorder" in hass.config.components: - await _insert_statistics(hass) return True diff --git a/homeassistant/components/demo/image_processing.py b/homeassistant/components/demo/image_processing.py index dc5565a1771..21322f49718 100644 --- a/homeassistant/components/demo/image_processing.py +++ b/homeassistant/components/demo/image_processing.py @@ -5,9 +5,6 @@ from homeassistant.components.image_processing import ( FaceInformation, ImageProcessingFaceEntity, ) -from homeassistant.components.openalpr_local.image_processing import ( - ImageProcessingAlprEntity, -) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -22,44 +19,11 @@ def setup_platform( """Set up the demo image processing platform.""" add_entities( [ - DemoImageProcessingAlpr("camera.demo_camera", "Demo Alpr"), DemoImageProcessingFace("camera.demo_camera", "Demo Face"), ] ) -class DemoImageProcessingAlpr(ImageProcessingAlprEntity): - """Demo ALPR image processing entity.""" - - def __init__(self, camera_entity: str, name: str) -> None: - """Initialize demo ALPR image processing entity.""" - super().__init__() - - self._attr_name = name - self._camera = camera_entity - - @property - def camera_entity(self) -> str: - """Return camera entity id from process pictures.""" - return self._camera - - @property - def confidence(self) -> int: - """Return minimum confidence for send events.""" - return 80 - - def process_image(self, image: bytes) -> None: - """Process image.""" - demo_data = { - "AC3829": 98.3, - "BE392034": 95.5, - "CD02394": 93.4, - "DF923043": 90.8, - } - - self.process_plates(demo_data, 1) - - class DemoImageProcessingFace(ImageProcessingFaceEntity): """Demo face identify image processing entity.""" diff --git a/homeassistant/components/demo/lock.py b/homeassistant/components/demo/lock.py index 47187d5ffc6..e75c2074aab 100644 --- a/homeassistant/components/demo/lock.py +++ b/homeassistant/components/demo/lock.py @@ -1,4 +1,4 @@ -"""Demo lock platform that has two fake locks.""" +"""Demo lock platform that implements locks.""" from __future__ import annotations import asyncio diff --git a/homeassistant/components/demo/manifest.json b/homeassistant/components/demo/manifest.json index df6fa494079..bce79f11881 100644 --- a/homeassistant/components/demo/manifest.json +++ b/homeassistant/components/demo/manifest.json @@ -2,7 +2,6 @@ "domain": "demo", "name": "Demo", "documentation": "https://www.home-assistant.io/integrations/demo", - "after_dependencies": ["recorder"], "dependencies": ["conversation", "group", "zone"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", diff --git a/homeassistant/components/demo/translations/ca.json b/homeassistant/components/demo/translations/ca.json index 44c1ff5c368..241c7feb1e9 100644 --- a/homeassistant/components/demo/translations/ca.json +++ b/homeassistant/components/demo/translations/ca.json @@ -5,10 +5,10 @@ "state_attributes": { "fan_mode": { "state": { - "auto_high": "Autom\u00e0tic alt", - "auto_low": "Autom\u00e0tic baix", - "on_high": "ON alt", - "on_low": "ON baix" + "auto_high": "Autom\u00e0tic (alt)", + "auto_low": "Autom\u00e0tic (baix)", + "on_high": "Engegat (alt)", + "on_low": "Engegat (baix)" } }, "swing_mode": { @@ -17,7 +17,7 @@ "2": "2", "3": "3", "auto": "Autom\u00e0tic", - "off": "OFF" + "off": "Desactivat" } } } @@ -27,7 +27,7 @@ "speed": { "state": { "light_speed": "Velocitat de la llum", - "ludicrous_speed": "Velocitat Ludicrous", + "ludicrous_speed": "Velocitat insensata", "ridiculous_speed": "Velocitat rid\u00edcula" } } diff --git a/homeassistant/components/demo/translations/el.json b/homeassistant/components/demo/translations/el.json index 94d3048e5b9..38ee71ffa9b 100644 --- a/homeassistant/components/demo/translations/el.json +++ b/homeassistant/components/demo/translations/el.json @@ -76,7 +76,7 @@ "fix_flow": { "step": { "confirm": { - "description": "\u03a0\u03b9\u03ad\u03c3\u03c4\u03b5 OK \u03cc\u03c4\u03b1\u03bd \u03c4\u03bf \u03c5\u03b3\u03c1\u03cc \u03c4\u03c9\u03bd \u03c6\u03bb\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03be\u03b1\u03bd\u03b1\u03b3\u03b5\u03bc\u03af\u03c3\u03b5\u03b9.", + "description": "\u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 SUBMIT \u03cc\u03c4\u03b1\u03bd \u03c4\u03bf \u03c5\u03b3\u03c1\u03cc \u03c4\u03c9\u03bd \u03c6\u03bb\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03be\u03b1\u03bd\u03b1\u03b3\u03b5\u03bc\u03af\u03c3\u03b5\u03b9.", "title": "\u03a4\u03bf \u03c5\u03b3\u03c1\u03cc \u03c4\u03c9\u03bd \u03c6\u03bb\u03b1\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03b1\u03bd\u03b1\u03b3\u03b5\u03bc\u03af\u03c3\u03b5\u03b9" } } diff --git a/homeassistant/components/demo/translations/fr.json b/homeassistant/components/demo/translations/fr.json index 754400b5bed..00564e59caa 100644 --- a/homeassistant/components/demo/translations/fr.json +++ b/homeassistant/components/demo/translations/fr.json @@ -1,4 +1,14 @@ { + "entity": { + "sensor": { + "thermostat_mode": { + "state": { + "away": "Absent", + "comfort": "Confort" + } + } + } + }, "issues": { "bad_psu": { "fix_flow": { diff --git a/homeassistant/components/demo/translations/lt.json b/homeassistant/components/demo/translations/lt.json new file mode 100644 index 00000000000..7dcf1541a15 --- /dev/null +++ b/homeassistant/components/demo/translations/lt.json @@ -0,0 +1,27 @@ +{ + "entity": { + "climate": { + "ubercool": { + "state_attributes": { + "fan_mode": { + "state": { + "auto_high": "Auto auk\u0161tas", + "auto_low": "Auto \u017eemas", + "on_high": "\u012ejungta auk\u0161tas", + "on_low": "\u012ejungta \u017eemas" + } + }, + "swing_mode": { + "state": { + "1": "1", + "2": "2", + "3": "3", + "auto": "Auto", + "off": "I\u0161jungta" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/nl.json b/homeassistant/components/demo/translations/nl.json index 3bffdfe51bb..feea4c24381 100644 --- a/homeassistant/components/demo/translations/nl.json +++ b/homeassistant/components/demo/translations/nl.json @@ -23,6 +23,15 @@ } } }, + "select": { + "speed": { + "state": { + "light_speed": "Lichtsnelheid", + "ludicrous_speed": "Bespottelijke snelheid", + "ridiculous_speed": "Belachelijke snelheid" + } + } + }, "vacuum": { "model_s": { "state_attributes": { @@ -51,6 +60,9 @@ } }, "title": "De thee is koud" + }, + "unfixable_problem": { + "title": "Dit is geen oplosbaar probleem" } }, "options": { diff --git a/homeassistant/components/demo/translations/pl.json b/homeassistant/components/demo/translations/pl.json index d42e9e256bd..8d6df7da6e9 100644 --- a/homeassistant/components/demo/translations/pl.json +++ b/homeassistant/components/demo/translations/pl.json @@ -1,5 +1,28 @@ { "entity": { + "climate": { + "ubercool": { + "state_attributes": { + "fan_mode": { + "state": { + "auto_high": "wysoki (auto)", + "auto_low": "niski (auto)", + "on_high": "wysoki", + "on_low": "niski" + } + }, + "swing_mode": { + "state": { + "1": "1", + "2": "2", + "3": "3", + "auto": "auto", + "off": "wy\u0142." + } + } + } + } + }, "select": { "speed": { "state": { @@ -18,6 +41,15 @@ "sleep": "noc" } } + }, + "vacuum": { + "model_s": { + "state_attributes": { + "cleaned_area": { + "name": "wyczyszczony obszar" + } + } + } } }, "issues": { diff --git a/homeassistant/components/demo/translations/tr.json b/homeassistant/components/demo/translations/tr.json index 2c3f9a44200..c675be6b497 100644 --- a/homeassistant/components/demo/translations/tr.json +++ b/homeassistant/components/demo/translations/tr.json @@ -1,4 +1,57 @@ { + "entity": { + "climate": { + "ubercool": { + "state_attributes": { + "fan_mode": { + "state": { + "auto_high": "Otomatik Y\u00fcksek", + "auto_low": "Otomatik D\u00fc\u015f\u00fck", + "on_high": "Y\u00fcksekte", + "on_low": "D\u00fc\u015f\u00fckte" + } + }, + "swing_mode": { + "state": { + "1": "1", + "2": "2", + "3": "3", + "auto": "Otomatik", + "off": "Kapal\u0131" + } + } + } + } + }, + "select": { + "speed": { + "state": { + "light_speed": "I\u015f\u0131k h\u0131z\u0131", + "ludicrous_speed": "Sa\u00e7ma H\u0131z", + "ridiculous_speed": "Anlams\u0131z H\u0131z" + } + } + }, + "sensor": { + "thermostat_mode": { + "state": { + "away": "D\u0131\u015far\u0131da", + "comfort": "Konfor", + "eco": "Eko", + "sleep": "Uyku" + } + } + }, + "vacuum": { + "model_s": { + "state_attributes": { + "cleaned_area": { + "name": "Temizlenmi\u015f Alan" + } + } + } + } + }, "issues": { "bad_psu": { "fix_flow": { @@ -11,6 +64,14 @@ }, "title": "G\u00fc\u00e7 kayna\u011f\u0131 stabil de\u011fil" }, + "cold_tea": { + "fix_flow": { + "abort": { + "not_tea_time": "\u015eu anda \u00e7ay yeniden \u0131s\u0131t\u0131lam\u0131yor" + } + }, + "title": "\u00c7ay so\u011fuk" + }, "out_of_blinker_fluid": { "fix_flow": { "step": { diff --git a/homeassistant/components/demo/translations/uk.json b/homeassistant/components/demo/translations/uk.json index 5ac1ac74708..06c23f5c64f 100644 --- a/homeassistant/components/demo/translations/uk.json +++ b/homeassistant/components/demo/translations/uk.json @@ -1,4 +1,20 @@ { + "entity": { + "climate": { + "ubercool": { + "state_attributes": { + "swing_mode": { + "state": { + "1": "1", + "2": "2", + "3": "3", + "auto": "\u0410\u0432\u0442\u043e" + } + } + } + } + } + }, "options": { "step": { "options_1": { diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index c1f864c8c2f..60da3df393e 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -110,14 +110,14 @@ class DenonDevice(MediaPlayerEntity): def _setup_sources(self, telnet): # NSFRN - Network name - nsfrn = self.telnet_request(telnet, "NSFRN ?")[len("NSFRN ") :] + nsfrn = self.telnet_request(telnet, "NSFRN ?").removeprefix("NSFRN ") if nsfrn: self._name = nsfrn # SSFUN - Configured sources with (optional) names self._source_list = {} for line in self.telnet_request(telnet, "SSFUN ?", all_lines=True): - ssfun = line[len("SSFUN") :].split(" ", 1) + ssfun = line.removeprefix("SSFUN").split(" ", 1) source = ssfun[0] if len(ssfun) == 2 and ssfun[1]: @@ -130,7 +130,7 @@ class DenonDevice(MediaPlayerEntity): # SSSOD - Deleted sources for line in self.telnet_request(telnet, "SSSOD ?", all_lines=True): - source, status = line[len("SSSOD") :].split(" ", 1) + source, status = line.removeprefix("SSSOD").split(" ", 1) if status == "DEL": for pretty_name, name in self._source_list.items(): if source == name: @@ -184,9 +184,9 @@ class DenonDevice(MediaPlayerEntity): self._volume_max = int(line[len("MVMAX ") : len("MVMAX XX")]) continue if line.startswith("MV"): - self._volume = int(line[len("MV") :]) + self._volume = int(line.removeprefix("MV")) self._muted = self.telnet_request(telnet, "MU?") == "MUON" - self._mediasource = self.telnet_request(telnet, "SI?")[len("SI") :] + self._mediasource = self.telnet_request(telnet, "SI?").removeprefix("SI") if self._mediasource in MEDIA_MODES.values(): self._mediainfo = "" @@ -202,7 +202,7 @@ class DenonDevice(MediaPlayerEntity): "NSE8", ] for line in self.telnet_request(telnet, "NSE", all_lines=True): - self._mediainfo += f"{line[len(answer_codes.pop(0)) :]}\n" + self._mediainfo += f"{line.removeprefix(answer_codes.pop(0))}\n" else: self._mediainfo = self.source diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index e1bd0f41808..c1c5a90dbac 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -5,7 +5,7 @@ from collections.abc import Awaitable, Callable, Coroutine from datetime import timedelta from functools import wraps import logging -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from denonavr import DenonAVR from denonavr.const import POWER_ON, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING @@ -16,7 +16,6 @@ from denonavr.exceptions import ( AvrTimoutError, DenonAvrError, ) -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.components.media_player import ( diff --git a/homeassistant/components/denonavr/translations/lv.json b/homeassistant/components/denonavr/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/denonavr/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/uk.json b/homeassistant/components/denonavr/translations/uk.json index dcc68648fcc..23928122c44 100644 --- a/homeassistant/components/denonavr/translations/uk.json +++ b/homeassistant/components/denonavr/translations/uk.json @@ -34,6 +34,7 @@ "init": { "data": { "show_all_sources": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u0432\u0441\u0456 \u0434\u0436\u0435\u0440\u0435\u043b\u0430", + "update_audyssey": "\u041e\u043d\u043e\u0432\u0456\u0442\u044c \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Audyssey", "zone2": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u043e\u043d\u0438 2", "zone3": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u043e\u043d\u0438 3" }, diff --git a/homeassistant/components/derivative/config_flow.py b/homeassistant/components/derivative/config_flow.py index f0ce6803719..e7c7a44117a 100644 --- a/homeassistant/components/derivative/config_flow.py +++ b/homeassistant/components/derivative/config_flow.py @@ -34,10 +34,10 @@ UNIT_PREFIXES = [ selector.SelectOptionDict(value="P", label="P (peta)"), ] TIME_UNITS = [ - selector.SelectOptionDict(value=UnitOfTime.SECONDS, label="Seconds"), - selector.SelectOptionDict(value=UnitOfTime.MINUTES, label="Minutes"), - selector.SelectOptionDict(value=UnitOfTime.HOURS, label="Hours"), - selector.SelectOptionDict(value=UnitOfTime.DAYS, label="Days"), + UnitOfTime.SECONDS, + UnitOfTime.MINUTES, + UnitOfTime.HOURS, + UnitOfTime.DAYS, ] OPTIONS_SCHEMA = vol.Schema( @@ -55,7 +55,9 @@ OPTIONS_SCHEMA = vol.Schema( selector.SelectSelectorConfig(options=UNIT_PREFIXES), ), vol.Required(CONF_UNIT_TIME, default=UnitOfTime.HOURS): selector.SelectSelector( - selector.SelectSelectorConfig(options=TIME_UNITS), + selector.SelectSelectorConfig( + options=TIME_UNITS, translation_key="time_unit" + ), ), } ) diff --git a/homeassistant/components/derivative/strings.json b/homeassistant/components/derivative/strings.json index 0a58b28a1c6..35f1679a31b 100644 --- a/homeassistant/components/derivative/strings.json +++ b/homeassistant/components/derivative/strings.json @@ -39,5 +39,15 @@ } } } + }, + "selector": { + "time_unit": { + "options": { + "s": "Seconds", + "min": "Minutes", + "h": "Hours", + "d": "Days" + } + } } } diff --git a/homeassistant/components/derivative/translations/bg.json b/homeassistant/components/derivative/translations/bg.json index b6c3577d1d0..922e3c59ff3 100644 --- a/homeassistant/components/derivative/translations/bg.json +++ b/homeassistant/components/derivative/translations/bg.json @@ -16,5 +16,15 @@ } } } + }, + "selector": { + "time_unit": { + "options": { + "d": "\u0414\u043d\u0438", + "h": "\u0427\u0430\u0441\u0430", + "min": "\u041c\u0438\u043d\u0443\u0442\u0438", + "s": "\u0421\u0435\u043a\u0443\u043d\u0434\u0438" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/ca.json b/homeassistant/components/derivative/translations/ca.json index 9dff83bb746..0bed95cf694 100644 --- a/homeassistant/components/derivative/translations/ca.json +++ b/homeassistant/components/derivative/translations/ca.json @@ -39,5 +39,15 @@ } } }, + "selector": { + "time_unit": { + "options": { + "d": "Dies", + "h": "Hores", + "min": "Minuts", + "s": "Segons" + } + } + }, "title": "Sensor derivatiu" } \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/de.json b/homeassistant/components/derivative/translations/de.json index 1a23de37c25..daeb3ec25d6 100644 --- a/homeassistant/components/derivative/translations/de.json +++ b/homeassistant/components/derivative/translations/de.json @@ -39,5 +39,15 @@ } } }, + "selector": { + "time_unit": { + "options": { + "d": "Tage", + "h": "Stunden", + "min": "Minuten", + "s": "Sekunden" + } + } + }, "title": "Ableitungssensor" } \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/el.json b/homeassistant/components/derivative/translations/el.json index a0c55e93a5e..265680f72b6 100644 --- a/homeassistant/components/derivative/translations/el.json +++ b/homeassistant/components/derivative/translations/el.json @@ -13,10 +13,10 @@ "data_description": { "round": "\u0395\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf.", "time_window": "\u0395\u03ac\u03bd \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af, \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03ac \u03c3\u03c4\u03b1\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03b9\u03bd\u03b7\u03c4\u03cc\u03c2 \u03bc\u03ad\u03c3\u03bf\u03c2 \u03cc\u03c1\u03bf\u03c2 \u03c4\u03c9\u03bd \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03c9\u03bd \u03b5\u03bd\u03c4\u03cc\u03c2 \u03b1\u03c5\u03c4\u03bf\u03cd \u03c4\u03bf\u03c5 \u03c0\u03b1\u03c1\u03b1\u03b8\u03cd\u03c1\u03bf\u03c5.", - "unit_prefix": "\u0397 \u03c0\u03b1\u03c1\u03ac\u03b3\u03c9\u03b3\u03bf\u03c2 \u03b8\u03b1 \u03ba\u03bb\u03b9\u03bc\u03b1\u03ba\u03c9\u03b8\u03b5\u03af \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5 \u03c4\u03b7\u03c2 \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03bf\u03c5." + "unit_prefix": "\u0397 \u03ad\u03be\u03bf\u03b4\u03bf\u03c2 \u03b8\u03b1 \u03ba\u03bb\u03b9\u03bc\u03b1\u03ba\u03c9\u03b8\u03b5\u03af \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5 \u03c4\u03b7\u03c2 \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03bf\u03c5." }, - "description": "\u0397 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf.\n\u0395\u03ac\u03bd \u03c4\u03bf \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03c0\u03b1\u03c1\u03ac\u03b8\u03c5\u03c1\u03bf \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 0, \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03ac \u03c3\u03c4\u03b1\u03b8\u03bc\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03ba\u03b9\u03bd\u03b7\u03c4\u03cc\u03c2 \u03bc\u03ad\u03c3\u03bf\u03c2 \u03cc\u03c1\u03bf\u03c2 \u03c4\u03c9\u03bd \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03c9\u03bd \u03b5\u03bd\u03c4\u03cc\u03c2 \u03c4\u03bf\u03c5 \u03c0\u03b1\u03c1\u03b1\u03b8\u03cd\u03c1\u03bf\u03c5.\n\u0397 \u03c0\u03b1\u03c1\u03ac\u03b3\u03c9\u03b3\u03bf\u03c2 \u03b8\u03b1 \u03ba\u03bb\u03b9\u03bc\u03b1\u03ba\u03ce\u03bd\u03b5\u03c4\u03b1\u03b9 \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5 \u03c4\u03b7\u03c2 \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03bf\u03c5.", - "title": "\u039d\u03ad\u03bf\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 Derivative" + "description": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03af\u03b6\u03b5\u03b9 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03ac\u03b3\u03c9\u03b3\u03bf \u03b5\u03bd\u03cc\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1.", + "title": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03b1\u03c1\u03b1\u03b3\u03ce\u03b3\u03bf\u03c5" } } }, diff --git a/homeassistant/components/derivative/translations/en.json b/homeassistant/components/derivative/translations/en.json index b91318b5237..b42cc73391f 100644 --- a/homeassistant/components/derivative/translations/en.json +++ b/homeassistant/components/derivative/translations/en.json @@ -39,5 +39,15 @@ } } }, + "selector": { + "time_unit": { + "options": { + "d": "Days", + "h": "Hours", + "min": "Minutes", + "s": "Seconds" + } + } + }, "title": "Derivative sensor" } \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/et.json b/homeassistant/components/derivative/translations/et.json index 4c3af55a294..ff8a401de9b 100644 --- a/homeassistant/components/derivative/translations/et.json +++ b/homeassistant/components/derivative/translations/et.json @@ -39,5 +39,15 @@ } } }, + "selector": { + "time_unit": { + "options": { + "d": "p\u00e4eva", + "h": "tundi", + "min": "minutit", + "s": "sekundit" + } + } + }, "title": "Tuletisandur" } \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/pl.json b/homeassistant/components/derivative/translations/pl.json index 3f97ff29c2c..d808301f732 100644 --- a/homeassistant/components/derivative/translations/pl.json +++ b/homeassistant/components/derivative/translations/pl.json @@ -39,5 +39,15 @@ } } }, + "selector": { + "time_unit": { + "options": { + "d": "dni", + "h": "godziny", + "min": "minuty", + "s": "sekundy" + } + } + }, "title": "Sensor pochodnej" } \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/ru.json b/homeassistant/components/derivative/translations/ru.json index bef5b20efdd..77e786fd6ff 100644 --- a/homeassistant/components/derivative/translations/ru.json +++ b/homeassistant/components/derivative/translations/ru.json @@ -39,5 +39,15 @@ } } }, + "selector": { + "time_unit": { + "options": { + "d": "\u0414\u043d\u0438", + "h": "\u0427\u0430\u0441\u044b", + "min": "\u041c\u0438\u043d\u0443\u0442\u044b", + "s": "\u0421\u0435\u043a\u0443\u043d\u0434\u044b" + } + } + }, "title": "\u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u043d\u0430\u044f" } \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/uk.json b/homeassistant/components/derivative/translations/uk.json new file mode 100644 index 00000000000..fe3fc997183 --- /dev/null +++ b/homeassistant/components/derivative/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/derivative/translations/zh-Hant.json b/homeassistant/components/derivative/translations/zh-Hant.json index 11236b0ad63..f11b6133993 100644 --- a/homeassistant/components/derivative/translations/zh-Hant.json +++ b/homeassistant/components/derivative/translations/zh-Hant.json @@ -39,5 +39,15 @@ } } }, + "selector": { + "time_unit": { + "options": { + "d": "\u5929", + "h": "\u5c0f\u6642", + "min": "\u5206\u9418", + "s": "\u79d2\u9418" + } + } + }, "title": "\u5c0e\u6578\u611f\u6e2c\u5668" } \ No newline at end of file diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 3b75f4fff4c..75caf6f4f0a 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -7,7 +7,7 @@ from enum import Enum from functools import wraps import logging from types import ModuleType -from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Union, overload +from typing import TYPE_CHECKING, Any, Literal, NamedTuple, TypeAlias, overload import voluptuous as vol import voluptuous_serialize @@ -43,12 +43,13 @@ if TYPE_CHECKING: from .condition import DeviceAutomationConditionProtocol from .trigger import DeviceAutomationTriggerProtocol - DeviceAutomationPlatformType = Union[ - ModuleType, - DeviceAutomationTriggerProtocol, - DeviceAutomationConditionProtocol, - DeviceAutomationActionProtocol, - ] + DeviceAutomationPlatformType: TypeAlias = ( + ModuleType + | DeviceAutomationTriggerProtocol + | DeviceAutomationConditionProtocol + | DeviceAutomationActionProtocol + ) + DOMAIN = "device_automation" @@ -120,7 +121,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @overload -async def async_get_device_automation_platform( # noqa: D103 +async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: Literal[DeviceAutomationType.TRIGGER], @@ -129,7 +130,7 @@ async def async_get_device_automation_platform( # noqa: D103 @overload -async def async_get_device_automation_platform( # noqa: D103 +async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: Literal[DeviceAutomationType.CONDITION], @@ -138,7 +139,7 @@ async def async_get_device_automation_platform( # noqa: D103 @overload -async def async_get_device_automation_platform( # noqa: D103 +async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: Literal[DeviceAutomationType.ACTION], @@ -147,15 +148,15 @@ async def async_get_device_automation_platform( # noqa: D103 @overload -async def async_get_device_automation_platform( # noqa: D103 +async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType -) -> "DeviceAutomationPlatformType": +) -> DeviceAutomationPlatformType: ... async def async_get_device_automation_platform( hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType -) -> "DeviceAutomationPlatformType": +) -> 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 index 081b6bb283a..58c124377ff 100644 --- a/homeassistant/components/device_automation/action.py +++ b/homeassistant/components/device_automation/action.py @@ -1,16 +1,17 @@ """Device action validator.""" from __future__ import annotations -from typing import Any, Protocol, cast +from typing import Any, Protocol import voluptuous as vol from homeassistant.const import CONF_DOMAIN from homeassistant.core import Context, 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 +from .helpers import async_validate_device_automation_config class DeviceAutomationActionProtocol(Protocol): @@ -50,15 +51,9 @@ 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 + return await async_validate_device_automation_config( + hass, config, cv.DEVICE_ACTION_SCHEMA, DeviceAutomationType.ACTION + ) async def async_call_action_from_config( diff --git a/homeassistant/components/device_automation/condition.py b/homeassistant/components/device_automation/condition.py index d656908f4be..3856458c3dd 100644 --- a/homeassistant/components/device_automation/condition.py +++ b/homeassistant/components/device_automation/condition.py @@ -1,7 +1,7 @@ """Validate device conditions.""" from __future__ import annotations -from typing import TYPE_CHECKING, Any, Protocol, cast +from typing import TYPE_CHECKING, Any, Protocol import voluptuous as vol @@ -11,7 +11,7 @@ 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 +from .helpers import async_validate_device_automation_config if TYPE_CHECKING: from homeassistant.helpers import condition @@ -50,16 +50,9 @@ 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 + return await async_validate_device_automation_config( + hass, config, cv.DEVICE_CONDITION_SCHEMA, DeviceAutomationType.CONDITION + ) async def async_condition_from_config( diff --git a/homeassistant/components/device_automation/helpers.py b/homeassistant/components/device_automation/helpers.py new file mode 100644 index 00000000000..5f844c36aa5 --- /dev/null +++ b/homeassistant/components/device_automation/helpers.py @@ -0,0 +1,80 @@ +"""Helpers for device oriented automations.""" +from __future__ import annotations + +from typing import cast + +import voluptuous as vol + +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.typing import ConfigType + +from . import DeviceAutomationType, async_get_device_automation_platform +from .exceptions import InvalidDeviceAutomationConfig + +DYNAMIC_VALIDATOR = { + DeviceAutomationType.ACTION: "async_validate_action_config", + DeviceAutomationType.CONDITION: "async_validate_condition_config", + DeviceAutomationType.TRIGGER: "async_validate_trigger_config", +} + +STATIC_VALIDATOR = { + DeviceAutomationType.ACTION: "ACTION_SCHEMA", + DeviceAutomationType.CONDITION: "CONDITION_SCHEMA", + DeviceAutomationType.TRIGGER: "TRIGGER_SCHEMA", +} + + +async def async_validate_device_automation_config( + hass: HomeAssistant, + config: ConfigType, + automation_schema: vol.Schema, + automation_type: DeviceAutomationType, +) -> ConfigType: + """Validate config.""" + validated_config: ConfigType = automation_schema(config) + platform = await async_get_device_automation_platform( + hass, validated_config[CONF_DOMAIN], automation_type + ) + if not hasattr(platform, DYNAMIC_VALIDATOR[automation_type]): + # Pass the unvalidated config to avoid mutating the raw config twice + return cast( + ConfigType, getattr(platform, STATIC_VALIDATOR[automation_type])(config) + ) + + # Only call the dynamic validator if the referenced device exists and the relevant + # config entry is loaded + registry = dr.async_get(hass) + if not (device := registry.async_get(validated_config[CONF_DEVICE_ID])): + # The device referenced by the device trigger does not exist + raise InvalidDeviceAutomationConfig( + f"Unknown device '{validated_config[CONF_DEVICE_ID]}'" + ) + + device_config_entry = None + for entry_id in device.config_entries: + if ( + not (entry := hass.config_entries.async_get_entry(entry_id)) + or entry.domain != validated_config[CONF_DOMAIN] + ): + continue + device_config_entry = entry + break + + if not device_config_entry: + # The config entry referenced by the device trigger does not exist + raise InvalidDeviceAutomationConfig( + f"Device '{validated_config[CONF_DEVICE_ID]}' has no config entry from " + f"domain '{validated_config[CONF_DOMAIN]}'" + ) + + if not await hass.config_entries.async_wait_component(device_config_entry): + # The component could not be loaded, skip the dynamic validation + return validated_config + + # Pass the unvalidated config to avoid mutating the raw config twice + return cast( + ConfigType, + await getattr(platform, DYNAMIC_VALIDATOR[automation_type])(hass, config), + ) diff --git a/homeassistant/components/device_automation/trigger.py b/homeassistant/components/device_automation/trigger.py index cd5b3a84c82..80e96ddaba9 100644 --- a/homeassistant/components/device_automation/trigger.py +++ b/homeassistant/components/device_automation/trigger.py @@ -1,13 +1,12 @@ """Offer device oriented automation.""" from __future__ import annotations -from typing import Any, Protocol, cast +from typing import Any, Protocol import voluptuous as vol -from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN +from homeassistant.const import CONF_DOMAIN from homeassistant.core import CALLBACK_TYPE, HomeAssistant -from homeassistant.helpers import device_registry as dr from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType @@ -16,7 +15,7 @@ from . import ( DeviceAutomationType, async_get_device_automation_platform, ) -from .exceptions import InvalidDeviceAutomationConfig +from .helpers import async_validate_device_automation_config TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) @@ -58,36 +57,9 @@ async def async_validate_trigger_config( hass: HomeAssistant, config: ConfigType ) -> ConfigType: """Validate config.""" - try: - 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)) - - # Only call the dynamic validator if the relevant config entry is loaded - registry = dr.async_get(hass) - if not (device := registry.async_get(config[CONF_DEVICE_ID])): - return config - - device_config_entry = None - for entry_id in device.config_entries: - if not (entry := hass.config_entries.async_get_entry(entry_id)): - continue - if entry.domain != config[CONF_DOMAIN]: - continue - device_config_entry = entry - break - - if not device_config_entry: - return config - - if not await hass.config_entries.async_wait_component(device_config_entry): - return config - - return await platform.async_validate_trigger_config(hass, config) - except InvalidDeviceAutomationConfig as err: - raise vol.Invalid(str(err) or "Invalid trigger configuration") from err + return await async_validate_device_automation_config( + hass, config, TRIGGER_SCHEMA, DeviceAutomationType.TRIGGER + ) async def async_attach_trigger( diff --git a/homeassistant/components/devolo_home_control/translations/lt.json b/homeassistant/components/devolo_home_control/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/uk.json b/homeassistant/components/devolo_home_control/translations/uk.json index d9546a36eb1..8f5f3f3effb 100644 --- a/homeassistant/components/devolo_home_control/translations/uk.json +++ b/homeassistant/components/devolo_home_control/translations/uk.json @@ -13,6 +13,11 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438 / devolo ID" } + }, + "zeroconf_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } } } } diff --git a/homeassistant/components/devolo_home_network/__init__.py b/homeassistant/components/devolo_home_network/__init__.py index 4c54dc721e1..5fdb75bb5f9 100644 --- a/homeassistant/components/devolo_home_network/__init__.py +++ b/homeassistant/components/devolo_home_network/__init__.py @@ -5,14 +5,24 @@ import logging from typing import Any import async_timeout -from devolo_plc_api.device import Device -from devolo_plc_api.exceptions.device import DeviceNotFound, DeviceUnavailable +from devolo_plc_api import Device +from devolo_plc_api.device_api import ( + ConnectedStationInfo, + NeighborAPInfo, + WifiGuestAccessGet, +) +from devolo_plc_api.exceptions.device import ( + DeviceNotFound, + DevicePasswordProtected, + DeviceUnavailable, +) +from devolo_plc_api.plcnet_api import LogicalNetwork from homeassistant.components import zeroconf from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -24,6 +34,8 @@ from .const import ( NEIGHBORING_WIFI_NETWORKS, PLATFORMS, SHORT_UPDATE_INTERVAL, + SWITCH_GUEST_WIFI, + SWITCH_LEDS, ) _LOGGER = logging.getLogger(__name__) @@ -48,27 +60,50 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"Unable to connect to {entry.data[CONF_IP_ADDRESS]}" ) from err - async def async_update_connected_plc_devices() -> dict[str, Any]: + async def async_update_connected_plc_devices() -> LogicalNetwork: """Fetch data from API endpoint.""" + assert device.plcnet try: async with async_timeout.timeout(10): - return await device.plcnet.async_get_network_overview() # type: ignore[no-any-return, union-attr] + return await device.plcnet.async_get_network_overview() except DeviceUnavailable as err: raise UpdateFailed(err) from err - async def async_update_wifi_connected_station() -> dict[str, Any]: + async def async_update_guest_wifi_status() -> WifiGuestAccessGet: """Fetch data from API endpoint.""" + assert device.device try: async with async_timeout.timeout(10): - return await device.device.async_get_wifi_connected_station() # type: ignore[no-any-return, union-attr] + return await device.device.async_get_wifi_guest_access() + except DeviceUnavailable as err: + raise UpdateFailed(err) from err + except DevicePasswordProtected as err: + raise ConfigEntryAuthFailed(err) from err + + async def async_update_led_status() -> bool: + """Fetch data from API endpoint.""" + assert device.device + try: + async with async_timeout.timeout(10): + return await device.device.async_get_led_setting() except DeviceUnavailable as err: raise UpdateFailed(err) from err - async def async_update_wifi_neighbor_access_points() -> dict[str, Any]: + async def async_update_wifi_connected_station() -> list[ConnectedStationInfo]: """Fetch data from API endpoint.""" + assert device.device + try: + async with async_timeout.timeout(10): + return await device.device.async_get_wifi_connected_station() + except DeviceUnavailable as err: + raise UpdateFailed(err) from err + + async def async_update_wifi_neighbor_access_points() -> list[NeighborAPInfo]: + """Fetch data from API endpoint.""" + assert device.device try: async with async_timeout.timeout(30): - return await device.device.async_get_wifi_neighbor_access_points() # type: ignore[no-any-return, union-attr] + return await device.device.async_get_wifi_neighbor_access_points() except DeviceUnavailable as err: raise UpdateFailed(err) from err @@ -76,7 +111,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Disconnect from device.""" await device.async_disconnect() - coordinators: dict[str, DataUpdateCoordinator] = {} + coordinators: dict[str, DataUpdateCoordinator[Any]] = {} if device.plcnet: coordinators[CONNECTED_PLC_DEVICES] = DataUpdateCoordinator( hass, @@ -85,6 +120,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=async_update_connected_plc_devices, update_interval=LONG_UPDATE_INTERVAL, ) + if device.device and "led" in device.device.features: + coordinators[SWITCH_LEDS] = DataUpdateCoordinator( + hass, + _LOGGER, + name=SWITCH_LEDS, + update_method=async_update_led_status, + update_interval=SHORT_UPDATE_INTERVAL, + ) if device.device and "wifi1" in device.device.features: coordinators[CONNECTED_WIFI_CLIENTS] = DataUpdateCoordinator( hass, @@ -100,6 +143,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=async_update_wifi_neighbor_access_points, update_interval=LONG_UPDATE_INTERVAL, ) + coordinators[SWITCH_GUEST_WIFI] = DataUpdateCoordinator( + hass, + _LOGGER, + name=SWITCH_GUEST_WIFI, + update_method=async_update_guest_wifi_status, + update_interval=SHORT_UPDATE_INTERVAL, + ) hass.data[DOMAIN][entry.entry_id] = {"device": device, "coordinators": coordinators} diff --git a/homeassistant/components/devolo_home_network/binary_sensor.py b/homeassistant/components/devolo_home_network/binary_sensor.py index 2e87bd180b1..ba174d30abd 100644 --- a/homeassistant/components/devolo_home_network/binary_sensor.py +++ b/homeassistant/components/devolo_home_network/binary_sensor.py @@ -3,8 +3,10 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass +from typing import Any -from devolo_plc_api.device import Device +from devolo_plc_api import Device +from devolo_plc_api.plcnet_api import LogicalNetwork from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -24,9 +26,9 @@ from .entity import DevoloEntity def _is_connected_to_router(entity: DevoloBinarySensorEntity) -> bool: """Check, if device is attached to the router.""" return all( - device["attached_to_router"] - for device in entity.coordinator.data["network"]["devices"] - if device["mac_address"] == entity.device.mac + device.attached_to_router + for device in entity.coordinator.data.devices + if device.mac_address == entity.device.mac ) @@ -62,36 +64,36 @@ async def async_setup_entry( ) -> None: """Get all devices and sensors and setup them via config entry.""" device: Device = hass.data[DOMAIN][entry.entry_id]["device"] - coordinators: dict[str, DataUpdateCoordinator] = hass.data[DOMAIN][entry.entry_id][ - "coordinators" - ] + coordinators: dict[str, DataUpdateCoordinator[Any]] = hass.data[DOMAIN][ + entry.entry_id + ]["coordinators"] entities: list[BinarySensorEntity] = [] if device.plcnet: entities.append( DevoloBinarySensorEntity( + entry, coordinators[CONNECTED_PLC_DEVICES], SENSOR_TYPES[CONNECTED_TO_ROUTER], device, - entry.title, ) ) async_add_entities(entities) -class DevoloBinarySensorEntity(DevoloEntity, BinarySensorEntity): +class DevoloBinarySensorEntity(DevoloEntity[LogicalNetwork], BinarySensorEntity): """Representation of a devolo binary sensor.""" def __init__( self, - coordinator: DataUpdateCoordinator, + entry: ConfigEntry, + coordinator: DataUpdateCoordinator[LogicalNetwork], description: DevoloBinarySensorEntityDescription, device: Device, - device_name: str, ) -> None: """Initialize entity.""" self.entity_description: DevoloBinarySensorEntityDescription = description - super().__init__(coordinator, device, device_name) + super().__init__(entry, coordinator, device) @property def is_on(self) -> bool: diff --git a/homeassistant/components/devolo_home_network/config_flow.py b/homeassistant/components/devolo_home_network/config_flow.py index 0acdc9cfa64..08892e19e4e 100644 --- a/homeassistant/components/devolo_home_network/config_flow.py +++ b/homeassistant/components/devolo_home_network/config_flow.py @@ -40,7 +40,7 @@ async def validate_input( return { SERIAL_NUMBER: str(device.serial_number), - TITLE: device.hostname.split(".")[0], + TITLE: device.hostname.split(".", maxsplit=1)[0], } @@ -85,7 +85,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="home_control") await self.async_set_unique_id(discovery_info.properties["SN"]) - self._abort_if_unique_id_configured() + self._abort_if_unique_id_configured( + updates={CONF_IP_ADDRESS: discovery_info.host} + ) self.context[CONF_HOST] = discovery_info.host self.context["title_placeholders"] = { diff --git a/homeassistant/components/devolo_home_network/const.py b/homeassistant/components/devolo_home_network/const.py index 1a49beb5d02..fffe9b5d482 100644 --- a/homeassistant/components/devolo_home_network/const.py +++ b/homeassistant/components/devolo_home_network/const.py @@ -2,12 +2,23 @@ from datetime import timedelta +from devolo_plc_api.device_api import ( + WIFI_BAND_2G, + WIFI_BAND_5G, + WIFI_VAP_GUEST_AP, + WIFI_VAP_MAIN_AP, +) + from homeassistant.const import Platform DOMAIN = "devolo_home_network" -PLATFORMS = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, Platform.SENSOR] +PLATFORMS = [ + Platform.BINARY_SENSOR, + Platform.DEVICE_TRACKER, + Platform.SENSOR, + Platform.SWITCH, +] -MAC_ADDRESS = "mac_address" PRODUCT = "product" SERIAL_NUMBER = "serial_number" TITLE = "title" @@ -16,16 +27,17 @@ LONG_UPDATE_INTERVAL = timedelta(minutes=5) SHORT_UPDATE_INTERVAL = timedelta(seconds=15) CONNECTED_PLC_DEVICES = "connected_plc_devices" -CONNECTED_STATIONS = "connected_stations" CONNECTED_TO_ROUTER = "connected_to_router" CONNECTED_WIFI_CLIENTS = "connected_wifi_clients" NEIGHBORING_WIFI_NETWORKS = "neighboring_wifi_networks" +SWITCH_GUEST_WIFI = "switch_guest_wifi" +SWITCH_LEDS = "switch_leds" WIFI_APTYPE = { - "WIFI_VAP_MAIN_AP": "Main", - "WIFI_VAP_GUEST_AP": "Guest", + WIFI_VAP_MAIN_AP: "Main", + WIFI_VAP_GUEST_AP: "Guest", } WIFI_BANDS = { - "WIFI_BAND_2G": 2.4, - "WIFI_BAND_5G": 5, + WIFI_BAND_2G: 2.4, + WIFI_BAND_5G: 5, } diff --git a/homeassistant/components/devolo_home_network/device_tracker.py b/homeassistant/components/devolo_home_network/device_tracker.py index e465266a0e7..79f2eb1f495 100644 --- a/homeassistant/components/devolo_home_network/device_tracker.py +++ b/homeassistant/components/devolo_home_network/device_tracker.py @@ -1,9 +1,8 @@ """Platform for device tracker integration.""" from __future__ import annotations -from typing import Any - from devolo_plc_api.device import Device +from devolo_plc_api.device_api import ConnectedStationInfo from homeassistant.components.device_tracker import ( DOMAIN as DEVICE_TRACKER_DOMAIN, @@ -20,14 +19,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import ( - CONNECTED_STATIONS, - CONNECTED_WIFI_CLIENTS, - DOMAIN, - MAC_ADDRESS, - WIFI_APTYPE, - WIFI_BANDS, -) +from .const import CONNECTED_WIFI_CLIENTS, DOMAIN, WIFI_APTYPE, WIFI_BANDS async def async_setup_entry( @@ -35,9 +27,9 @@ async def async_setup_entry( ) -> None: """Get all devices and sensors and setup them via config entry.""" device: Device = hass.data[DOMAIN][entry.entry_id]["device"] - coordinators: dict[str, DataUpdateCoordinator] = hass.data[DOMAIN][entry.entry_id][ - "coordinators" - ] + coordinators: dict[ + str, DataUpdateCoordinator[list[ConnectedStationInfo]] + ] = hass.data[DOMAIN][entry.entry_id]["coordinators"] registry = entity_registry.async_get(hass) tracked = set() @@ -45,16 +37,16 @@ async def async_setup_entry( def new_device_callback() -> None: """Add new devices if needed.""" new_entities = [] - for station in coordinators[CONNECTED_WIFI_CLIENTS].data[CONNECTED_STATIONS]: - if station[MAC_ADDRESS] in tracked: + for station in coordinators[CONNECTED_WIFI_CLIENTS].data: + if station.mac_address in tracked: continue new_entities.append( DevoloScannerEntity( - coordinators[CONNECTED_WIFI_CLIENTS], device, station[MAC_ADDRESS] + coordinators[CONNECTED_WIFI_CLIENTS], device, station.mac_address ) ) - tracked.add(station[MAC_ADDRESS]) + tracked.add(station.mac_address) async_add_entities(new_entities) @callback @@ -90,11 +82,16 @@ async def async_setup_entry( ) -class DevoloScannerEntity(CoordinatorEntity, ScannerEntity): +class DevoloScannerEntity( + CoordinatorEntity[DataUpdateCoordinator[list[ConnectedStationInfo]]], ScannerEntity +): """Representation of a devolo device tracker.""" def __init__( - self, coordinator: DataUpdateCoordinator, device: Device, mac: str + self, + coordinator: DataUpdateCoordinator[list[ConnectedStationInfo]], + device: Device, + mac: str, ) -> None: """Initialize entity.""" super().__init__(coordinator) @@ -105,22 +102,22 @@ class DevoloScannerEntity(CoordinatorEntity, ScannerEntity): def extra_state_attributes(self) -> dict[str, str]: """Return the attributes.""" attrs: dict[str, str] = {} - if not self.coordinator.data[CONNECTED_STATIONS]: + if not self.coordinator.data: return {} - station: dict[str, Any] = next( + station = next( ( station - for station in self.coordinator.data[CONNECTED_STATIONS] - if station[MAC_ADDRESS] == self.mac_address + for station in self.coordinator.data + if station.mac_address == self.mac_address ), - {}, + None, ) if station: - attrs["wifi"] = WIFI_APTYPE.get(station["vap_type"], STATE_UNKNOWN) + attrs["wifi"] = WIFI_APTYPE.get(station.vap_type, STATE_UNKNOWN) attrs["band"] = ( - f"{WIFI_BANDS.get(station['band'])} {UnitOfFrequency.GIGAHERTZ}" - if WIFI_BANDS.get(station["band"]) + f"{WIFI_BANDS.get(station.band)} {UnitOfFrequency.GIGAHERTZ}" + if WIFI_BANDS.get(station.band) else STATE_UNKNOWN ) return attrs @@ -137,8 +134,8 @@ class DevoloScannerEntity(CoordinatorEntity, ScannerEntity): """Return true if the device is connected to the network.""" return any( station - for station in self.coordinator.data[CONNECTED_STATIONS] - if station[MAC_ADDRESS] == self.mac_address + for station in self.coordinator.data + if station.mac_address == self.mac_address ) @property diff --git a/homeassistant/components/devolo_home_network/diagnostics.py b/homeassistant/components/devolo_home_network/diagnostics.py new file mode 100644 index 00000000000..bd4393d73dd --- /dev/null +++ b/homeassistant/components/devolo_home_network/diagnostics.py @@ -0,0 +1,38 @@ +"""Diagnostics support for devolo Home Network.""" +from __future__ import annotations + +from typing import Any + +from devolo_plc_api import Device + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +TO_REDACT = {CONF_PASSWORD} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + device: Device = hass.data[DOMAIN][entry.entry_id]["device"] + + diag_data = { + "entry": async_redact_data(entry.as_dict(), TO_REDACT), + "device_info": { + "mt_number": device.mt_number, + "product": device.product, + "firmware": device.firmware_version, + "device_api": device.device is not None, + "plcnet_api": device.plcnet is not None, + }, + } + + if device.device: + diag_data["device_info"]["features"] = device.device.features + + return diag_data diff --git a/homeassistant/components/devolo_home_network/entity.py b/homeassistant/components/devolo_home_network/entity.py index b7b2275109f..a26d8dce8f6 100644 --- a/homeassistant/components/devolo_home_network/entity.py +++ b/homeassistant/components/devolo_home_network/entity.py @@ -1,8 +1,17 @@ """Generic platform.""" from __future__ import annotations -from devolo_plc_api.device import Device +from typing import TypeVar +from devolo_plc_api.device import Device +from devolo_plc_api.device_api import ( + ConnectedStationInfo, + NeighborAPInfo, + WifiGuestAccessGet, +) +from devolo_plc_api.plcnet_api import LogicalNetwork + +from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -11,26 +20,41 @@ from homeassistant.helpers.update_coordinator import ( from .const import DOMAIN +_DataT = TypeVar( + "_DataT", + bound=( + LogicalNetwork + | list[ConnectedStationInfo] + | list[NeighborAPInfo] + | WifiGuestAccessGet + | bool + ), +) -class DevoloEntity(CoordinatorEntity): + +class DevoloEntity(CoordinatorEntity[DataUpdateCoordinator[_DataT]]): """Representation of a devolo home network device.""" _attr_has_entity_name = True def __init__( - self, coordinator: DataUpdateCoordinator, device: Device, device_name: str + self, + entry: ConfigEntry, + coordinator: DataUpdateCoordinator[_DataT], + device: Device, ) -> None: """Initialize a devolo home network device.""" super().__init__(coordinator) self.device = device + self.entry = entry self._attr_device_info = DeviceInfo( configuration_url=f"http://{device.ip}", identifiers={(DOMAIN, str(device.serial_number))}, manufacturer="devolo", model=device.product, - name=device_name, + name=entry.title, sw_version=device.firmware_version, ) self._attr_unique_id = f"{device.serial_number}_{self.entity_description.key}" diff --git a/homeassistant/components/devolo_home_network/manifest.json b/homeassistant/components/devolo_home_network/manifest.json index 858afc6e5e8..bf3ff5c5481 100644 --- a/homeassistant/components/devolo_home_network/manifest.json +++ b/homeassistant/components/devolo_home_network/manifest.json @@ -4,7 +4,7 @@ "integration_type": "device", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/devolo_home_network", - "requirements": ["devolo-plc-api==0.9.0"], + "requirements": ["devolo-plc-api==1.1.0"], "zeroconf": [ { "type": "_dvl-deviceapi._tcp.local.", "properties": { "MT": "*" } } ], diff --git a/homeassistant/components/devolo_home_network/sensor.py b/homeassistant/components/devolo_home_network/sensor.py index 08d61cd6eff..2cca7c3b44b 100644 --- a/homeassistant/components/devolo_home_network/sensor.py +++ b/homeassistant/components/devolo_home_network/sensor.py @@ -3,9 +3,11 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass -from typing import Any +from typing import Any, Generic, TypeVar from devolo_plc_api.device import Device +from devolo_plc_api.device_api import ConnectedStationInfo, NeighborAPInfo +from devolo_plc_api.plcnet_api import LogicalNetwork from homeassistant.components.sensor import ( SensorEntity, @@ -26,47 +28,51 @@ from .const import ( ) from .entity import DevoloEntity +_DataT = TypeVar( + "_DataT", + bound=LogicalNetwork | list[ConnectedStationInfo] | list[NeighborAPInfo], +) + @dataclass -class DevoloSensorRequiredKeysMixin: +class DevoloSensorRequiredKeysMixin(Generic[_DataT]): """Mixin for required keys.""" - value_func: Callable[[dict[str, Any]], int] + value_func: Callable[[_DataT], int] @dataclass class DevoloSensorEntityDescription( - SensorEntityDescription, DevoloSensorRequiredKeysMixin + SensorEntityDescription, DevoloSensorRequiredKeysMixin[_DataT] ): """Describes devolo sensor entity.""" -SENSOR_TYPES: dict[str, DevoloSensorEntityDescription] = { - CONNECTED_PLC_DEVICES: DevoloSensorEntityDescription( +SENSOR_TYPES: dict[str, DevoloSensorEntityDescription[Any]] = { + CONNECTED_PLC_DEVICES: DevoloSensorEntityDescription[LogicalNetwork]( key=CONNECTED_PLC_DEVICES, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:lan", name="Connected PLC devices", value_func=lambda data: len( - {device["mac_address_from"] for device in data["network"]["data_rates"]} + {device.mac_address_from for device in data.data_rates} ), ), - CONNECTED_WIFI_CLIENTS: DevoloSensorEntityDescription( + CONNECTED_WIFI_CLIENTS: DevoloSensorEntityDescription[list[ConnectedStationInfo]]( key=CONNECTED_WIFI_CLIENTS, - entity_registry_enabled_default=True, icon="mdi:wifi", name="Connected Wifi clients", state_class=SensorStateClass.MEASUREMENT, - value_func=lambda data: len(data["connected_stations"]), + value_func=len, ), - NEIGHBORING_WIFI_NETWORKS: DevoloSensorEntityDescription( + NEIGHBORING_WIFI_NETWORKS: DevoloSensorEntityDescription[list[NeighborAPInfo]]( key=NEIGHBORING_WIFI_NETWORKS, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, icon="mdi:wifi-marker", name="Neighboring Wifi networks", - value_func=lambda data: len(data["neighbor_aps"]), + value_func=len, ), } @@ -76,53 +82,55 @@ async def async_setup_entry( ) -> None: """Get all devices and sensors and setup them via config entry.""" device: Device = hass.data[DOMAIN][entry.entry_id]["device"] - coordinators: dict[str, DataUpdateCoordinator] = hass.data[DOMAIN][entry.entry_id][ - "coordinators" - ] + coordinators: dict[str, DataUpdateCoordinator[Any]] = hass.data[DOMAIN][ + entry.entry_id + ]["coordinators"] - entities: list[DevoloSensorEntity] = [] + entities: list[DevoloSensorEntity[Any]] = [] if device.plcnet: entities.append( DevoloSensorEntity( + entry, coordinators[CONNECTED_PLC_DEVICES], SENSOR_TYPES[CONNECTED_PLC_DEVICES], device, - entry.title, ) ) if device.device and "wifi1" in device.device.features: entities.append( DevoloSensorEntity( + entry, coordinators[CONNECTED_WIFI_CLIENTS], SENSOR_TYPES[CONNECTED_WIFI_CLIENTS], device, - entry.title, ) ) entities.append( DevoloSensorEntity( + entry, coordinators[NEIGHBORING_WIFI_NETWORKS], SENSOR_TYPES[NEIGHBORING_WIFI_NETWORKS], device, - entry.title, ) ) async_add_entities(entities) -class DevoloSensorEntity(DevoloEntity, SensorEntity): +class DevoloSensorEntity(DevoloEntity[_DataT], SensorEntity): """Representation of a devolo sensor.""" + entity_description: DevoloSensorEntityDescription[_DataT] + def __init__( self, - coordinator: DataUpdateCoordinator, - description: DevoloSensorEntityDescription, + entry: ConfigEntry, + coordinator: DataUpdateCoordinator[_DataT], + description: DevoloSensorEntityDescription[_DataT], device: Device, - device_name: str, ) -> None: """Initialize entity.""" - self.entity_description: DevoloSensorEntityDescription = description - super().__init__(coordinator, device, device_name) + self.entity_description = description + super().__init__(entry, coordinator, device) @property def native_value(self) -> int: diff --git a/homeassistant/components/devolo_home_network/switch.py b/homeassistant/components/devolo_home_network/switch.py new file mode 100644 index 00000000000..35ce84c0969 --- /dev/null +++ b/homeassistant/components/devolo_home_network/switch.py @@ -0,0 +1,132 @@ +"""Platform for switch integration.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any, Generic, TypeVar + +from devolo_plc_api.device import Device +from devolo_plc_api.device_api import WifiGuestAccessGet +from devolo_plc_api.exceptions.device import DevicePasswordProtected, DeviceUnavailable + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, SWITCH_GUEST_WIFI, SWITCH_LEDS +from .entity import DevoloEntity + +_DataT = TypeVar("_DataT", bound=WifiGuestAccessGet | bool) + + +@dataclass +class DevoloSwitchRequiredKeysMixin(Generic[_DataT]): + """Mixin for required keys.""" + + is_on_func: Callable[[_DataT], bool] + turn_on_func: Callable[[Device], Awaitable[bool]] + turn_off_func: Callable[[Device], Awaitable[bool]] + + +@dataclass +class DevoloSwitchEntityDescription( + SwitchEntityDescription, DevoloSwitchRequiredKeysMixin[_DataT] +): + """Describes devolo switch entity.""" + + +SWITCH_TYPES: dict[str, DevoloSwitchEntityDescription[Any]] = { + SWITCH_GUEST_WIFI: DevoloSwitchEntityDescription[WifiGuestAccessGet]( + key=SWITCH_GUEST_WIFI, + icon="mdi:wifi", + name="Enable guest Wifi", + is_on_func=lambda data: data.enabled is True, + turn_on_func=lambda device: device.device.async_set_wifi_guest_access(True), # type: ignore[union-attr] + turn_off_func=lambda device: device.device.async_set_wifi_guest_access(False), # type: ignore[union-attr] + ), + SWITCH_LEDS: DevoloSwitchEntityDescription[bool]( + key=SWITCH_LEDS, + entity_category=EntityCategory.CONFIG, + icon="mdi:led-off", + name="Enable LEDs", + is_on_func=bool, + turn_on_func=lambda device: device.device.async_set_led_setting(True), # type: ignore[union-attr] + turn_off_func=lambda device: device.device.async_set_led_setting(False), # type: ignore[union-attr] + ), +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Get all devices and sensors and setup them via config entry.""" + device: Device = hass.data[DOMAIN][entry.entry_id]["device"] + coordinators: dict[str, DataUpdateCoordinator[Any]] = hass.data[DOMAIN][ + entry.entry_id + ]["coordinators"] + + entities: list[DevoloSwitchEntity[Any]] = [] + if device.device and "led" in device.device.features: + entities.append( + DevoloSwitchEntity( + entry, + coordinators[SWITCH_LEDS], + SWITCH_TYPES[SWITCH_LEDS], + device, + ) + ) + if device.device and "wifi1" in device.device.features: + entities.append( + DevoloSwitchEntity( + entry, + coordinators[SWITCH_GUEST_WIFI], + SWITCH_TYPES[SWITCH_GUEST_WIFI], + device, + ) + ) + async_add_entities(entities) + + +class DevoloSwitchEntity(DevoloEntity[_DataT], SwitchEntity): + """Representation of a devolo switch.""" + + entity_description: DevoloSwitchEntityDescription[_DataT] + + def __init__( + self, + entry: ConfigEntry, + coordinator: DataUpdateCoordinator[_DataT], + description: DevoloSwitchEntityDescription[_DataT], + device: Device, + ) -> None: + """Initialize entity.""" + self.entity_description = description + super().__init__(entry, coordinator, device) + + @property + def is_on(self) -> bool: + """State of the switch.""" + return self.entity_description.is_on_func(self.coordinator.data) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + try: + await self.entity_description.turn_on_func(self.device) + except DevicePasswordProtected: + self.entry.async_start_reauth(self.hass) + except DeviceUnavailable: + pass # The coordinator will handle this + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + try: + await self.entity_description.turn_off_func(self.device) + except DevicePasswordProtected: + self.entry.async_start_reauth(self.hass) + except DeviceUnavailable: + pass # The coordinator will handle this + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/devolo_home_network/translations/lv.json b/homeassistant/components/devolo_home_network/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_network/translations/tr.json b/homeassistant/components/devolo_home_network/translations/tr.json index def4954dc42..edcc78a13e1 100644 --- a/homeassistant/components/devolo_home_network/translations/tr.json +++ b/homeassistant/components/devolo_home_network/translations/tr.json @@ -20,7 +20,7 @@ "data": { "ip_address": "IP Adresi" }, - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" }, "zeroconf_confirm": { "description": "{host_name} ` ana bilgisayar ad\u0131na sahip devolo ev a\u011f\u0131 cihaz\u0131n\u0131 Home Assistant'a eklemek ister misiniz?", diff --git a/homeassistant/components/devolo_home_network/translations/uk.json b/homeassistant/components/devolo_home_network/translations/uk.json new file mode 100644 index 00000000000..d9efd22e750 --- /dev/null +++ b/homeassistant/components/devolo_home_network/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/el.json b/homeassistant/components/dexcom/translations/el.json index 29ca3b114ca..7d154f08eca 100644 --- a/homeassistant/components/dexcom/translations/el.json +++ b/homeassistant/components/dexcom/translations/el.json @@ -16,7 +16,7 @@ "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" + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 Dexcom" } } }, diff --git a/homeassistant/components/dexcom/translations/lt.json b/homeassistant/components/dexcom/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/dexcom/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 2ebb0fd63e0..db74522cc30 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -2,7 +2,7 @@ "domain": "dhcp", "name": "DHCP Discovery", "documentation": "https://www.home-assistant.io/integrations/dhcp", - "requirements": ["scapy==2.4.5", "aiodiscover==1.4.13"], + "requirements": ["scapy==2.5.0", "aiodiscover==1.4.13"], "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/homeassistant/components/diagnostics/__init__.py b/homeassistant/components/diagnostics/__init__.py index b54a710e807..1a3732ca1e2 100644 --- a/homeassistant/components/diagnostics/__init__.py +++ b/homeassistant/components/diagnostics/__init__.py @@ -1,7 +1,7 @@ """The Diagnostics integration.""" from __future__ import annotations -from collections.abc import Callable, Coroutine +from collections.abc import Callable, Coroutine, Mapping from dataclasses import dataclass, field from http import HTTPStatus import json @@ -38,10 +38,11 @@ class DiagnosticsPlatformData: """Diagnostic platform data.""" config_entry_diagnostics: Callable[ - [HomeAssistant, ConfigEntry], Coroutine[Any, Any, Any] + [HomeAssistant, ConfigEntry], Coroutine[Any, Any, Mapping[str, Any]] ] | None device_diagnostics: Callable[ - [HomeAssistant, ConfigEntry, DeviceEntry], Coroutine[Any, Any, Any] + [HomeAssistant, ConfigEntry, DeviceEntry], + Coroutine[Any, Any, Mapping[str, Any]], ] | None @@ -72,12 +73,12 @@ class DiagnosticsProtocol(Protocol): async def async_get_config_entry_diagnostics( self, hass: HomeAssistant, config_entry: ConfigEntry - ) -> Any: + ) -> Mapping[str, Any]: """Return diagnostics for a config entry.""" async def async_get_device_diagnostics( self, hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry - ) -> Any: + ) -> Mapping[str, Any]: """Return diagnostics for a device.""" @@ -148,7 +149,7 @@ def handle_get( async def _async_get_json_file_response( hass: HomeAssistant, - data: Any, + data: Mapping[str, Any], filename: str, domain: str, d_id: str, diff --git a/homeassistant/components/dialogflow/translations/el.json b/homeassistant/components/dialogflow/translations/el.json index b2d36ed6235..ed230dd0ef1 100644 --- a/homeassistant/components/dialogflow/translations/el.json +++ b/homeassistant/components/dialogflow/translations/el.json @@ -6,7 +6,7 @@ "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." + "default": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c4\u03ad\u03bb\u03bd\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\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 \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- \u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5: application/json\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": { diff --git a/homeassistant/components/dialogflow/translations/hu.json b/homeassistant/components/dialogflow/translations/hu.json index 69fdaea4d00..d9bae308439 100644 --- a/homeassistant/components/dialogflow/translations/hu.json +++ b/homeassistant/components/dialogflow/translations/hu.json @@ -6,7 +6,7 @@ "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." }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a [Dialogflow webhook integr\u00e1ci\u00f3j\u00e1t]({dialogflow_url}). \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t]({docs_url})." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni Home Assistantba, be kell \u00e1ll\u00edtania a [Dialogflow webhook integr\u00e1ci\u00f3j\u00e1t]({dialogflow_url}). \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 adatokat: \n\n - URL: `{webhook_url}`\n - Met\u00f3dus: POST\n - Tartalomt\u00edpus: alkalmaz\u00e1s/json \n\nB\u0151vebb inform\u00e1ci\u00f3 [a dokument\u00e1ci\u00f3ban]({docs_url}) olvashat\u00f3." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/translations/tr.json b/homeassistant/components/dialogflow/translations/tr.json index 27378ab3284..c667289ddcf 100644 --- a/homeassistant/components/dialogflow/translations/tr.json +++ b/homeassistant/components/dialogflow/translations/tr.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, "create_entry": { - "default": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in [Dialogflow'un webhook entegrasyonunu]( {dialogflow_url} ) ayarlaman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST\n - \u0130\u00e7erik T\u00fcr\u00fc: uygulama/json \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url}" + "default": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in [Dialogflow'un web kancas\u0131 entegrasyonunu]( {dialogflow_url} ) kurman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST\n - \u0130\u00e7erik T\u00fcr\u00fc: uygulama/json \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url} ) bak\u0131n." }, "step": { "user": { diff --git a/homeassistant/components/directv/manifest.json b/homeassistant/components/directv/manifest.json index d6fc946ab79..99b1d6a0100 100644 --- a/homeassistant/components/directv/manifest.json +++ b/homeassistant/components/directv/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/directv", "requirements": ["directv==0.4.0"], "codeowners": [], - "quality_scale": "gold", + "quality_scale": "silver", "config_flow": true, "ssdp": [ { diff --git a/homeassistant/components/directv/translations/lv.json b/homeassistant/components/directv/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/directv/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/__init__.py b/homeassistant/components/dlink/__init__.py index 644e7975a0e..40fce4acf76 100644 --- a/homeassistant/components/dlink/__init__.py +++ b/homeassistant/components/dlink/__init__.py @@ -1 +1,39 @@ -"""The dlink component.""" +"""The D-Link Power Plug integration.""" +from __future__ import annotations + +from pyW215.pyW215 import SmartPlug + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import CONF_USE_LEGACY_PROTOCOL, DOMAIN +from .data import SmartPlugData + +PLATFORMS = [Platform.SWITCH] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up D-Link Power Plug from a config entry.""" + smartplug = await hass.async_add_executor_job( + SmartPlug, + entry.data[CONF_HOST], + entry.data[CONF_PASSWORD], + entry.data[CONF_USERNAME], + entry.data[CONF_USE_LEGACY_PROTOCOL], + ) + if not smartplug.authenticated and smartplug.use_legacy_protocol: + raise ConfigEntryNotReady("Cannot connect/authenticate") + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SmartPlugData(smartplug) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/dlink/config_flow.py b/homeassistant/components/dlink/config_flow.py new file mode 100644 index 00000000000..4499e2efffc --- /dev/null +++ b/homeassistant/components/dlink/config_flow.py @@ -0,0 +1,136 @@ +"""Config flow for the D-Link Power Plug integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from pyW215.pyW215 import SmartPlug +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components import dhcp +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult + +from .const import CONF_USE_LEGACY_PROTOCOL, DEFAULT_NAME, DEFAULT_USERNAME, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class DLinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for D-Link Power Plug.""" + + def __init__(self) -> None: + """Initialize a D-Link Power Plug flow.""" + self.ip_address: str | None = None + + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Handle dhcp discovery.""" + await self.async_set_unique_id(discovery_info.macaddress) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) + for entry in self.hass.config_entries.async_entries(DOMAIN): + if not entry.unique_id and entry.data[CONF_HOST] == discovery_info.ip: + # Add mac address as the unique id, can be removed with import + self.hass.config_entries.async_update_entry( + entry, unique_id=discovery_info.macaddress + ) + return self.async_abort(reason="already_configured") + + self.ip_address = discovery_info.ip + return await self.async_step_confirm_discovery() + + async def async_step_confirm_discovery( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Allow the user to confirm adding the device.""" + errors = {} + if user_input is not None: + if ( + error := await self.hass.async_add_executor_job( + self._try_connect, user_input + ) + ) is None: + return self.async_create_entry( + title=DEFAULT_NAME, + data=user_input | {CONF_HOST: self.ip_address}, + ) + errors["base"] = error + + user_input = user_input or {} + return self.async_show_form( + step_id="confirm_discovery", + data_schema=vol.Schema( + { + vol.Optional( + CONF_USERNAME, + default=user_input.get(CONF_USERNAME, DEFAULT_USERNAME), + ): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_USE_LEGACY_PROTOCOL): bool, + } + ), + errors=errors, + ) + + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Import a config entry.""" + self._async_abort_entries_match({CONF_HOST: config[CONF_HOST]}) + title = config.pop(CONF_NAME, DEFAULT_NAME) + return self.async_create_entry( + title=title, + data=config, + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by the user.""" + errors = {} + if user_input is not None: + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + + if ( + error := await self.hass.async_add_executor_job( + self._try_connect, user_input + ) + ) is None: + return self.async_create_entry( + title=DEFAULT_NAME, + data=user_input, + ) + errors["base"] = error + + user_input = user_input or {} + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_HOST, default=user_input.get(CONF_HOST, self.ip_address) + ): str, + vol.Optional( + CONF_USERNAME, + default=user_input.get(CONF_USERNAME, DEFAULT_USERNAME), + ): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_USE_LEGACY_PROTOCOL): bool, + } + ), + errors=errors, + ) + + def _try_connect(self, user_input: dict[str, Any]) -> str | None: + """Try connecting to D-Link Power Plug.""" + try: + smartplug = SmartPlug( + user_input.get(CONF_HOST, self.ip_address), + user_input[CONF_PASSWORD], + user_input[CONF_USERNAME], + user_input[CONF_USE_LEGACY_PROTOCOL], + ) + except Exception as ex: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception: %s", ex) + return "unknown" + if not smartplug.authenticated and smartplug.use_legacy_protocol: + return "cannot_connect" + return None diff --git a/homeassistant/components/dlink/const.py b/homeassistant/components/dlink/const.py new file mode 100644 index 00000000000..b39cd8be476 --- /dev/null +++ b/homeassistant/components/dlink/const.py @@ -0,0 +1,12 @@ +"""Constants for the D-Link Power Plug integration.""" + +ATTRIBUTION = "Data provided by D-Link" +ATTR_TOTAL_CONSUMPTION = "total_consumption" + +CONF_USE_LEGACY_PROTOCOL = "use_legacy_protocol" + +DEFAULT_NAME = "D-Link Smart Plug W215" +DEFAULT_USERNAME = "admin" +DOMAIN = "dlink" + +MANUFACTURER = "D-Link" diff --git a/homeassistant/components/dlink/data.py b/homeassistant/components/dlink/data.py new file mode 100644 index 00000000000..b93cd219166 --- /dev/null +++ b/homeassistant/components/dlink/data.py @@ -0,0 +1,57 @@ +"""Data for the D-Link Power Plug integration.""" +from __future__ import annotations + +from datetime import datetime +import logging +import urllib + +from pyW215.pyW215 import SmartPlug + +from homeassistant.util import dt as dt_util + +_LOGGER = logging.getLogger(__name__) + + +class SmartPlugData: + """Get the latest data from smart plug.""" + + def __init__(self, smartplug: SmartPlug) -> None: + """Initialize the data object.""" + self.smartplug = smartplug + self.state: str | None = None + self.temperature: str = "" + self.current_consumption: str = "" + self.total_consumption: str = "" + self.available = False + self._n_tried = 0 + self._last_tried: datetime | None = None + + def update(self) -> None: + """Get the latest data from the smart plug.""" + if self._last_tried is not None: + last_try_s = (dt_util.now() - self._last_tried).total_seconds() / 60 + retry_seconds = min(self._n_tried * 2, 10) - last_try_s + if self._n_tried > 0 and retry_seconds > 0: + _LOGGER.warning("Waiting %s s to retry", retry_seconds) + return + + _state = "unknown" + + try: + self._last_tried = dt_util.now() + _state = self.smartplug.state + except urllib.error.HTTPError: + _LOGGER.error("D-Link connection problem") + if _state == "unknown": + self._n_tried += 1 + self.available = False + _LOGGER.warning("Failed to connect to D-Link switch") + return + + self.state = _state + self.available = True + + self.temperature = self.smartplug.temperature + self.current_consumption = self.smartplug.current_consumption + self.total_consumption = self.smartplug.total_consumption + self._n_tried = 0 diff --git a/homeassistant/components/dlink/entity.py b/homeassistant/components/dlink/entity.py new file mode 100644 index 00000000000..33302f7fffa --- /dev/null +++ b/homeassistant/components/dlink/entity.py @@ -0,0 +1,41 @@ +"""Entity representing a D-Link Power Plug device.""" +from __future__ import annotations + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ATTR_CONNECTIONS +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription + +from .const import ATTRIBUTION, DOMAIN, MANUFACTURER +from .data import SmartPlugData + + +class DLinkEntity(Entity): + """Representation of a D-Link Power Plug entity.""" + + _attr_attribution = ATTRIBUTION + + def __init__( + self, + config_entry: ConfigEntry, + data: SmartPlugData, + description: EntityDescription, + ) -> None: + """Initialize a D-Link Power Plug entity.""" + self.data = data + self.entity_description = description + if config_entry.source == SOURCE_IMPORT: + self._attr_name = config_entry.title + else: + self._attr_has_entity_name = True + self._attr_unique_id = f"{config_entry.entry_id}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, config_entry.entry_id)}, + manufacturer=MANUFACTURER, + model=data.smartplug.model_name, + name=config_entry.title, + ) + if config_entry.unique_id: + self._attr_device_info[ATTR_CONNECTIONS] = { + (dr.CONNECTION_NETWORK_MAC, config_entry.unique_id) + } diff --git a/homeassistant/components/dlink/manifest.json b/homeassistant/components/dlink/manifest.json index 9319eb8dd0f..112e771839b 100644 --- a/homeassistant/components/dlink/manifest.json +++ b/homeassistant/components/dlink/manifest.json @@ -1,9 +1,12 @@ { "domain": "dlink", "name": "D-Link Wi-Fi Smart Plugs", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlink", "requirements": ["pyW215==0.7.0"], - "codeowners": [], + "dhcp": [{ "hostname": "dsp-w215" }], + "codeowners": ["@tkdrob"], "iot_class": "local_polling", - "loggers": ["pyW215"] + "loggers": ["pyW215"], + "integration_type": "device" } diff --git a/homeassistant/components/dlink/strings.json b/homeassistant/components/dlink/strings.json new file mode 100644 index 00000000000..9ac7453093c --- /dev/null +++ b/homeassistant/components/dlink/strings.json @@ -0,0 +1,34 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "password": "Password (default: PIN code on the back)", + "username": "[%key:common::config_flow::data::username%]", + "use_legacy_protocol": "Use legacy protocol" + } + }, + "confirm_discovery": { + "data": { + "password": "[%key:component::dlink::config::step::user::data::password%]", + "username": "[%key:common::config_flow::data::username%]", + "use_legacy_protocol": "[%key:component::dlink::config::step::user::data::use_legacy_protocol%]" + } + } + }, + "error": { + "cannot_connect": "Failed to connect/authenticate", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "issues": { + "deprecated_yaml": { + "title": "The D-Link Smart Plug YAML configuration is being removed", + "description": "Configuring D-Link Smart Plug using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the D-Link Power Plug YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/dlink/switch.py b/homeassistant/components/dlink/switch.py index b38460ddb8f..e6b9a4c7883 100644 --- a/homeassistant/components/dlink/switch.py +++ b/homeassistant/components/dlink/switch.py @@ -1,15 +1,17 @@ -"""Support for D-Link W215 smart switch.""" +"""Support for D-Link Power Plug Switches.""" from __future__ import annotations from datetime import timedelta -import logging from typing import Any -import urllib -from pyW215.pyW215 import SmartPlug import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity +from homeassistant.components.switch import ( + PLATFORM_SCHEMA, + SwitchEntity, + SwitchEntityDescription, +) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, @@ -21,31 +23,35 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import dt as dt_util -_LOGGER = logging.getLogger(__name__) - -ATTR_TOTAL_CONSUMPTION = "total_consumption" - -CONF_USE_LEGACY_PROTOCOL = "use_legacy_protocol" - -DEFAULT_NAME = "D-Link Smart Plug W215" -DEFAULT_PASSWORD = "" -DEFAULT_USERNAME = "admin" +from .const import ( + ATTR_TOTAL_CONSUMPTION, + CONF_USE_LEGACY_PROTOCOL, + DEFAULT_NAME, + DEFAULT_USERNAME, + DOMAIN, +) +from .entity import DLinkEntity SCAN_INTERVAL = timedelta(minutes=2) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + vol.Required(CONF_PASSWORD, default=""): cv.string, vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_USE_LEGACY_PROTOCOL, default=False): cv.boolean, } ) +SWITCH_TYPE = SwitchEntityDescription( + key="switch", + name="Switch", +) + def setup_platform( hass: HomeAssistant, @@ -54,47 +60,50 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up a D-Link Smart Plug.""" - - host = config[CONF_HOST] - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - use_legacy_protocol = config[CONF_USE_LEGACY_PROTOCOL] - name = config[CONF_NAME] - - smartplug = SmartPlug(host, password, username, use_legacy_protocol) - data = SmartPlugData(smartplug) - - add_entities([SmartPlugSwitch(hass, data, name)], True) + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.4.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) -class SmartPlugSwitch(SwitchEntity): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the D-Link Power Plug switch.""" + async_add_entities( + [SmartPlugSwitch(entry, hass.data[DOMAIN][entry.entry_id], SWITCH_TYPE)], + True, + ) + + +class SmartPlugSwitch(DLinkEntity, SwitchEntity): """Representation of a D-Link Smart Plug switch.""" - def __init__(self, hass, data, name): - """Initialize the switch.""" - self.units = hass.config.units - self.data = data - self._name = name - @property - def name(self): - """Return the name of the Smart Plug.""" - return self._name - - @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the device.""" try: - ui_temp = self.units.temperature( + temperature = self.hass.config.units.temperature( int(self.data.temperature), UnitOfTemperature.CELSIUS ) - temperature = ui_temp - except (ValueError, TypeError): + except ValueError: temperature = None try: total_consumption = float(self.data.total_consumption) - except (ValueError, TypeError): + except ValueError: total_consumption = None attrs = { @@ -105,7 +114,7 @@ class SmartPlugSwitch(SwitchEntity): return attrs @property - def is_on(self): + def is_on(self) -> bool: """Return true if switch is on.""" return self.data.state == "ON" @@ -125,48 +134,3 @@ class SmartPlugSwitch(SwitchEntity): def available(self) -> bool: """Return True if entity is available.""" return self.data.available - - -class SmartPlugData: - """Get the latest data from smart plug.""" - - def __init__(self, smartplug): - """Initialize the data object.""" - self.smartplug = smartplug - self.state = None - self.temperature = None - self.current_consumption = None - self.total_consumption = None - self.available = False - self._n_tried = 0 - self._last_tried = None - - def update(self): - """Get the latest data from the smart plug.""" - if self._last_tried is not None: - last_try_s = (dt_util.now() - self._last_tried).total_seconds() / 60 - retry_seconds = min(self._n_tried * 2, 10) - last_try_s - if self._n_tried > 0 and retry_seconds > 0: - _LOGGER.warning("Waiting %s s to retry", retry_seconds) - return - - _state = "unknown" - - try: - self._last_tried = dt_util.now() - _state = self.smartplug.state - except urllib.error.HTTPError: - _LOGGER.error("D-Link connection problem") - if _state == "unknown": - self._n_tried += 1 - self.available = False - _LOGGER.warning("Failed to connect to D-Link switch") - return - - self.state = _state - self.available = True - - self.temperature = self.smartplug.temperature - self.current_consumption = self.smartplug.current_consumption - self.total_consumption = self.smartplug.total_consumption - self._n_tried = 0 diff --git a/homeassistant/components/dlink/translations/bg.json b/homeassistant/components/dlink/translations/bg.json new file mode 100644 index 00000000000..41947ef4c39 --- /dev/null +++ b/homeassistant/components/dlink/translations/bg.json @@ -0,0 +1,34 @@ +{ + "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/\u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0438\u0440\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "confirm_discovery": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430 (\u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435: \u041f\u0418\u041d \u043a\u043e\u0434 \u043d\u0430 \u0433\u044a\u0440\u0431\u0430)", + "use_legacy_protocol": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043d\u0430\u0441\u043b\u0435\u0434\u0435\u043d \u043f\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": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430 (\u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435: PIN \u043a\u043e\u0434 \u043e\u0442\u0437\u0430\u0434)", + "use_legacy_protocol": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043d\u0430\u0441\u043b\u0435\u0434\u0435\u043d \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 D-Link Smart Plug \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 D-Link Power Plug \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 D-Link Smart Plug \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/ca.json b/homeassistant/components/dlink/translations/ca.json new file mode 100644 index 00000000000..741a8f9c006 --- /dev/null +++ b/homeassistant/components/dlink/translations/ca.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar/autenticar", + "unknown": "Error inesperat" + }, + "step": { + "confirm_discovery": { + "data": { + "password": "Contrasenya (per defecte: codi PIN de la part posterior)", + "use_legacy_protocol": "Utilitza el protocol heretat ('legacy')", + "username": "Nom d'usuari" + } + }, + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya (per defecte: codi PIN de la part posterior)", + "use_legacy_protocol": "Utilitza el protocol heretat ('legacy')", + "username": "Nom d'usuari" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de D-Link Smart Plug mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML de D-Link Smart Plug del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de D-Link Smart Plug est\u00e0 sent eliminada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/de.json b/homeassistant/components/dlink/translations/de.json new file mode 100644 index 00000000000..dc2807d1155 --- /dev/null +++ b/homeassistant/components/dlink/translations/de.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung/Authentifizierung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "confirm_discovery": { + "data": { + "password": "Passwort (Standard: PIN-Code auf der R\u00fcckseite)", + "use_legacy_protocol": "Legacy-Protokoll verwenden", + "username": "Benutzername" + } + }, + "user": { + "data": { + "host": "Host", + "password": "Passwort (Standard: PIN-Code auf der R\u00fcckseite)", + "use_legacy_protocol": "Legacy-Protokoll verwenden", + "username": "Benutzername" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration des D-Link Smart Plug mittels YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die D-Link Power Plug YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die D-Link Smart Plug YAML-Konfiguration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/el.json b/homeassistant/components/dlink/translations/el.json new file mode 100644 index 00000000000..c0eab884931 --- /dev/null +++ b/homeassistant/components/dlink/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": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2/\u03c0\u03b9\u03c3\u03c4\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": { + "confirm_discovery": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 (\u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae: \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03c3\u03c4\u03bf \u03c0\u03af\u03c3\u03c9 \u03bc\u03ad\u03c1\u03bf\u03c2)", + "use_legacy_protocol": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03c0\u03b1\u03bb\u03b1\u03b9\u03bf\u03cd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03bf\u03c5", + "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", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 (\u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae: \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 PIN \u03c3\u03c4\u03bf \u03c0\u03af\u03c3\u03c9 \u03bc\u03ad\u03c1\u03bf\u03c2)", + "use_legacy_protocol": "\u03a7\u03c1\u03ae\u03c3\u03b7 \u03c0\u03b1\u03bb\u03b1\u03b9\u03bf\u03cd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03bf\u03c5", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ad\u03be\u03c5\u03c0\u03bd\u03bf\u03c5 \u03b2\u03cd\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2 D-Link \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03c4\u03bf\u03c5 D-Link Power Plug \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML Smart Plug D-Link \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/en.json b/homeassistant/components/dlink/translations/en.json new file mode 100644 index 00000000000..1fa47bbb3c1 --- /dev/null +++ b/homeassistant/components/dlink/translations/en.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect/authenticate", + "unknown": "Unexpected error" + }, + "step": { + "confirm_discovery": { + "data": { + "password": "Password (default: PIN code on the back)", + "use_legacy_protocol": "Use legacy protocol", + "username": "Username" + } + }, + "user": { + "data": { + "host": "Host", + "password": "Password (default: PIN code on the back)", + "use_legacy_protocol": "Use legacy protocol", + "username": "Username" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring D-Link Smart Plug using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the D-Link Power Plug YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The D-Link Smart Plug YAML configuration is being removed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/es.json b/homeassistant/components/dlink/translations/es.json new file mode 100644 index 00000000000..2a62797aab6 --- /dev/null +++ b/homeassistant/components/dlink/translations/es.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar/autenticar", + "unknown": "Error inesperado" + }, + "step": { + "confirm_discovery": { + "data": { + "password": "Contrase\u00f1a (por defecto: c\u00f3digo PIN en la parte posterior)", + "use_legacy_protocol": "Usar protocolo heredado", + "username": "Nombre de usuario" + } + }, + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a (por defecto: c\u00f3digo PIN en la parte posterior)", + "use_legacy_protocol": "Usar protocolo heredado", + "username": "Nombre de usuario" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de D-Link Smart Plug mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML de D-Link Power Plug de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de D-Link Smart Plug" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/et.json b/homeassistant/components/dlink/translations/et.json new file mode 100644 index 00000000000..4696ebd8aed --- /dev/null +++ b/homeassistant/components/dlink/translations/et.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchenduse loomine/autentimine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "confirm_discovery": { + "data": { + "password": "Parool (vaikimisi: PIN-kood tagak\u00fcljel)", + "use_legacy_protocol": "Kasuta p\u00e4randprotokolli", + "username": "Kasutajanimi" + } + }, + "user": { + "data": { + "host": "Host", + "password": "Parool (vaikimisi: PIN-kood tagak\u00fcljel)", + "use_legacy_protocol": "Kasuta p\u00e4randprotokolli", + "username": "Kasutajanimi" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "D-Linki nutipistiku konfigureerimine YAML-i abil eemaldatakse.\n\nOlemasolev YAML-i konfiguratsioon imporditakse kasutajaliidesesse automaatselt.\n\nSelle probleemi lahendamiseks eemalda failist configuration.yaml D-Linki toitepistiku YAML konfiguratsioon ja taask\u00e4ivita Home Assistant.", + "title": "D-Link Smart Plugi YAML-i konfiguratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/he.json b/homeassistant/components/dlink/translations/he.json new file mode 100644 index 00000000000..a64eb02d6aa --- /dev/null +++ b/homeassistant/components/dlink/translations/he.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "confirm_discovery": { + "data": { + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/hu.json b/homeassistant/components/dlink/translations/hu.json new file mode 100644 index 00000000000..02e35bfd414 --- /dev/null +++ b/homeassistant/components/dlink/translations/hu.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Nem siker\u00fclt a csatlakoz\u00e1s/hiteles\u00edt\u00e9s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "confirm_discovery": { + "data": { + "password": "Jelsz\u00f3 (alap\u00e9rtelmezett: PIN-k\u00f3d a h\u00e1toldalon)", + "use_legacy_protocol": "Hagyom\u00e1nyos protokoll haszn\u00e1lata", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + }, + "user": { + "data": { + "host": "C\u00edm", + "password": "Jelsz\u00f3 (alap\u00e9rtelmezett: PIN-k\u00f3d a h\u00e1toldalon)", + "use_legacy_protocol": "Hagyom\u00e1nyos protokoll haszn\u00e1lata", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A D-Link Smart Plug konfigur\u00e1l\u00e1sa YAML haszn\u00e1lat\u00e1val elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a D-Link Power Plug YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistant-ot.", + "title": "A D-Link Smart Plug YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/id.json b/homeassistant/components/dlink/translations/id.json new file mode 100644 index 00000000000..0fcd27853cc --- /dev/null +++ b/homeassistant/components/dlink/translations/id.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung/autentikasi", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "confirm_discovery": { + "data": { + "password": "Kata sandi (default: kode PIN di belakang)", + "use_legacy_protocol": "Gunakan protokol lawas", + "username": "Nama Pengguna" + } + }, + "user": { + "data": { + "host": "Host", + "password": "Kata sandi (default: kode PIN di belakang)", + "use_legacy_protocol": "Gunakan protokol lawas", + "username": "Nama Pengguna" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi integrasi D-Link Smart Plug lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML integrasi D-Link Smart Plug dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML D-Link Smart Plug dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/it.json b/homeassistant/components/dlink/translations/it.json new file mode 100644 index 00000000000..299a8c2420d --- /dev/null +++ b/homeassistant/components/dlink/translations/it.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Connessione/autenticazione non riuscita", + "unknown": "Errore imprevisto" + }, + "step": { + "confirm_discovery": { + "data": { + "password": "Password (predefinita: codice PIN sul retro)", + "use_legacy_protocol": "Utilizza il vecchio protocollo", + "username": "Nome utente" + } + }, + "user": { + "data": { + "host": "Host", + "password": "Password (predefinita: codice PIN sul retro)", + "use_legacy_protocol": "Utilizza il vecchio protocollo", + "username": "Nome utente" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di D-Link Smart Plug tramite YAML \u00e8 stata rimossa. \n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente. \n\nRimuovi la configurazione YAML di D-Link Power Plug dal tuo file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di D-Link Smart Plug \u00e8 in fase di rimozione" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/lv.json b/homeassistant/components/dlink/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/dlink/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/nl.json b/homeassistant/components/dlink/translations/nl.json new file mode 100644 index 00000000000..1f9202804d8 --- /dev/null +++ b/homeassistant/components/dlink/translations/nl.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al ingesteld" + }, + "error": { + "cannot_connect": "Verbinding/authenticatie is mislukt", + "unknown": "Onverwachte fout" + }, + "step": { + "confirm_discovery": { + "data": { + "password": "Wachtwoord (standaard: PIN code op de achterkant)", + "use_legacy_protocol": "Gebruik het oude protocol", + "username": "Gebruikersnaam" + } + }, + "user": { + "data": { + "host": "Hostnaam", + "password": "Wachtwoord (standaard: PIN code op de achterkant)", + "use_legacy_protocol": "Het oude protocol gebruiken", + "username": "Gebruikersnaam" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "De mogelijkheid om D-Link Smart Plug via YAML te configureren wordt verwijderd.\n\nJe bestaande YAML configuratie is automatisch ge\u00efmporteerd naar de UI configuratie.\n\nVerwijder de D-Link Power Plug YAML configuratie van je configuration.yaml bestand en herstart Home Assistant om dit probleem op te lossen.", + "title": "De D-Link Smart Plug YAML configuratie wordt verwijderd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/no.json b/homeassistant/components/dlink/translations/no.json new file mode 100644 index 00000000000..a5c5c0af8cf --- /dev/null +++ b/homeassistant/components/dlink/translations/no.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Kunne ikke koble til/autentisere", + "unknown": "Uventet feil" + }, + "step": { + "confirm_discovery": { + "data": { + "password": "Passord (standard: PIN-kode p\u00e5 baksiden)", + "use_legacy_protocol": "Bruk eldre protokoll", + "username": "Brukernavn" + } + }, + "user": { + "data": { + "host": "Vert", + "password": "Passord (standard: PIN-kode p\u00e5 baksiden)", + "use_legacy_protocol": "Bruk eldre protokoll", + "username": "Brukernavn" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av D-Link Smart Plug med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern D-Link Power Plug YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "D-Link Smart Plug YAML-konfigurasjonen blir fjernet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/pl.json b/homeassistant/components/dlink/translations/pl.json new file mode 100644 index 00000000000..b52ee8da07d --- /dev/null +++ b/homeassistant/components/dlink/translations/pl.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia/uwierzytelni\u0107", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "confirm_discovery": { + "data": { + "password": "Has\u0142o (domy\u015blnie: kod PIN z ty\u0142u urz\u0105dzenia)", + "use_legacy_protocol": "U\u017cyj starszego protoko\u0142u", + "username": "Nazwa u\u017cytkownika" + } + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o (domy\u015blnie: kod PIN z ty\u0142u urz\u0105dzenia)", + "use_legacy_protocol": "U\u017cyj starszego protoko\u0142u", + "username": "Nazwa u\u017cytkownika" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja D-Link Smart Plug przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla D-Link Smart Plug zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/pt-BR.json b/homeassistant/components/dlink/translations/pt-BR.json new file mode 100644 index 00000000000..443173beb62 --- /dev/null +++ b/homeassistant/components/dlink/translations/pt-BR.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha ao conectar/autenticar", + "unknown": "Erro inesperado" + }, + "step": { + "confirm_discovery": { + "data": { + "password": "Senha (padr\u00e3o: c\u00f3digo PIN no verso)", + "use_legacy_protocol": "Usar protocolo legado", + "username": "Nome de usu\u00e1rio" + } + }, + "user": { + "data": { + "host": "Host", + "password": "Senha (padr\u00e3o: c\u00f3digo PIN no verso)", + "use_legacy_protocol": "Usar protocolo legado", + "username": "Nome de usu\u00e1rio" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do D-Link Smart Plug usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a IU automaticamente. \n\n Remova a configura\u00e7\u00e3o D-Link Power Plug YAML do seu arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML de D-Link Smart Plug est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/pt.json b/homeassistant/components/dlink/translations/pt.json new file mode 100644 index 00000000000..ae100e45845 --- /dev/null +++ b/homeassistant/components/dlink/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "unknown": "Erro inesperado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/ru.json b/homeassistant/components/dlink/translations/ru.json new file mode 100644 index 00000000000..9b5065ad966 --- /dev/null +++ b/homeassistant/components/dlink/translations/ru.json @@ -0,0 +1,34 @@ +{ + "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 / \u043f\u0440\u043e\u0439\u0442\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "confirm_discovery": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: PIN-\u043a\u043e\u0434 \u043d\u0430 \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0439 \u0441\u0442\u043e\u0440\u043e\u043d\u0435)", + "use_legacy_protocol": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c (\u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e: PIN-\u043a\u043e\u0434 \u043d\u0430 \u043e\u0431\u0440\u0430\u0442\u043d\u043e\u0439 \u0441\u0442\u043e\u0440\u043e\u043d\u0435)", + "use_legacy_protocol": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 D-Link Smart Plug \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 D-Link Smart Plug \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/sk.json b/homeassistant/components/dlink/translations/sk.json new file mode 100644 index 00000000000..538e1c9b4d8 --- /dev/null +++ b/homeassistant/components/dlink/translations/sk.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165/overi\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "confirm_discovery": { + "data": { + "password": "Heslo (predvolen\u00e9: PIN k\u00f3d na zadnej strane)", + "use_legacy_protocol": "Pou\u017e\u00edvanie star\u0161ieho protokolu", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + }, + "user": { + "data": { + "host": "Hostite\u013e", + "password": "Heslo (predvolen\u00e9: PIN k\u00f3d na zadnej strane)", + "use_legacy_protocol": "Pou\u017e\u00edvanie star\u0161ieho protokolu", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigur\u00e1cia D-Link Smart Plug pomocou YAML sa odstra\u0148uje. \n\n Va\u0161a existuj\u00faca konfigur\u00e1cia YAML bola importovan\u00e1 do pou\u017e\u00edvate\u013esk\u00e9ho rozhrania automaticky. \n\n Odstr\u00e1\u0148te konfigur\u00e1ciu D-Link Power Plug YAML zo s\u00faboru configuration.yaml a re\u0161tartujte Home Assistant, aby ste tento probl\u00e9m vyrie\u0161ili.", + "title": "Konfigur\u00e1cia D-Link Smart Plug YAML sa odstra\u0148uje" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/tr.json b/homeassistant/components/dlink/translations/tr.json new file mode 100644 index 00000000000..5903e672a0f --- /dev/null +++ b/homeassistant/components/dlink/translations/tr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanamad\u0131/kimli\u011fi do\u011frulanamad\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Sunucu", + "password": "Parola (varsay\u0131lan: arkadaki PIN kodu)", + "use_legacy_protocol": "Eski protokol\u00fc kullan", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "YAML kullanarak D-Link Smart Plug yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z, kullan\u0131c\u0131 aray\u00fcz\u00fcne otomatik olarak aktar\u0131ld\u0131. \n\n D-Link Power Plug YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu \u00e7\u00f6zmek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "D-Link Smart Plug YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/uk.json b/homeassistant/components/dlink/translations/uk.json new file mode 100644 index 00000000000..fa3974b7973 --- /dev/null +++ b/homeassistant/components/dlink/translations/uk.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f/\u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0443\u0432\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": { + "confirm_discovery": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c (\u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c: PIN-\u043a\u043e\u0434 \u043d\u0430 \u0437\u0432\u043e\u0440\u043e\u0442\u0456)", + "use_legacy_protocol": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u0437\u0430\u0441\u0442\u0430\u0440\u0456\u043b\u0438\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c (\u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c: PIN-\u043a\u043e\u0434 \u043d\u0430 \u0437\u0432\u043e\u0440\u043e\u0442\u0456)", + "use_legacy_protocol": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u0437\u0430\u0441\u0442\u0430\u0440\u0456\u043b\u0438\u0439 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f D-Link Smart Plug \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e YAML \u0432\u0438\u0434\u0430\u043b\u044f\u0454\u0442\u044c\u0441\u044f.\n\n\u0412\u0430\u0448\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f YAML \u0431\u0443\u043b\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0456\u043c\u043f\u043e\u0440\u0442\u043e\u0432\u0430\u043d\u0430 \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430.\n\n\u0412\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e D-Link Power Plug YAML \u0456\u0437 \u0444\u0430\u0439\u043b\u0443 configuration.yaml \u0456 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c Home Assistant, \u0449\u043e\u0431 \u0440\u043e\u0437\u0432'\u044f\u0437\u0430\u0442\u0438 \u0446\u044e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f D-Link Smart Plug YAML \u0432\u0438\u0434\u0430\u043b\u044f\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlink/translations/zh-Hant.json b/homeassistant/components/dlink/translations/zh-Hant.json new file mode 100644 index 00000000000..21074c09e04 --- /dev/null +++ b/homeassistant/components/dlink/translations/zh-Hant.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda/\u9a57\u8b49\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "confirm_discovery": { + "data": { + "password": "\u5bc6\u78bc\uff08\u9810\u8a2d\u70ba\u88dd\u7f6e\u5f8c\u65b9\u7684 PIN \u78bc\uff09", + "use_legacy_protocol": "\u4f7f\u7528\u820a\u901a\u8a0a\u5354\u5b9a", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc\uff08\u9810\u8a2d\u70ba\u88dd\u7f6e\u5f8c\u65b9\u7684 PIN \u78bc\uff09", + "use_legacy_protocol": "\u4f7f\u7528\u820a\u901a\u8a0a\u5354\u5b9a", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 D-Link \u667a\u80fd\u63d2\u5ea7 \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 D-Link \u667a\u80fd\u63d2\u5ea7 YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "D-Link \u667a\u80fd\u63d2\u5ea7 YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/config_flow.py b/homeassistant/components/dlna_dmr/config_flow.py index d1d1aad0ce9..219f0497ff0 100644 --- a/homeassistant/components/dlna_dmr/config_flow.py +++ b/homeassistant/components/dlna_dmr/config_flow.py @@ -6,7 +6,7 @@ from functools import partial from ipaddress import IPv6Address, ip_address import logging from pprint import pformat -from typing import Any, Optional, cast +from typing import Any, cast from urllib.parse import urlparse from async_upnp_client.client import UpnpError @@ -36,7 +36,7 @@ from .data import get_domain_data LOGGER = logging.getLogger(__name__) -FlowInput = Optional[Mapping[str, Any]] +FlowInput = Mapping[str, Any] | None class ConnectError(IntegrationError): diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index acb2cfd4405..460e50d18e4 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Renderer", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": ["async-upnp-client==0.33.0", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.33.1", "getmac==0.8.2"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 658adc58ba6..63bdb8fa603 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable, Coroutine, Sequence import contextlib from datetime import datetime, timedelta import functools -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from async_upnp_client.client import UpnpService, UpnpStateVariable from async_upnp_client.const import NotificationSubType @@ -14,7 +14,6 @@ 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 media_source, ssdp diff --git a/homeassistant/components/dlna_dmr/translations/lv.json b/homeassistant/components/dlna_dmr/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/dlna_dmr/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dmr/translations/tr.json b/homeassistant/components/dlna_dmr/translations/tr.json index 26d911056b7..34bc1776e22 100644 --- a/homeassistant/components/dlna_dmr/translations/tr.json +++ b/homeassistant/components/dlna_dmr/translations/tr.json @@ -16,7 +16,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" }, "import_turn_on": { "description": "L\u00fctfen cihaz\u0131 a\u00e7\u0131n ve ta\u015f\u0131maya devam etmek i\u00e7in g\u00f6nder'i t\u0131klay\u0131n" diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index f141b2c1519..f7407195964 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -3,7 +3,7 @@ "name": "DLNA Digital Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dlna_dms", - "requirements": ["async-upnp-client==0.33.0"], + "requirements": ["async-upnp-client==0.33.1"], "dependencies": ["ssdp"], "after_dependencies": ["media_source"], "ssdp": [ diff --git a/homeassistant/components/dlna_dms/translations/lv.json b/homeassistant/components/dlna_dms/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/dlna_dms/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dlna_dms/translations/tr.json b/homeassistant/components/dlna_dms/translations/tr.json index dc8a5bf02e8..3bdeb2eef73 100644 --- a/homeassistant/components/dlna_dms/translations/tr.json +++ b/homeassistant/components/dlna_dms/translations/tr.json @@ -10,7 +10,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" }, "user": { "data": { diff --git a/homeassistant/components/dnsip/strings.json b/homeassistant/components/dnsip/strings.json index 41ce6c5aeb7..713cc84efd4 100644 --- a/homeassistant/components/dnsip/strings.json +++ b/homeassistant/components/dnsip/strings.json @@ -22,6 +22,9 @@ } } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, "error": { "invalid_resolver": "Invalid IP address for resolver" } diff --git a/homeassistant/components/dnsip/translations/bg.json b/homeassistant/components/dnsip/translations/bg.json index 76814626cd2..0cb084e723f 100644 --- a/homeassistant/components/dnsip/translations/bg.json +++ b/homeassistant/components/dnsip/translations/bg.json @@ -3,5 +3,10 @@ "error": { "invalid_hostname": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442" } + }, + "options": { + "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" + } } } \ No newline at end of file diff --git a/homeassistant/components/dnsip/translations/ca.json b/homeassistant/components/dnsip/translations/ca.json index f84a0e0a643..4b3e2d320f5 100644 --- a/homeassistant/components/dnsip/translations/ca.json +++ b/homeassistant/components/dnsip/translations/ca.json @@ -14,6 +14,9 @@ } }, "options": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, "error": { "invalid_resolver": "Adre\u00e7a IP del resolutor inv\u00e0lida" }, diff --git a/homeassistant/components/dnsip/translations/de.json b/homeassistant/components/dnsip/translations/de.json index 93a4bf8fb26..601f7e51b70 100644 --- a/homeassistant/components/dnsip/translations/de.json +++ b/homeassistant/components/dnsip/translations/de.json @@ -14,6 +14,9 @@ } }, "options": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, "error": { "invalid_resolver": "Ung\u00fcltige IP-Adresse f\u00fcr Aufl\u00f6ser" }, diff --git a/homeassistant/components/dnsip/translations/en.json b/homeassistant/components/dnsip/translations/en.json index 2c773375860..c2f2d233212 100644 --- a/homeassistant/components/dnsip/translations/en.json +++ b/homeassistant/components/dnsip/translations/en.json @@ -14,6 +14,9 @@ } }, "options": { + "abort": { + "already_configured": "Service is already configured" + }, "error": { "invalid_resolver": "Invalid IP address for resolver" }, diff --git a/homeassistant/components/dnsip/translations/et.json b/homeassistant/components/dnsip/translations/et.json index f49e83e9b2a..2a8920756cc 100644 --- a/homeassistant/components/dnsip/translations/et.json +++ b/homeassistant/components/dnsip/translations/et.json @@ -14,6 +14,9 @@ } }, "options": { + "abort": { + "already_configured": "Teenus on juba seadistatud" + }, "error": { "invalid_resolver": "Lahendaja IP-aadress on vale" }, diff --git a/homeassistant/components/dnsip/translations/no.json b/homeassistant/components/dnsip/translations/no.json index ef665c89805..6ff0c6e00fe 100644 --- a/homeassistant/components/dnsip/translations/no.json +++ b/homeassistant/components/dnsip/translations/no.json @@ -14,6 +14,9 @@ } }, "options": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, "error": { "invalid_resolver": "Ugyldig IP-adresse for resolver" }, diff --git a/homeassistant/components/dnsip/translations/ru.json b/homeassistant/components/dnsip/translations/ru.json index 0882153776a..9c778f2db12 100644 --- a/homeassistant/components/dnsip/translations/ru.json +++ b/homeassistant/components/dnsip/translations/ru.json @@ -14,6 +14,9 @@ } }, "options": { + "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": { "invalid_resolver": "\u041d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441 \u0434\u043b\u044f \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u0432\u0430\u0442\u0435\u043b\u044f." }, diff --git a/homeassistant/components/dnsip/translations/zh-Hant.json b/homeassistant/components/dnsip/translations/zh-Hant.json index 5c46b1b0282..c98627e2d7d 100644 --- a/homeassistant/components/dnsip/translations/zh-Hant.json +++ b/homeassistant/components/dnsip/translations/zh-Hant.json @@ -14,6 +14,9 @@ } }, "options": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "invalid_resolver": "\u89e3\u6790\u5668 IP \u4f4d\u5740\u7121\u6548" }, diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 39ed9c552bb..534164e2633 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -2,7 +2,7 @@ "domain": "doods", "name": "DOODS - Dedicated Open Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": ["pydoods==1.0.2", "pillow==9.3.0"], + "requirements": ["pydoods==1.0.2", "pillow==9.4.0"], "codeowners": [], "iot_class": "local_polling", "loggers": ["pydoods"] diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 678340c0259..4ad5e24247e 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -117,9 +117,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="not_doorbird_device") chop_ending = "._axis-video._tcp.local." - friendly_hostname = discovery_info.name - if friendly_hostname.endswith(chop_ending): - friendly_hostname = friendly_hostname[: -len(chop_ending)] + friendly_hostname = discovery_info.name.removesuffix(chop_ending) self.context["title_placeholders"] = { CONF_NAME: friendly_hostname, diff --git a/homeassistant/components/doorbird/translations/lt.json b/homeassistant/components/doorbird/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/doorbird/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/lv.json b/homeassistant/components/doorbird/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/doorbird/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/lv.json b/homeassistant/components/dsmr/translations/lv.json new file mode 100644 index 00000000000..c7b244df6d7 --- /dev/null +++ b/homeassistant/components/dsmr/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/pl.json b/homeassistant/components/dsmr/translations/pl.json index 84a04cff625..7ad220caff3 100644 --- a/homeassistant/components/dsmr/translations/pl.json +++ b/homeassistant/components/dsmr/translations/pl.json @@ -48,6 +48,16 @@ } } }, + "entity": { + "sensor": { + "electricity_tariff": { + "state": { + "low": "niska", + "normal": "normalna" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/dsmr/translations/tr.json b/homeassistant/components/dsmr/translations/tr.json index 2bdcd58a865..abe4653b84d 100644 --- a/homeassistant/components/dsmr/translations/tr.json +++ b/homeassistant/components/dsmr/translations/tr.json @@ -44,6 +44,16 @@ } } }, + "entity": { + "sensor": { + "electricity_tariff": { + "state": { + "low": "D\u00fc\u015f\u00fck", + "normal": "Normal" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/dsmr/translations/uk.json b/homeassistant/components/dsmr/translations/uk.json index 9bca6b00c74..7052b10c480 100644 --- a/homeassistant/components/dsmr/translations/uk.json +++ b/homeassistant/components/dsmr/translations/uk.json @@ -2,6 +2,11 @@ "config": { "abort": { "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "step": { + "setup_serial": { + "title": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } } }, "options": { diff --git a/homeassistant/components/dsmr_reader/definitions.py b/homeassistant/components/dsmr_reader/definitions.py index cc0c851ebda..ddf149d680f 100644 --- a/homeassistant/components/dsmr_reader/definitions.py +++ b/homeassistant/components/dsmr_reader/definitions.py @@ -209,7 +209,6 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = ( DSMRReaderSensorEntityDescription( key="dsmr/consumption/gas/currently_delivered", name="Current gas usage", - device_class=SensorDeviceClass.GAS, native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, state_class=SensorStateClass.MEASUREMENT, ), diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py index 7130380cbf5..72e24c52724 100644 --- a/homeassistant/components/dsmr_reader/sensor.py +++ b/homeassistant/components/dsmr_reader/sensor.py @@ -69,7 +69,10 @@ class DSMRSensor(SensorEntity): @callback def message_received(message): """Handle new MQTT messages.""" - if self.entity_description.state is not None: + if message.payload == "": + self._attr_native_value = None + elif self.entity_description.state is not None: + # Perform optional additional parsing self._attr_native_value = self.entity_description.state(message.payload) else: self._attr_native_value = message.payload diff --git a/homeassistant/components/dsmr_reader/translations/sk.json b/homeassistant/components/dsmr_reader/translations/sk.json index e4c3aef8b19..9bd42fc2223 100644 --- a/homeassistant/components/dsmr_reader/translations/sk.json +++ b/homeassistant/components/dsmr_reader/translations/sk.json @@ -5,7 +5,7 @@ }, "step": { "confirm": { - "description": "Uistite sa, \u017ee ste nakonfigurovali zdroje \u00fadajov \u201erozdelen\u00e1 t\u00e9ma\u201c v aplik\u00e1cii DSMR Reader." + "description": "Uistite sa, \u017ee ste nakonfigurovali zdroje \u00fadajov 'rozdelen\u00e1 t\u00e9ma' v aplik\u00e1cii DSMR Reader." } } }, diff --git a/homeassistant/components/dunehd/translations/el.json b/homeassistant/components/dunehd/translations/el.json index e14fb5c7308..21672d0a3dd 100644 --- a/homeassistant/components/dunehd/translations/el.json +++ b/homeassistant/components/dunehd/translations/el.json @@ -13,7 +13,7 @@ "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." + "description": "\u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7." } } } diff --git a/homeassistant/components/dunehd/translations/lv.json b/homeassistant/components/dunehd/translations/lv.json new file mode 100644 index 00000000000..862ef1ca431 --- /dev/null +++ b/homeassistant/components/dunehd/translations/lv.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/humidifier.py b/homeassistant/components/ecobee/humidifier.py index 502e84b7866..f4c4dad6527 100644 --- a/homeassistant/components/ecobee/humidifier.py +++ b/homeassistant/components/ecobee/humidifier.py @@ -144,7 +144,7 @@ class EcobeeHumidifier(HumidifierEntity): self.data.ecobee.set_humidifier_mode(self.thermostat_index, mode) self.update_without_throttle = True - def set_humidity(self, humidity): + def set_humidity(self, humidity: int) -> None: """Set the humidity level.""" self.data.ecobee.set_humidity(self.thermostat_index, humidity) self.update_without_throttle = True diff --git a/homeassistant/components/econet/__init__.py b/homeassistant/components/econet/__init__.py index ee8db43baf2..6fa54fc70fb 100644 --- a/homeassistant/components/econet/__init__.py +++ b/homeassistant/components/econet/__init__.py @@ -116,6 +116,8 @@ class EcoNetEntity(Entity): def __init__(self, econet): """Initialize.""" self._econet = econet + self._attr_name = econet.device_name + self._attr_unique_id = f"{econet.device_id}_{econet.device_name}" async def async_added_to_hass(self): """Subscribe to device events.""" @@ -143,16 +145,6 @@ class EcoNetEntity(Entity): name=self._econet.device_name, ) - @property - def name(self): - """Return the name of the entity.""" - return self._econet.device_name - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return f"{self._econet.device_id}_{self._econet.device_name}" - @property def temperature_unit(self): """Return the unit of measurement.""" diff --git a/homeassistant/components/econet/binary_sensor.py b/homeassistant/components/econet/binary_sensor.py index b82fbb58c72..b869c3fb135 100644 --- a/homeassistant/components/econet/binary_sensor.py +++ b/homeassistant/components/econet/binary_sensor.py @@ -64,19 +64,12 @@ class EcoNetBinarySensor(EcoNetEntity, BinarySensorEntity): """Initialize.""" super().__init__(econet_device) self.entity_description = description - self._econet = econet_device + self._attr_name = f"{econet_device.device_name}_{description.name}" + self._attr_unique_id = ( + f"{econet_device.device_id}_{econet_device.device_name}_{description.name}" + ) @property def is_on(self): """Return true if the binary sensor is on.""" return getattr(self._econet, self.entity_description.key) - - @property - def name(self): - """Return the name of the entity.""" - return f"{self._econet.device_name}_{self.entity_description.name}" - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return f"{self._econet.device_id}_{self._econet.device_name}_{self.entity_description.name}" diff --git a/homeassistant/components/econet/sensor.py b/homeassistant/components/econet/sensor.py index 130cc6fce37..6de0ba3ff23 100644 --- a/homeassistant/components/econet/sensor.py +++ b/homeassistant/components/econet/sensor.py @@ -1,50 +1,83 @@ """Support for Rheem EcoNet water heaters.""" -from pyeconet.equipment import EquipmentType +from __future__ import annotations -from homeassistant.components.sensor import SensorEntity +from pyeconet.equipment import Equipment, EquipmentType + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, UnitOfEnergy, UnitOfVolume +from homeassistant.const import ( + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS, + UnitOfEnergy, + UnitOfVolume, +) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import EcoNetEntity from .const import DOMAIN, EQUIPMENT -ENERGY_KILO_BRITISH_THERMAL_UNIT = "kBtu" - -TANK_HEALTH = "tank_health" -AVAILABLE_HOT_WATER = "available_hot_water" -COMPRESSOR_HEALTH = "compressor_health" -OVERRIDE_STATUS = "override_status" -WATER_USAGE_TODAY = "water_usage_today" -POWER_USAGE_TODAY = "power_usage_today" -ALERT_COUNT = "alert_count" -WIFI_SIGNAL = "wifi_signal" -RUNNING_STATE = "running_state" - -SENSOR_NAMES_TO_ATTRIBUTES = { - TANK_HEALTH: "tank_health", - AVAILABLE_HOT_WATER: "tank_hot_water_availability", - COMPRESSOR_HEALTH: "compressor_health", - OVERRIDE_STATUS: "override_status", - WATER_USAGE_TODAY: "todays_water_usage", - POWER_USAGE_TODAY: "todays_energy_usage", - ALERT_COUNT: "alert_count", - WIFI_SIGNAL: "wifi_signal", - RUNNING_STATE: "running_state", -} - -SENSOR_NAMES_TO_UNIT_OF_MEASUREMENT = { - TANK_HEALTH: PERCENTAGE, - AVAILABLE_HOT_WATER: PERCENTAGE, - COMPRESSOR_HEALTH: PERCENTAGE, - OVERRIDE_STATUS: None, - WATER_USAGE_TODAY: UnitOfVolume.GALLONS, - POWER_USAGE_TODAY: None, # Depends on unit type - ALERT_COUNT: None, - WIFI_SIGNAL: None, - RUNNING_STATE: None, # This is just a string -} +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="tank_health", + name="tank_health", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="tank_hot_water_availability", + name="available_hot_water", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="compressor_health", + name="compressor_health", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="override_status", + name="override_status", + ), + SensorEntityDescription( + key="todays_water_usage", + name="water_usage_today", + native_unit_of_measurement=UnitOfVolume.GALLONS, + device_class=SensorDeviceClass.WATER, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="todays_energy_usage", + name="power_usage_today", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + SensorEntityDescription( + key="alert_count", + name="alert_count", + ), + SensorEntityDescription( + key="wifi_signal", + name="wifi_signal", + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="running_state", + name="running_state", + ), +) async def async_setup_entry( @@ -52,22 +85,16 @@ async def async_setup_entry( ) -> None: """Set up EcoNet sensor based on a config entry.""" - equipment = hass.data[DOMAIN][EQUIPMENT][entry.entry_id] - sensors = [] - all_equipment = equipment[EquipmentType.WATER_HEATER].copy() - all_equipment.extend(equipment[EquipmentType.THERMOSTAT].copy()) + data = hass.data[DOMAIN][EQUIPMENT][entry.entry_id] + equipment = data[EquipmentType.WATER_HEATER].copy() + equipment.extend(data[EquipmentType.THERMOSTAT].copy()) - for _equip in all_equipment: - for name, attribute in SENSOR_NAMES_TO_ATTRIBUTES.items(): - if getattr(_equip, attribute, None) is not None: - sensors.append(EcoNetSensor(_equip, name)) - # This is None to start with and all device have it - sensors.append(EcoNetSensor(_equip, WIFI_SIGNAL)) - - for water_heater in equipment[EquipmentType.WATER_HEATER]: - # These aren't part of the device and start off as None in pyeconet so always add them - sensors.append(EcoNetSensor(water_heater, WATER_USAGE_TODAY)) - sensors.append(EcoNetSensor(water_heater, POWER_USAGE_TODAY)) + sensors = [ + EcoNetSensor(_equip, description) + for _equip in equipment + for description in SENSOR_TYPES + if getattr(_equip, description.key, False) is not False + ] async_add_entities(sensors) @@ -75,39 +102,26 @@ async def async_setup_entry( class EcoNetSensor(EcoNetEntity, SensorEntity): """Define a Econet sensor.""" - def __init__(self, econet_device, device_name): + def __init__( + self, + econet_device: Equipment, + description: SensorEntityDescription, + ) -> None: """Initialize.""" super().__init__(econet_device) - self._econet = econet_device - self._device_name = device_name + self.entity_description = description + self._attr_name = f"{econet_device.device_name}_{description.name}" + self._attr_unique_id = ( + f"{econet_device.device_id}_{econet_device.device_name}_{description.name}" + ) @property def native_value(self): """Return sensors state.""" - value = getattr(self._econet, SENSOR_NAMES_TO_ATTRIBUTES[self._device_name]) + value = getattr(self._econet, self.entity_description.key) + if self.entity_description.name == "power_usage_today": + if self._econet.energy_type == "KBTU": + value = value * 0.2930710702 # Convert kBtu to kWh if isinstance(value, float): value = round(value, 2) return value - - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - unit_of_measurement = SENSOR_NAMES_TO_UNIT_OF_MEASUREMENT[self._device_name] - if self._device_name == POWER_USAGE_TODAY: - if self._econet.energy_type == ENERGY_KILO_BRITISH_THERMAL_UNIT.upper(): - unit_of_measurement = ENERGY_KILO_BRITISH_THERMAL_UNIT - else: - unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR - return unit_of_measurement - - @property - def name(self) -> str: - """Return the name of the entity.""" - return f"{self._econet.device_name}_{self._device_name}" - - @property - def unique_id(self) -> str: - """Return the unique ID of the entity.""" - return ( - f"{self._econet.device_id}_{self._econet.device_name}_{self._device_name}" - ) diff --git a/homeassistant/components/econet/translations/hu.json b/homeassistant/components/econet/translations/hu.json index 0f9cf18f203..a26659783a4 100644 --- a/homeassistant/components/econet/translations/hu.json +++ b/homeassistant/components/econet/translations/hu.json @@ -15,7 +15,7 @@ "email": "E-mail", "password": "Jelsz\u00f3" }, - "title": "\u00c1ll\u00edtsa be a Rheem EcoNet fi\u00f3kot" + "title": "Rheem EcoNet fi\u00f3k be\u00e1ll\u00edt\u00e1sa" } } } diff --git a/homeassistant/components/econet/translations/lv.json b/homeassistant/components/econet/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/econet/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/tr.json b/homeassistant/components/econet/translations/tr.json index 5261e78e7e4..e4d6c8125fa 100644 --- a/homeassistant/components/econet/translations/tr.json +++ b/homeassistant/components/econet/translations/tr.json @@ -15,7 +15,7 @@ "email": "E-posta", "password": "Parola" }, - "title": "Rheem EcoNet Hesab\u0131n\u0131 Kur" + "title": "Rheem EcoNet Hesab\u0131n\u0131 Kurun" } } } diff --git a/homeassistant/components/econet/translations/uk.json b/homeassistant/components/econet/translations/uk.json new file mode 100644 index 00000000000..5c722c2a338 --- /dev/null +++ b/homeassistant/components/econet/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecowitt/manifest.json b/homeassistant/components/ecowitt/manifest.json index f6508b4fc57..e1abdb0a0e0 100644 --- a/homeassistant/components/ecowitt/manifest.json +++ b/homeassistant/components/ecowitt/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecowitt", "dependencies": ["webhook"], - "requirements": ["aioecowitt==2022.11.0"], + "requirements": ["aioecowitt==2023.01.0"], "codeowners": ["@pvizeli"], "iot_class": "local_push" } diff --git a/homeassistant/components/ecowitt/translations/ca.json b/homeassistant/components/ecowitt/translations/ca.json index bfd4e1111cd..d25494cf584 100644 --- a/homeassistant/components/ecowitt/translations/ca.json +++ b/homeassistant/components/ecowitt/translations/ca.json @@ -1,7 +1,7 @@ { "config": { "create_entry": { - "default": "Per acabar de configurar la integraci\u00f3, utilitza l'aplicaci\u00f3 Ecowitt (al m\u00f2bil) o v\u00e9s a Ecowitt WebUI a trav\u00e9s d'un navegador anant a l'adre\u00e7a IP de l'estaci\u00f3.\n\nTria la teva estaci\u00f3 -> Men\u00fa Altres -> Servidors de pujada DIY. Prem seg\u00fcent (next) i selecciona 'Personalitzat' ('Customized')\n\n- IP del servidor: `{servidor}`\n- Ruta: `{path}`\n- Port: `{port}`\n\nFes clic a 'Desa' ('Save')." + "default": "Per acabar de configurar la integraci\u00f3, utilitza l'aplicaci\u00f3 Ecowitt (al m\u00f2bil) o v\u00e9s a Ecowitt WebUI a trav\u00e9s d'un navegador anant a l'adre\u00e7a IP de l'estaci\u00f3.\n\nTria la teva estaci\u00f3 -> Men\u00fa Altres -> Servidors de pujada DIY. Prem seg\u00fcent (next) i selecciona 'Personalitzat' ('Customized')\n\n- IP del servidor: `{server}`\n- Ruta: `{path}`\n- Port: `{port}`\n\nFes clic a 'Desa' ('Save')." }, "step": { "user": { diff --git a/homeassistant/components/ecowitt/translations/el.json b/homeassistant/components/ecowitt/translations/el.json index 9c5910d71e6..256891b15db 100644 --- a/homeassistant/components/ecowitt/translations/el.json +++ b/homeassistant/components/ecowitt/translations/el.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03c3\u03c4\u03bf\u03cd\u03bd \u03c4\u03b1 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b1 \u03b2\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7. \n\n \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Ecowitt (\u03c3\u03c4\u03bf \u03c4\u03b7\u03bb\u03ad\u03c6\u03c9\u03bd\u03cc \u03c3\u03b1\u03c2) \u03ae \u03b1\u03c0\u03bf\u03ba\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf Ecowitt WebUI \u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03c0\u03b5\u03c1\u03b9\u03ae\u03b3\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03c4\u03bf\u03c5 \u03c3\u03c4\u03b1\u03b8\u03bc\u03bf\u03cd.\n \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03c3\u03c4\u03b1\u03b8\u03bc\u03cc \u03c3\u03b1\u03c2 - > \u039c\u03b5\u03bd\u03bf\u03cd \u0386\u03bb\u03bb\u03b1 - > \u0394\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ad\u03c2 \u03bc\u03b5\u03c4\u03b1\u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7\u03c2 DIY.\n \u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03bf \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \"\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf\" \n\n \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03bf \u03c0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf Ecowitt \u03ba\u03b1\u03b9 \u03b2\u03ac\u03bb\u03c4\u03b5 \u03c4\u03bf ip/hostname \u03c4\u03bf\u03c5 hass server \u03c3\u03b1\u03c2.\n \u0397 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03b5\u03b9, \u03bc\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03c4\u03b9\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5 \u03bc\u03b5 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ad\u03c2 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc /.\n \u0391\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2. \u03a4\u03bf Ecowitt \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1 \u03bd\u03b1 \u03b1\u03c1\u03c7\u03af\u03c3\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03b5\u03af \u03bd\u03b1 \u03c3\u03c4\u03b5\u03af\u03bb\u03b5\u03b9 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03c3\u03c4\u03bf\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03c3\u03b1\u03c2." + "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 Ecowitt;" } } } diff --git a/homeassistant/components/ecowitt/translations/sk.json b/homeassistant/components/ecowitt/translations/sk.json index cead9f1f2f8..4033ba57801 100644 --- a/homeassistant/components/ecowitt/translations/sk.json +++ b/homeassistant/components/ecowitt/translations/sk.json @@ -1,7 +1,7 @@ { "config": { "create_entry": { - "default": "Na dokon\u010denie nastavenia integr\u00e1cie pou\u017eite aplik\u00e1ciu Ecowitt (na telef\u00f3ne) alebo prejdite do webov\u00e9ho rozhrania Ecowitt v prehliada\u010di na IP adrese stanice. \n\nVyberte si svoju stanicu - > Ponuka Ostatn\u00e9 - > Urob si s\u00e1m servery. Kliknite \u010falej a vyberte \u201ePrisp\u00f4soben\u00e9\u201c \n\n - IP servera: `{server}`\n - Cesta: `{path}`\n - Port: `{port}` \n\nKliknite na 'Ulo\u017ei\u0165'." + "default": "Na dokon\u010denie nastavenia integr\u00e1cie pou\u017eite aplik\u00e1ciu Ecowitt (na telef\u00f3ne) alebo prejdite do webov\u00e9ho rozhrania Ecowitt v prehliada\u010di na IP adrese stanice. \n\nVyberte si svoju stanicu - > Ponuka Ostatn\u00e9 - > Urob si s\u00e1m servery. Kliknite \u010falej a vyberte 'Prisp\u00f4soben\u00e9' \n\n - IP servera: `{server}`\n - Cesta: `{path}`\n - Port: `{port}` \n\nKliknite na 'Ulo\u017ei\u0165'." }, "step": { "user": { diff --git a/homeassistant/components/efergy/translations/lv.json b/homeassistant/components/efergy/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/efergy/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/translations/lv.json b/homeassistant/components/eight_sleep/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/eight_sleep/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/lv.json b/homeassistant/components/elgato/translations/lv.json index 5babfa037ac..b91ccecb9d1 100644 --- a/homeassistant/components/elgato/translations/lv.json +++ b/homeassistant/components/elgato/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/elkm1/translations/sk.json b/homeassistant/components/elkm1/translations/sk.json index 0ece3fd0388..621827c721a 100644 --- a/homeassistant/components/elkm1/translations/sk.json +++ b/homeassistant/components/elkm1/translations/sk.json @@ -34,7 +34,7 @@ "temperature_unit": "Jednotka teploty pou\u017e\u00edva ElkM1.", "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" }, - "description": "Re\u0165azec adresy mus\u00ed by\u0165 v tvare 'adresa[:port]' pre 'zabezpe\u010den\u00e9' a 'nezabezpe\u010den\u00e9'. Pr\u00edklad: '192.168.1.1'. Port je volite\u013en\u00fd a \u0161tandardne je nastaven\u00fd na 2101 pre \u201enezabezpe\u010den\u00fd\u201c a 2601 pre \u201ezabezpe\u010den\u00fd\u201c. Pre s\u00e9riov\u00fd protokol mus\u00ed by\u0165 adresa v tvare 'tty[:baud]'. Pr\u00edklad: '/dev/ttyS1'. Prenosov\u00e1 r\u00fdchlos\u0165 je volite\u013en\u00e1 a predvolen\u00e1 je 115200.", + "description": "Re\u0165azec adresy mus\u00ed by\u0165 v tvare 'adresa[:port]' pre 'zabezpe\u010den\u00e9' a 'nezabezpe\u010den\u00e9'. Pr\u00edklad: '192.168.1.1'. Port je volite\u013en\u00fd a \u0161tandardne je nastaven\u00fd na 2101 pre 'nezabezpe\u010den\u00fd' a 2601 pre 'zabezpe\u010den\u00fd'. Pre s\u00e9riov\u00fd protokol mus\u00ed by\u0165 adresa v tvare 'tty[:baud]'. Pr\u00edklad: '/dev/ttyS1'. Prenosov\u00e1 r\u00fdchlos\u0165 je volite\u013en\u00e1 a predvolen\u00e1 je 115200.", "title": "Pripojte k Elk-M1 Control" }, "user": { diff --git a/homeassistant/components/elkm1/translations/uk.json b/homeassistant/components/elkm1/translations/uk.json index d794c6ecf69..d327836aff6 100644 --- a/homeassistant/components/elkm1/translations/uk.json +++ b/homeassistant/components/elkm1/translations/uk.json @@ -2,7 +2,9 @@ "config": { "abort": { "address_already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437 \u0446\u0456\u0454\u044e \u0430\u0434\u0440\u0435\u0441\u043e\u044e \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435.", - "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437 \u0446\u0438\u043c \u043f\u0440\u0435\u0444\u0456\u043a\u0441\u043e\u043c \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437 \u0446\u0438\u043c \u043f\u0440\u0435\u0444\u0456\u043a\u0441\u043e\u043c \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435.", + "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", @@ -10,7 +12,22 @@ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { + "discovered_connection": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "manual_connection": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, "user": { + "data": { + "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + }, "description": "\u0420\u044f\u0434\u043e\u043a \u0430\u0434\u0440\u0435\u0441\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0430 \u0431\u0443\u0442\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'addres[:port]' \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0456\u0432 'secure' \u0456 'non-secure' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: '192.168.1.1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'port' \u0432\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u043d \u0434\u043e\u0440\u0456\u0432\u043d\u044e\u0454 2101 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'non-secure' \u0456 2601 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'secure'. \u0414\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'serial' \u0430\u0434\u0440\u0435\u0441\u0430 \u043f\u043e\u0432\u0438\u043d\u043d\u0430 \u0431\u0443\u0442\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'tty[:baud]' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: '/dev/ttyS1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'baud' \u0432\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u043d \u0434\u043e\u0440\u0456\u0432\u043d\u044e\u0454 115200.", "title": "Elk-M1 Control" } diff --git a/homeassistant/components/elmax/translations/lv.json b/homeassistant/components/elmax/translations/lv.json index 9cac0f2bb8e..fcf4212b0a5 100644 --- a/homeassistant/components/elmax/translations/lv.json +++ b/homeassistant/components/elmax/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "step": { "panels": { "data": { diff --git a/homeassistant/components/elmax/translations/uk.json b/homeassistant/components/elmax/translations/uk.json new file mode 100644 index 00000000000..7750311efe5 --- /dev/null +++ b/homeassistant/components/elmax/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "panels": { + "data": { + "panel_id": "ID \u043f\u0430\u043d\u0435\u043b\u0456", + "panel_name": "\u041d\u0430\u0437\u0432\u0430 \u041f\u0430\u043d\u0435\u043b\u0456", + "panel_pin": "PIN \u041a\u043e\u0434" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c, \u044f\u043a\u043e\u044e \u043f\u0430\u043d\u0435\u043b\u043b\u044e \u0432\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043a\u0435\u0440\u0443\u0432\u0430\u0442\u0438 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0446\u0456\u0454\u0457 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457. \u0417\u0430\u0443\u0432\u0430\u0436\u0442\u0435, \u0449\u043e \u043f\u0430\u043d\u0435\u043b\u044c \u043c\u0430\u0454 \u0431\u0443\u0442\u0438 \u0423\u0412\u0406\u041c\u041a\u041d\u0415\u041d\u0410, \u0449\u043e\u0431 \u0457\u0457 \u043c\u043e\u0436\u043d\u0430 \u0431\u0443\u043b\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438." + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index f9382d1060b..4a427615aaf 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -285,15 +285,15 @@ class EmonCmsData: except requests.exceptions.RequestException as exception: _LOGGER.error(exception) return + + if req.status_code == HTTPStatus.OK: + self.data = req.json() else: - if req.status_code == HTTPStatus.OK: - self.data = req.json() - else: - _LOGGER.error( - ( - "Please verify if the specified configuration value " - "'%s' is correct! (HTTP Status_code = %d)" - ), - CONF_URL, - req.status_code, - ) + _LOGGER.error( + ( + "Please verify if the specified configuration value " + "'%s' is correct! (HTTP Status_code = %d)" + ), + CONF_URL, + req.status_code, + ) diff --git a/homeassistant/components/emonitor/translations/el.json b/homeassistant/components/emonitor/translations/el.json index 7a5ed2b1482..24ce1bb3454 100644 --- a/homeassistant/components/emonitor/translations/el.json +++ b/homeassistant/components/emonitor/translations/el.json @@ -11,7 +11,7 @@ "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" + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 SiteSage Emonitor" }, "user": { "data": { diff --git a/homeassistant/components/emonitor/translations/lv.json b/homeassistant/components/emonitor/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/emonitor/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emonitor/translations/tr.json b/homeassistant/components/emonitor/translations/tr.json index 72d9a2fcc6b..5471e3b0909 100644 --- a/homeassistant/components/emonitor/translations/tr.json +++ b/homeassistant/components/emonitor/translations/tr.json @@ -10,8 +10,8 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?", - "title": "SiteSage Emonitor Kurulumu" + "description": "{name} ( {host} ) kurmak istiyor musunuz?", + "title": "SiteSage Emonitor'u kurun" }, "user": { "data": { diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index ec06f70a3cc..86102242b31 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -134,7 +134,7 @@ async def async_setup(hass: HomeAssistant, yaml_config: ConfigType) -> bool: # We misunderstood the startup signal. You're not allowed to change # anything during startup. Temp workaround. - # pylint: disable=protected-access + # pylint: disable-next=protected-access app._on_startup.freeze() await app.startup() diff --git a/homeassistant/components/emulated_roku/translations/lv.json b/homeassistant/components/emulated_roku/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/emulated_roku/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energie_vanons/__init__.py b/homeassistant/components/energie_vanons/__init__.py new file mode 100644 index 00000000000..b5cd5b7fcd3 --- /dev/null +++ b/homeassistant/components/energie_vanons/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Energie VanOns.""" diff --git a/homeassistant/components/energie_vanons/manifest.json b/homeassistant/components/energie_vanons/manifest.json new file mode 100644 index 00000000000..12d9a90d543 --- /dev/null +++ b/homeassistant/components/energie_vanons/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "energie_vanons", + "name": "Energie VanOns", + "integration_type": "virtual", + "supported_by": "energyzero" +} diff --git a/homeassistant/components/energy/data.py b/homeassistant/components/energy/data.py index 339c0c638e2..6f6b481b044 100644 --- a/homeassistant/components/energy/data.py +++ b/homeassistant/components/energy/data.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections import Counter from collections.abc import Awaitable, Callable -from typing import Literal, TypedDict, Union +from typing import Literal, TypedDict import voluptuous as vol @@ -120,9 +120,13 @@ class WaterSourceType(TypedDict): number_energy_price: float | None # Price for energy ($/m³) -SourceType = Union[ - GridSourceType, SolarSourceType, BatterySourceType, GasSourceType, WaterSourceType -] +SourceType = ( + GridSourceType + | SolarSourceType + | BatterySourceType + | GasSourceType + | WaterSourceType +) class DeviceConsumption(TypedDict): diff --git a/homeassistant/components/energy/sensor.py b/homeassistant/components/energy/sensor.py index 5ad4c74a6cf..6f16d2dc831 100644 --- a/homeassistant/components/energy/sensor.py +++ b/homeassistant/components/energy/sensor.py @@ -43,6 +43,7 @@ SUPPORTED_STATE_CLASSES = { VALID_ENERGY_UNITS: set[str] = { UnitOfEnergy.GIGA_JOULE, UnitOfEnergy.KILO_WATT_HOUR, + UnitOfEnergy.MEGA_JOULE, UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.WATT_HOUR, } diff --git a/homeassistant/components/energy/strings.json b/homeassistant/components/energy/strings.json index 6cdcd827633..611d36882ee 100644 --- a/homeassistant/components/energy/strings.json +++ b/homeassistant/components/energy/strings.json @@ -1,3 +1,61 @@ { - "title": "Energy" + "title": "Energy", + "issues": { + "entity_not_defined": { + "title": "Entity not defined", + "description": "Check the integration or your configuration that provides:" + }, + "recorder_untracked": { + "title": "Entity not tracked", + "description": "The recorder has been configured to exclude these configured entities:" + }, + "entity_unavailable": { + "title": "Entity unavailable", + "description": "The state of these configured entities are currently not available:" + }, + "entity_state_non_numeric": { + "title": "Entity has non-numeric state", + "description": "The following entities have a state that cannot be parsed as a number:" + }, + "entity_negative_state": { + "title": "Entity has a negative state", + "description": "The following entities have a negative state while a positive state is expected:" + }, + "entity_unexpected_unit_energy": { + "title": "Unexpected unit of measurement", + "description": "The following entities do not have an expected unit of measurement (either of {energy_units}):" + }, + "entity_unexpected_unit_gas": { + "title": "[%key:component::energy::issues::entity_unexpected_unit_energy::title%]", + "description": "The following entities do not have an expected unit of measurement (either of {energy_units} for an energy sensor or either of {gas_units} for a gas sensor):" + }, + "entity_unexpected_unit_water": { + "title": "[%key:component::energy::issues::entity_unexpected_unit_energy::title%]", + "description": "The following entities do not have the expected unit of measurement (either of {water_units}):" + }, + "entity_unexpected_unit_energy_price": { + "title": "[%key:component::energy::issues::entity_unexpected_unit_energy::title%]", + "description": "The following entities do not have an expected unit of measurement {price_units}:" + }, + "entity_unexpected_unit_gas_price": { + "title": "[%key:component::energy::issues::entity_unexpected_unit_energy_price::title%]", + "description": "[%key:component::energy::issues::entity_unexpected_unit_energy::description%]" + }, + "entity_unexpected_unit_water_price": { + "title": "[%key:component::energy::issues::entity_unexpected_unit_energy::title%]", + "description": "[%key:component::energy::issues::entity_unexpected_unit_energy::description%]" + }, + "entity_unexpected_state_class": { + "title": "Unexpected state class", + "description": "The following entities do not have the expected state class:" + }, + "entity_unexpected_device_class": { + "title": "Unexpected device class", + "description": "The following entities do not have the expected device class:" + }, + "entity_state_class_measurement_no_last_reset": { + "title": "Last reset missing", + "description": "The following entities have state class 'measurement' but 'last_reset' is missing:" + } + } } diff --git a/homeassistant/components/energy/translations/bg.json b/homeassistant/components/energy/translations/bg.json index cada66c2ac2..362c25efd61 100644 --- a/homeassistant/components/energy/translations/bg.json +++ b/homeassistant/components/energy/translations/bg.json @@ -1,3 +1,26 @@ { + "issues": { + "entity_not_defined": { + "title": "\u041e\u0431\u0435\u043a\u0442\u044a\u0442 \u043d\u0435 \u0435 \u0434\u0435\u0444\u0438\u043d\u0438\u0440\u0430\u043d" + }, + "entity_unexpected_unit_energy": { + "title": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u043c\u0435\u0440\u043d\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0430" + }, + "entity_unexpected_unit_energy_price": { + "title": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u043c\u0435\u0440\u043d\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0430" + }, + "entity_unexpected_unit_gas": { + "title": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u043c\u0435\u0440\u043d\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0430" + }, + "entity_unexpected_unit_gas_price": { + "title": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u043c\u0435\u0440\u043d\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0430" + }, + "entity_unexpected_unit_water": { + "title": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u043c\u0435\u0440\u043d\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0430" + }, + "entity_unexpected_unit_water_price": { + "title": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u043c\u0435\u0440\u043d\u0430 \u0435\u0434\u0438\u043d\u0438\u0446\u0430" + } + }, "title": "\u0415\u043d\u0435\u0440\u0433\u0438\u044f" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/ca.json b/homeassistant/components/energy/translations/ca.json index c8d85790fdd..86eb01be3ec 100644 --- a/homeassistant/components/energy/translations/ca.json +++ b/homeassistant/components/energy/translations/ca.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "Les entitats seg\u00fcents tenen un estat negatiu, nom\u00e9s s'esperen estats positius:", + "title": "L'entitat t\u00e9 un estat negatiu" + }, + "entity_not_defined": { + "description": "Comprova la integraci\u00f3 o la configuraci\u00f3 que proporciona:", + "title": "Entitat no definida" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "Les entitats seg\u00fcents tenen la clase d'estat 'measurement' per\u00f2 els falta 'last_reset':", + "title": "Falta 'last_reset'" + }, + "entity_state_non_numeric": { + "description": "Les entitats seg\u00fcents tenen un estat que no es pot formatar com a un n\u00famero:", + "title": "L'entitat no t\u00e9 un estat num\u00e8ric" + }, + "entity_unavailable": { + "description": "L'estat actual d'aquestes entitats configurades no est\u00e0 disponible:", + "title": "Entitat no disponible" + }, + "entity_unexpected_device_class": { + "description": "Les entitats seg\u00fcents no tenen la classe de dispositiu esperada:", + "title": "Classe de dispositiu inesperada" + }, + "entity_unexpected_state_class": { + "description": "Les entitats seg\u00fcents no tenen la classe d'estat esperada:", + "title": "Classe d'estat inesperada" + }, + "entity_unexpected_unit_energy": { + "description": "Les entitats seg\u00fcents no tenen la unitat de mesura esperada ({energy_units}):", + "title": "Unitat de mesura inesperada" + }, + "entity_unexpected_unit_energy_price": { + "description": "Les entitats seg\u00fcents no tenen la unitat de mesura esperada {price_units}:", + "title": "Unitat de mesura inesperada" + }, + "entity_unexpected_unit_gas": { + "description": "Les entitats seg\u00fcents no tenen la unitat de mesura esperada ({energy_units} pels sensors d'energia o {gas_units} pels sensors de gas):", + "title": "Unitat de mesura inesperada" + }, + "entity_unexpected_unit_gas_price": { + "description": "Les entitats seg\u00fcents no tenen la unitat de mesura esperada ({energy_units}):", + "title": "Unitat de mesura inesperada" + }, + "entity_unexpected_unit_water": { + "description": "Les entitats seg\u00fcents no tenen la unitat de mesura esperada ({water_units}):", + "title": "Unitat de mesura inesperada" + }, + "entity_unexpected_unit_water_price": { + "description": "Les entitats seg\u00fcents no tenen la unitat de mesura esperada ({energy_units}):", + "title": "Unitat de mesura inesperada" + }, + "recorder_untracked": { + "description": "El 'recorder' s'ha configurat per excloure les seg\u00fcents entitats:", + "title": "Entitat sense seguiment" + } + }, "title": "Energia" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/de.json b/homeassistant/components/energy/translations/de.json index 53457a69447..85f91f44c69 100644 --- a/homeassistant/components/energy/translations/de.json +++ b/homeassistant/components/energy/translations/de.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "Die folgenden Entit\u00e4ten haben einen negativen Zustand, w\u00e4hrend ein positiver Zustand erwartet wird:", + "title": "Entit\u00e4t hat einen negativen Zustand" + }, + "entity_not_defined": { + "description": "\u00dcberpr\u00fcfe die Integration oder Konfiguration, die Folgendes bereitstellt:", + "title": "Entit\u00e4t nicht definiert" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "Die folgenden Entit\u00e4ten haben die Zustandsklasse \"measurement\", aber \"last_reset\" fehlt:", + "title": "Letzter Reset fehlt" + }, + "entity_state_non_numeric": { + "description": "Die folgenden Entit\u00e4ten haben einen Zustand, der nicht als Zahl geparst werden kann:", + "title": "Entit\u00e4t hat einen nicht-numerischen Status" + }, + "entity_unavailable": { + "description": "Der Status dieser konfigurierten Entit\u00e4ten ist derzeit nicht verf\u00fcgbar:", + "title": "Entit\u00e4t nicht verf\u00fcgbar" + }, + "entity_unexpected_device_class": { + "description": "Die folgenden Entit\u00e4ten haben nicht die erwartete Ger\u00e4teklasse:", + "title": "Unerwartete Ger\u00e4teklasse" + }, + "entity_unexpected_state_class": { + "description": "Die folgenden Entit\u00e4ten haben nicht die erwartete Zustandsklasse:", + "title": "Unerwartete Zustandsklasse" + }, + "entity_unexpected_unit_energy": { + "description": "Die folgenden Entit\u00e4ten haben keine erwartete Ma\u00dfeinheit (eine von {energy_units}):", + "title": "Unerwartete Ma\u00dfeinheit" + }, + "entity_unexpected_unit_energy_price": { + "description": "Die folgenden Entit\u00e4ten haben keine erwartete Ma\u00dfeinheit {price_units}:", + "title": "Unerwartete Ma\u00dfeinheit" + }, + "entity_unexpected_unit_gas": { + "description": "Die folgenden Entit\u00e4ten haben keine erwartete Ma\u00dfeinheit (entweder {energy_units} f\u00fcr einen Energiesensor oder {gas_units} f\u00fcr einen Gassensor):", + "title": "Unerwartete Ma\u00dfeinheit" + }, + "entity_unexpected_unit_gas_price": { + "description": "Die folgenden Entit\u00e4ten haben keine erwartete Ma\u00dfeinheit (eine von {energy_units}):", + "title": "Unerwartete Ma\u00dfeinheit" + }, + "entity_unexpected_unit_water": { + "description": "Die folgenden Entit\u00e4ten haben nicht die erwartete Ma\u00dfeinheit (eine von {water_units}):", + "title": "Unerwartete Ma\u00dfeinheit" + }, + "entity_unexpected_unit_water_price": { + "description": "Die folgenden Entit\u00e4ten haben keine erwartete Ma\u00dfeinheit (eine von {energy_units}):", + "title": "Unerwartete Ma\u00dfeinheit" + }, + "recorder_untracked": { + "description": "Der Rekorder wurde so konfiguriert, dass er diese konfigurierten Entit\u00e4ten ausschlie\u00dft:", + "title": "Entit\u00e4t nicht nachverfolgt" + } + }, "title": "Energie" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/el.json b/homeassistant/components/energy/translations/el.json index cdc7b83c2ee..5e39c2a5f53 100644 --- a/homeassistant/components/energy/translations/el.json +++ b/homeassistant/components/energy/translations/el.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03c1\u03bd\u03b7\u03c4\u03b9\u03ba\u03ae \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b5\u03bd\u03ce \u03b1\u03bd\u03b1\u03bc\u03ad\u03bd\u03b5\u03c4\u03b1\u03b9 \u03b8\u03b5\u03c4\u03b9\u03ba\u03ae \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7:", + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03b1\u03c1\u03bd\u03b7\u03c4\u03b9\u03ba\u03ae \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7" + }, + "entity_not_defined": { + "description": "\u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03ae \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03b9:", + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03ba\u03bb\u03ac\u03c3\u03b7 \"\u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\", \u03b1\u03bb\u03bb\u03ac \u03bb\u03b5\u03af\u03c0\u03b5\u03b9 \u03b7 \"\u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1_\u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac\":", + "title": "\u039b\u03b5\u03af\u03c0\u03b5\u03b9 \u03b7 \u03c4\u03b5\u03bb\u03b5\u03c5\u03c4\u03b1\u03af\u03b1 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac" + }, + "entity_state_non_numeric": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03ad\u03c7\u03bf\u03c5\u03bd \u03bc\u03b9\u03b1 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c0\u03bf\u03c5 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03bd\u03b1\u03bb\u03c5\u03b8\u03b5\u03af \u03c9\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2:", + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03bc\u03b7 \u03b1\u03c1\u03b9\u03b8\u03bc\u03b7\u03c4\u03b9\u03ba\u03ae \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7" + }, + "entity_unavailable": { + "description": "\u0397 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b1\u03c5\u03c4\u03ce\u03bd \u03c4\u03c9\u03bd \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03c9\u03bd \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c2 \u03c4\u03bf \u03c0\u03b1\u03c1\u03cc\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7:", + "title": "\u039f\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03bc\u03b7 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7" + }, + "entity_unexpected_device_class": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03ba\u03bb\u03ac\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ce\u03bd:", + "title": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03ba\u03bb\u03ac\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" + }, + "entity_unexpected_state_class": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03ba\u03bb\u03ac\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2:", + "title": "\u0391\u03c0\u03c1\u03bf\u03c3\u03b4\u03cc\u03ba\u03b7\u03c4\u03b7 \u03ba\u03bb\u03ac\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2" + }, + "entity_unexpected_unit_energy": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 (\u03b5\u03af\u03c4\u03b5 \u03b1\u03c0\u03cc {energy_units}):", + "title": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2" + }, + "entity_unexpected_unit_energy_price": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 {price_units}:", + "title": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2" + }, + "entity_unexpected_unit_gas": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 (\u03b5\u03af\u03c4\u03b5 {energy_units} \u03b3\u03b9\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1\u03c2 \u03b5\u03af\u03c4\u03b5 {gas_units} \u03b3\u03b9\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b1\u03b5\u03c1\u03af\u03bf\u03c5:)", + "title": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2" + }, + "entity_unexpected_unit_gas_price": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 (\u03b5\u03af\u03c4\u03b5 \u03b1\u03c0\u03cc {energy_units}):", + "title": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2" + }, + "entity_unexpected_unit_water": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 (\u03b5\u03af\u03c4\u03b5 \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 {water_units}):", + "title": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2" + }, + "entity_unexpected_unit_water_price": { + "description": "\u039f\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2 (\u03b5\u03af\u03c4\u03b5 \u03b1\u03c0\u03cc {energy_units}):", + "title": "\u039c\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7\u03c2" + }, + "recorder_untracked": { + "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03be\u03b1\u03b9\u03c1\u03b5\u03af \u03b1\u03c5\u03c4\u03ad\u03c2 \u03c4\u03b9\u03c2 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2:", + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b4\u03b5\u03bd \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af\u03c4\u03b1\u03b9" + } + }, "title": "\u0395\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/en.json b/homeassistant/components/energy/translations/en.json index 109e1bd5af8..92e9e83aa20 100644 --- a/homeassistant/components/energy/translations/en.json +++ b/homeassistant/components/energy/translations/en.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "The following entities have a negative state while a positive state is expected:", + "title": "Entity has a negative state" + }, + "entity_not_defined": { + "description": "Check the integration or your configuration that provides:", + "title": "Entity not defined" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "The following entities have state class 'measurement' but 'last_reset' is missing:", + "title": "Last reset missing" + }, + "entity_state_non_numeric": { + "description": "The following entities have a state that cannot be parsed as a number:", + "title": "Entity has non-numeric state" + }, + "entity_unavailable": { + "description": "The state of these configured entities are currently not available:", + "title": "Entity unavailable" + }, + "entity_unexpected_device_class": { + "description": "The following entities do not have the expected device class:", + "title": "Unexpected device class" + }, + "entity_unexpected_state_class": { + "description": "The following entities do not have the expected state class:", + "title": "Unexpected state class" + }, + "entity_unexpected_unit_energy": { + "description": "The following entities do not have an expected unit of measurement (either of {energy_units}):", + "title": "Unexpected unit of measurement" + }, + "entity_unexpected_unit_energy_price": { + "description": "The following entities do not have an expected unit of measurement {price_units}:", + "title": "Unexpected unit of measurement" + }, + "entity_unexpected_unit_gas": { + "description": "The following entities do not have an expected unit of measurement (either of {energy_units} for an energy sensor or either of {gas_units} for a gas sensor):", + "title": "Unexpected unit of measurement" + }, + "entity_unexpected_unit_gas_price": { + "description": "The following entities do not have an expected unit of measurement (either of {energy_units}):", + "title": "Unexpected unit of measurement" + }, + "entity_unexpected_unit_water": { + "description": "The following entities do not have the expected unit of measurement (either of {water_units}):", + "title": "Unexpected unit of measurement" + }, + "entity_unexpected_unit_water_price": { + "description": "The following entities do not have an expected unit of measurement (either of {energy_units}):", + "title": "Unexpected unit of measurement" + }, + "recorder_untracked": { + "description": "The recorder has been configured to exclude these configured entities:", + "title": "Entity not tracked" + } + }, "title": "Energy" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/es.json b/homeassistant/components/energy/translations/es.json index 64c2f5bffa1..cdf35af1a66 100644 --- a/homeassistant/components/energy/translations/es.json +++ b/homeassistant/components/energy/translations/es.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "Las siguientes entidades tienen un estado negativo mientras se espera un estado positivo:", + "title": "La entidad tiene un estado negativo" + }, + "entity_not_defined": { + "description": "Comprueba la integraci\u00f3n o la configuraci\u00f3n que proporciona:", + "title": "Entidad no definida" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "Las siguientes entidades tienen la clase de estado 'measurement' pero falta 'last_reset':", + "title": "Falta el \u00faltimo restablecimiento" + }, + "entity_state_non_numeric": { + "description": "Las siguientes entidades tienen un estado que no puede ser analizado como un n\u00famero:", + "title": "La entidad tiene un estado no num\u00e9rico" + }, + "entity_unavailable": { + "description": "El estado de estas entidades configuradas no est\u00e1 disponible actualmente:", + "title": "Entidad no disponible" + }, + "entity_unexpected_device_class": { + "description": "Las siguientes entidades no tienen la clase de dispositivo esperada:", + "title": "Clase de dispositivo inesperada" + }, + "entity_unexpected_state_class": { + "description": "Las siguientes entidades no tienen la clase de estado esperada:", + "title": "Clase de estado inesperada" + }, + "entity_unexpected_unit_energy": { + "description": "Las siguientes entidades no tienen una unidad de medida esperada (ninguna de {energy_units}):", + "title": "Unidad de medida inesperada" + }, + "entity_unexpected_unit_energy_price": { + "description": "Las siguientes entidades no tienen una unidad de medida {price_units}:", + "title": "Unidad de medida inesperada" + }, + "entity_unexpected_unit_gas": { + "description": "Las siguientes entidades no tienen una unidad de medida esperada (ya sea de {energy_units} para un sensor de energ\u00eda o de {gas_units} para un sensor de gas):", + "title": "Unidad de medida inesperada" + }, + "entity_unexpected_unit_gas_price": { + "description": "Las siguientes entidades no tienen una unidad de medida esperada (ninguna de {energy_units}):", + "title": "Unidad de medida inesperada" + }, + "entity_unexpected_unit_water": { + "description": "Las siguientes entidades no tienen la unidad de medida esperada (ninguna de {water_units}):", + "title": "Unidad de medida inesperada" + }, + "entity_unexpected_unit_water_price": { + "description": "Las siguientes entidades no tienen una unidad de medida esperada (ninguna de {energy_units}):", + "title": "Unidad de medida inesperada" + }, + "recorder_untracked": { + "description": "La grabadora se ha configurado para excluir estas entidades configuradas:", + "title": "Entidad sin seguimiento" + } + }, "title": "Energ\u00eda" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/et.json b/homeassistant/components/energy/translations/et.json index c8d85790fdd..816a000ebe8 100644 --- a/homeassistant/components/energy/translations/et.json +++ b/homeassistant/components/energy/translations/et.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "J\u00e4rgmistel olemitel on negatiivne olek, samas kui eeldatakse positiivset olekut:", + "title": "Olemil on negatiivne olek" + }, + "entity_not_defined": { + "description": "Kontrolli sidumisi v\u00f5i oma konfiguratsiooni, mis pakub:", + "title": "Olem on m\u00e4\u00e4ratlemata" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "J\u00e4rgmistel olemitel on olekuklass 'measurement' kuid puudub 'last_reset':", + "title": "Olekuklass 'last_reset' puudub" + }, + "entity_state_non_numeric": { + "description": "J\u00e4rgnevatel \u00fcksustel on olek, mida ei saa kasutada numbrina:", + "title": "Olemil on mittenumbriline olek" + }, + "entity_unavailable": { + "description": "Nende konfigureeritud \u00fcksuste olekud ei ole praegu k\u00e4ttesaadavad:", + "title": "Olem pole saadaval" + }, + "entity_unexpected_device_class": { + "description": "J\u00e4rgmistel olemitel puudub oodatud seadmeklass:", + "title": "Ootamatu seadmeklass" + }, + "entity_unexpected_state_class": { + "description": "J\u00e4rgmistel olemitel puudub oodatud olekuklass:", + "title": "Ootamatu olekuklass" + }, + "entity_unexpected_unit_energy": { + "description": "J\u00e4rgmistel \u00fcksustel puudub eeldatav m\u00f5\u00f5t\u00fchik (\u00fcksk\u00f5ik milline {energy_units} ):", + "title": "Tundmatu m\u00f5\u00f5t\u00fchik" + }, + "entity_unexpected_unit_energy_price": { + "description": "J\u00e4rgmistel \u00fcksustel ei ole eeldatavat m\u00f5\u00f5t\u00fchikut {price_units} :", + "title": "Tundmatu m\u00f5\u00f5t\u00fchik" + }, + "entity_unexpected_unit_gas": { + "description": "J\u00e4rgmistel \u00fcksustel pole eeldatavat m\u00f5\u00f5t\u00fchikut (energiaanduri puhul {energy_units} v\u00f5i gaasianduri puhul {gas_units} ):", + "title": "Tundmatu m\u00f5\u00f5t\u00fchik" + }, + "entity_unexpected_unit_gas_price": { + "description": "J\u00e4rgmistel \u00fcksustel puudub eeldatav m\u00f5\u00f5t\u00fchik (\u00fcksk\u00f5ik milline {energy_units}):", + "title": "Tundmatu m\u00f5\u00f5t\u00fchik" + }, + "entity_unexpected_unit_water": { + "description": "J\u00e4rgmistel \u00fcksustel puudub eeldatav m\u00f5\u00f5t\u00fchik (\u00fcksk\u00f5ik milline {water_units} ):", + "title": "Tundmatu m\u00f5\u00f5t\u00fchik" + }, + "entity_unexpected_unit_water_price": { + "description": "J\u00e4rgmistel \u00fcksustel puudub eeldatav m\u00f5\u00f5t\u00fchik (\u00fcksk\u00f5ik milline {energy_units}):", + "title": "Tundmatu m\u00f5\u00f5t\u00fchik" + }, + "recorder_untracked": { + "description": "Salvesti on konfigureeritud v\u00e4listama j\u00e4rgmisi konfigureeritud olemeid:", + "title": "Olemit ei j\u00e4lgita" + } + }, "title": "Energia" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/he.json b/homeassistant/components/energy/translations/he.json index 3c61aad6089..2a7e63b244b 100644 --- a/homeassistant/components/energy/translations/he.json +++ b/homeassistant/components/energy/translations/he.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d9\u05e9 \u05de\u05e6\u05d1 \u05e9\u05dc\u05d9\u05dc\u05d9 \u05d1\u05e2\u05d5\u05d3 \u05e9\u05e6\u05e4\u05d5\u05d9 \u05de\u05e6\u05d1 \u05d7\u05d9\u05d5\u05d1\u05d9:", + "title": "\u05dc\u05d9\u05e9\u05d5\u05ea \u05d9\u05e9 \u05de\u05e6\u05d1 \u05e9\u05dc\u05d9\u05dc\u05d9" + }, + "entity_not_defined": { + "description": "\u05d1\u05d3\u05d9\u05e7\u05ea \u05d4\u05e9\u05d9\u05dc\u05d5\u05d1 \u05d0\u05d5 \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05e9\u05dc\u05da \u05d4\u05de\u05e1\u05d5\u05e4\u05e7\u05ea:", + "title": "\u05d9\u05e9\u05d5\u05ea \u05dc\u05d0 \u05de\u05d5\u05d2\u05d3\u05e8\u05ea" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d9\u05e9 \u05de\u05e6\u05d1 \u05de\u05e1\u05d5\u05d2 'measurement' \u05d0\u05da 'last_reset' \u05d7\u05e1\u05e8 \u05e2\u05d1\u05d5\u05e8\u05df:", + "title": "\u05d0\u05d9\u05e4\u05d5\u05e1 \u05d0\u05d7\u05e8\u05d5\u05df \u05d7\u05e1\u05e8" + }, + "entity_state_non_numeric": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d9\u05e9 \u05de\u05e6\u05d1 \u05e9\u05dc\u05d0 \u05e0\u05d9\u05ea\u05df \u05dc\u05e0\u05ea\u05d7 \u05db\u05de\u05e1\u05e4\u05e8:", + "title": "\u05dc\u05d9\u05e9\u05d5\u05ea \u05d9\u05e9 \u05de\u05e6\u05d1 \u05dc\u05d0 \u05de\u05e1\u05e4\u05e8\u05d9" + }, + "entity_unavailable": { + "description": "\u05d4\u05de\u05e6\u05d1 \u05e9\u05dc \u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05e9\u05ea\u05e6\u05d5\u05e8\u05ea\u05df \u05e0\u05e7\u05d1\u05e2\u05d4 \u05d0\u05d9\u05e0\u05d5 \u05d6\u05de\u05d9\u05df \u05db\u05e2\u05ea:", + "title": "\u05d4\u05d9\u05e9\u05d5\u05ea \u05d0\u05d9\u05e0\u05d4 \u05d6\u05de\u05d9\u05e0\u05d4" + }, + "entity_unexpected_device_class": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d0\u05d9\u05df \u05d0\u05ea \u05de\u05d7\u05dc\u05e7\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05d4\u05e6\u05e4\u05d5\u05d9\u05d4:", + "title": "\u05de\u05d7\u05dc\u05e7\u05ea \u05d4\u05ea\u05e7\u05df \u05dc\u05d0 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "entity_unexpected_state_class": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d0\u05d9\u05df \u05d0\u05ea \u05de\u05d7\u05dc\u05e7\u05ea \u05d4\u05de\u05e6\u05d1 \u05d4\u05e6\u05e4\u05d5\u05d9\u05d4:", + "title": "\u05de\u05d7\u05dc\u05e7\u05ea \u05de\u05e6\u05d1 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "entity_unexpected_unit_energy": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d0\u05d9\u05df \u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05e6\u05e4\u05d5\u05d9\u05d4 (\u05d0\u05d7\u05ea \u05de\u05ea\u05d5\u05da {energy_units}):", + "title": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "entity_unexpected_unit_energy_price": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d0\u05d9\u05df \u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05e6\u05e4\u05d5\u05d9\u05d4 {price_units}:", + "title": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "entity_unexpected_unit_gas": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d0\u05d9\u05df \u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d3\u05d9\u05d3\u05d4 \u05e6\u05e4\u05d5\u05d9\u05d4 (\u05d0\u05d7\u05ea \u05de\u05ea\u05d5\u05da {energy_units} \u05e2\u05d1\u05d5\u05e8 \u05d7\u05d9\u05d9\u05e9\u05df \u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05d0\u05d5 \u05d0\u05d7\u05ea \u05de\u05ea\u05d5\u05da {gas_units} \u05e2\u05d1\u05d5\u05e8 \u05d7\u05d9\u05d9\u05e9\u05df \u05d2\u05d6):", + "title": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "entity_unexpected_unit_gas_price": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d0\u05d9\u05df \u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05e6\u05e4\u05d5\u05d9\u05d4 (\u05d0\u05d7\u05ea \u05de\u05ea\u05d5\u05da {energy_units}):", + "title": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "entity_unexpected_unit_water": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d0\u05d9\u05df \u05d0\u05ea \u05d9\u05d7\u05d9\u05d3\u05ea \u05d4\u05de\u05d3\u05d9\u05d3\u05d4 \u05d4\u05e6\u05e4\u05d5\u05d9\u05d4 (\u05d0\u05d7\u05ea \u05de\u05ea\u05d5\u05da {water_units}):", + "title": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "entity_unexpected_unit_water_price": { + "description": "\u05dc\u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05d4\u05d1\u05d0\u05d5\u05ea \u05d0\u05d9\u05df \u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05e6\u05e4\u05d5\u05d9\u05d4 (\u05d0\u05d7\u05ea \u05de\u05ea\u05d5\u05da {energy_units}):", + "title": "\u05d9\u05d7\u05d9\u05d3\u05ea \u05de\u05d9\u05d3\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "recorder_untracked": { + "description": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05de\u05e7\u05dc\u05d9\u05d8 \u05e0\u05e7\u05d1\u05e2\u05d4 \u05dc\u05dc\u05d0 \u05d4\u05db\u05dc\u05dc\u05ea \u05d9\u05e9\u05d5\u05d9\u05d5\u05ea \u05e9\u05ea\u05e6\u05d5\u05e8\u05ea\u05df \u05e0\u05e7\u05d1\u05e2\u05d4:", + "title": "\u05d9\u05e9\u05d5\u05ea \u05e9\u05dc\u05d0 \u05de\u05d1\u05e6\u05e2\u05ea \u05de\u05e2\u05e7\u05d1" + } + }, "title": "\u05d0\u05e0\u05e8\u05d2\u05d9\u05d4" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/hu.json b/homeassistant/components/energy/translations/hu.json index c8d85790fdd..ab28ebf3d27 100644 --- a/homeassistant/components/energy/translations/hu.json +++ b/homeassistant/components/energy/translations/hu.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1sok negat\u00edv \u00e1llapot\u00faak, m\u00edg pozit\u00edv az elv\u00e1rt:", + "title": "Az entit\u00e1snak negat\u00edv \u00e1llapota van" + }, + "entity_not_defined": { + "description": "Ellen\u0151rizze az integr\u00e1ci\u00f3t vagy a konfigur\u00e1ci\u00f3t, amely a k\u00f6vetkez\u0151t adja:", + "title": "Az entit\u00e1s nincs meghat\u00e1rozva" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1sok 'measurement' (m\u00e9rt\u00e9kegys\u00e9g) \u00e1llapotoszt\u00e1llyal rendelkeznek ugyan, de hi\u00e1nyzik nekik a 'last_reset' (utols\u00f3_null\u00e1z\u00e1s) id\u0151pontja:", + "title": "Utols\u00f3 null\u00e1z\u00e1s id\u0151pontja hi\u00e1nyzik" + }, + "entity_state_non_numeric": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1sok \u00e1llapota nem \u00e9rtelmezhet\u0151 sz\u00e1mk\u00e9nt:", + "title": "Az entit\u00e1s nem numerikus \u00e1llapot\u00fa" + }, + "entity_unavailable": { + "description": "Ezen konfigur\u00e1lt entit\u00e1sok \u00e1llapota jelenleg nem \u00e9rhet\u0151 el:", + "title": "Entit\u00e1s nem \u00e9rhet\u0151 el" + }, + "entity_unexpected_device_class": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1sok nem megfelel\u0151 eszk\u00f6zoszt\u00e1lyban (device class) vannak:", + "title": "Nem az elv\u00e1rt eszk\u00f6zoszt\u00e1ly" + }, + "entity_unexpected_state_class": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1sok nem rendelkeznek az elv\u00e1rt \u00e1llapotoszt\u00e1llyal (state class):", + "title": "Nem az elv\u00e1rt \u00e1llapotoszt\u00e1ly" + }, + "entity_unexpected_unit_energy": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1soknak nem elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9ge van (nem {energy_units}):", + "title": "Nem az elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9g" + }, + "entity_unexpected_unit_energy_price": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1soknak nem elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9ge van ({price_units}):", + "title": "Nem az elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9g" + }, + "entity_unexpected_unit_gas": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1soknak nem elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9ge van (energia\u00e9rz\u00e9kel\u0151 eset\u00e9n az {energy_units} vagy g\u00e1z\u00e9rz\u00e9kel\u0151 eset\u00e9n a {gas_units} valamelyike):", + "title": "Nem az elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9g" + }, + "entity_unexpected_unit_gas_price": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1soknak nem elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9ge van (nem {energy_units}):", + "title": "Nem az elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9g" + }, + "entity_unexpected_unit_water": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1soknak nem elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9ge van (nem {water_units}):", + "title": "Nem az elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9g" + }, + "entity_unexpected_unit_water_price": { + "description": "A k\u00f6vetkez\u0151 entit\u00e1soknak nem elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9ge van (nem {energy_units}):", + "title": "Nem az elv\u00e1rt m\u00e9rt\u00e9kegys\u00e9g" + }, + "recorder_untracked": { + "description": "A napl\u00f3z\u00f3 (recorder) \u00fagy van konfigur\u00e1lva, hogy kiz\u00e1rja az al\u00e1bbi a konfigur\u00e1lt entit\u00e1sokat:", + "title": "Az entit\u00e1s nincs napl\u00f3zva" + } + }, "title": "Energia" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/id.json b/homeassistant/components/energy/translations/id.json index 168ae4ae877..f76959335ce 100644 --- a/homeassistant/components/energy/translations/id.json +++ b/homeassistant/components/energy/translations/id.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "Entitas berikut memiliki status negatif sementara status positif diharapkan:", + "title": "Entitas memiliki status negatif" + }, + "entity_not_defined": { + "description": "Periksa integrasi atau konfigurasi Anda yang menyediakan:", + "title": "Entitas tidak didefinisikan" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "Entitas berikut memiliki kelas status 'measurement' tetapi 'last_reset' tidak ada:", + "title": "Pengaturan ulang terakhir tidak ada" + }, + "entity_state_non_numeric": { + "description": "Entitas berikut memiliki status yang tidak dapat diuraikan sebagai bilangan:", + "title": "Entitas memiliki status nonnumerik" + }, + "entity_unavailable": { + "description": "Status entitas yang dikonfigurasi ini saat ini tidak tersedia:", + "title": "Entitas tidak tersedia" + }, + "entity_unexpected_device_class": { + "description": "Entitas berikut tidak memiliki kelas perangkat yang diharapkan:", + "title": "Kelas perangkat yang tidak diharapkan" + }, + "entity_unexpected_state_class": { + "description": "Entitas berikut tidak memiliki kelas status yang diharapkan:", + "title": "Kelas status yang tidak diharapkan" + }, + "entity_unexpected_unit_energy": { + "description": "Entitas berikut tidak memiliki unit pengukuran yang diharapkan (salah satu dari {energy_units}):", + "title": "Satuan pengukuran yang tidak diharapkan" + }, + "entity_unexpected_unit_energy_price": { + "description": "Entitas berikut tidak memiliki unit pengukuran yang diharapkan (salah satu dari {price_units}):", + "title": "Satuan pengukuran yang tidak diharapkan" + }, + "entity_unexpected_unit_gas": { + "description": "Entitas berikut tidak memiliki satuan pengukuran yang diharapkan (salah satu dari {energy_units}) untuk sensor energi atau salah satu dari {gas_units} untuk sensor gas):", + "title": "Satuan pengukuran yang tidak diharapkan" + }, + "entity_unexpected_unit_gas_price": { + "description": "Entitas berikut tidak memiliki unit pengukuran yang diharapkan (salah satu dari {energy_units}):", + "title": "Satuan pengukuran yang tidak diharapkan" + }, + "entity_unexpected_unit_water": { + "description": "Entitas berikut tidak memiliki unit pengukuran yang diharapkan (salah satu dari {water_units}):", + "title": "Satuan pengukuran yang tidak diharapkan" + }, + "entity_unexpected_unit_water_price": { + "description": "Entitas berikut tidak memiliki unit pengukuran yang diharapkan (salah satu dari {energy_units}):", + "title": "Satuan pengukuran yang tidak diharapkan" + }, + "recorder_untracked": { + "description": "Perekam telah dikonfigurasi untuk mengecualikan entitas yang dikonfigurasi ini:", + "title": "Entitas tidak dilacak" + } + }, "title": "Energi" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/it.json b/homeassistant/components/energy/translations/it.json index c8d85790fdd..90b0078757a 100644 --- a/homeassistant/components/energy/translations/it.json +++ b/homeassistant/components/energy/translations/it.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "Le seguenti entit\u00e0 hanno uno stato negativo mentre \u00e8 previsto uno stato positivo:", + "title": "L'entit\u00e0 ha uno stato negativo" + }, + "entity_not_defined": { + "description": "Controlla l'integrazione o la tua configurazione che fornisce:", + "title": "Entit\u00e0 non definita" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "Le seguenti entit\u00e0 hanno la classe di stato 'measurement' ma manca 'last_reset':", + "title": "Ultimo ripristino mancante" + }, + "entity_state_non_numeric": { + "description": "Le seguenti entit\u00e0 hanno uno stato che non pu\u00f2 essere analizzato come numero:", + "title": "L'entit\u00e0 ha uno stato non numerico" + }, + "entity_unavailable": { + "description": "Lo stato di queste entit\u00e0 configurate non \u00e8 attualmente disponibile:", + "title": "Entit\u00e0 non disponibile" + }, + "entity_unexpected_device_class": { + "description": "Le seguenti entit\u00e0 non hanno la classe di dispositivo prevista:", + "title": "Classe di dispositivo inattesa" + }, + "entity_unexpected_state_class": { + "description": "Le seguenti entit\u00e0 non hanno la classe di stato prevista:", + "title": "Classe di stato inattesa" + }, + "entity_unexpected_unit_energy": { + "description": "Le seguenti entit\u00e0 non hanno un'unit\u00e0 di misura prevista (una tra {energy_units}):", + "title": "Unit\u00e0 di misura inaspettata" + }, + "entity_unexpected_unit_energy_price": { + "description": "Le seguenti entit\u00e0 non hanno un'unit\u00e0 di misura prevista in {price_units}:", + "title": "Unit\u00e0 di misura inaspettata" + }, + "entity_unexpected_unit_gas": { + "description": "Le seguenti entit\u00e0 non hanno un'unit\u00e0 di misura prevista (di {energy_units} per un sensore di energia o di {gas_units} per un sensore di gas):", + "title": "Unit\u00e0 di misura inaspettata" + }, + "entity_unexpected_unit_gas_price": { + "description": "Le seguenti entit\u00e0 non hanno un'unit\u00e0 di misura prevista (una tra {energy_units}):", + "title": "Unit\u00e0 di misura inaspettata" + }, + "entity_unexpected_unit_water": { + "description": "Le seguenti entit\u00e0 non hanno l'unit\u00e0 di misura prevista (una delle due {water_units}):", + "title": "Unit\u00e0 di misura inaspettata" + }, + "entity_unexpected_unit_water_price": { + "description": "Le seguenti entit\u00e0 non hanno un'unit\u00e0 di misura prevista (una tra {energy_units}):", + "title": "Unit\u00e0 di misura inaspettata" + }, + "recorder_untracked": { + "description": "Il registratore \u00e8 stato configurato per escludere queste entit\u00e0 configurate:", + "title": "Entit\u00e0 non tracciata" + } + }, "title": "Energia" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/lv.json b/homeassistant/components/energy/translations/lv.json new file mode 100644 index 00000000000..8b61c739b28 --- /dev/null +++ b/homeassistant/components/energy/translations/lv.json @@ -0,0 +1,16 @@ +{ + "issues": { + "entity_unexpected_unit_energy": { + "title": "M\u0113rvien\u012bba at\u0161\u0137iras no sagaid\u0101m\u0101s" + }, + "entity_unexpected_unit_energy_price": { + "title": "M\u0113rvien\u012bba at\u0161\u0137iras no sagaid\u0101m\u0101s" + }, + "entity_unexpected_unit_gas_price": { + "title": "M\u0113rvien\u012bba at\u0161\u0137iras no sagaid\u0101m\u0101s" + }, + "recorder_untracked": { + "title": "Vien\u012bba nav izsekota" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/nl.json b/homeassistant/components/energy/translations/nl.json index 53457a69447..f9a82257563 100644 --- a/homeassistant/components/energy/translations/nl.json +++ b/homeassistant/components/energy/translations/nl.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "De volgende entiteiten hebben een negatieve status terwijl een positieve status wordt verwacht:", + "title": "Entiteit heeft een negatieve status" + }, + "entity_not_defined": { + "description": "Controleer de integratie van je configuratie die hier in voorziet:", + "title": "Entiteit niet gedefinieerd" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "De volgende entiteiten hebben een statusklasse (state class) 'measurement' maar 'last_reset' mist:", + "title": "Laatste reset ontbreekt" + }, + "entity_state_non_numeric": { + "description": "De volgende entiteiten hebben een status die niet kan worden gelezen als een getal:", + "title": "Entiteit heeft een niet-numerieke status" + }, + "entity_unavailable": { + "description": "De status van deze geconfigureerde entiteiten zijn momenteel niet beschikbaar:", + "title": "Entiteit niet beschikbaar" + }, + "entity_unexpected_device_class": { + "description": "De volgende entiteiten hebben niet de verwachte apparaatklasse (device class):", + "title": "Onverwachte apparaatklasse" + }, + "entity_unexpected_state_class": { + "description": "De volgende entiteiten hebben niet de verwachte statusklasse (state class):", + "title": "Onverwachte statusklasse (state class)" + }, + "entity_unexpected_unit_energy": { + "description": "De volgende entiteiten hebben niet de verwachte meeteenheid (\u00e9\u00e9n van {energy_units}):", + "title": "Onverwachte meeteenheid" + }, + "entity_unexpected_unit_energy_price": { + "description": "De volgende entiteiten hebben niet de verwachte meeteenheid {price_units}:", + "title": "Onverwachte meeteenheid" + }, + "entity_unexpected_unit_gas": { + "description": "De volgende entiteiten hebben niet de verwachte meeteenheid ({enery_units} voor een energiesensor of {gas_units} voor een gassensor):", + "title": "Onverwachte meeteenheid" + }, + "entity_unexpected_unit_gas_price": { + "description": "De volgende entiteiten hebben niet de verwachte meeteenheid (\u00e9\u00e9n van {energy_units}):", + "title": "Onverwachte meeteenheid" + }, + "entity_unexpected_unit_water": { + "description": "De volgende entiteiten hebben niet de verwachte meeteenheid (\u00e9\u00e9n van {water_units}):", + "title": "Onverwachte meeteenheid" + }, + "entity_unexpected_unit_water_price": { + "description": "De volgende entiteiten hebben niet de verwachte meeteenheid (\u00e9\u00e9n van {energy_units}):", + "title": "Onverwachte meeteenheid" + }, + "recorder_untracked": { + "description": "De recorder is ingesteld om onderstaande entiteiten uit te sluiten:", + "title": "Entiteit wordt niet bijgehouden" + } + }, "title": "Energie" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/no.json b/homeassistant/components/energy/translations/no.json index 168ae4ae877..2471ecc57be 100644 --- a/homeassistant/components/energy/translations/no.json +++ b/homeassistant/components/energy/translations/no.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "F\u00f8lgende enheter har en negativ tilstand mens en positiv tilstand forventes:", + "title": "Enheten har en negativ tilstand" + }, + "entity_not_defined": { + "description": "Sjekk integrasjonen eller konfigurasjonen som gir:", + "title": "Entitet ikke definert" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "F\u00f8lgende enheter har tilstandsklasse 'm\u00e5ling', men 'last_reset' mangler:", + "title": "Siste tilbakestilling mangler" + }, + "entity_state_non_numeric": { + "description": "F\u00f8lgende enheter har en tilstand som ikke kan analyseres som et tall:", + "title": "Enheten har ikke-numerisk tilstand" + }, + "entity_unavailable": { + "description": "Tilstanden til disse konfigurerte enhetene er for \u00f8yeblikket ikke tilgjengelig:", + "title": "Enheten er utilgjengelig" + }, + "entity_unexpected_device_class": { + "description": "F\u00f8lgende enheter har ikke den forventede enhetsklassen:", + "title": "Uventet enhetsklasse" + }, + "entity_unexpected_state_class": { + "description": "F\u00f8lgende enheter har ikke den forventede tilstandsklassen:", + "title": "Uventet statsklasse" + }, + "entity_unexpected_unit_energy": { + "description": "F\u00f8lgende enheter har ikke en forventet m\u00e5leenhet (enten av {energy_units} ):", + "title": "Uventet m\u00e5leenhet" + }, + "entity_unexpected_unit_energy_price": { + "description": "F\u00f8lgende enheter har ikke en forventet m\u00e5leenhet {price_units} :", + "title": "Uventet m\u00e5leenhet" + }, + "entity_unexpected_unit_gas": { + "description": "F\u00f8lgende enheter har ikke en forventet m\u00e5leenhet (enten p\u00e5 {energy_units} for en energisensor eller en av {gas_units} for en gasssensor):", + "title": "Uventet m\u00e5leenhet" + }, + "entity_unexpected_unit_gas_price": { + "description": "F\u00f8lgende enheter har ikke en forventet m\u00e5leenhet (enten av {energy_units} ):", + "title": "Uventet m\u00e5leenhet" + }, + "entity_unexpected_unit_water": { + "description": "F\u00f8lgende enheter har ikke den forventede m\u00e5leenheten (enten av {water_units} ):", + "title": "Uventet m\u00e5leenhet" + }, + "entity_unexpected_unit_water_price": { + "description": "F\u00f8lgende enheter har ikke en forventet m\u00e5leenhet (enten av {energy_units} ):", + "title": "Uventet m\u00e5leenhet" + }, + "recorder_untracked": { + "description": "Opptakeren er konfigurert til \u00e5 ekskludere disse konfigurerte enhetene:", + "title": "Entitet ikke sporet" + } + }, "title": "Energi" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/pl.json b/homeassistant/components/energy/translations/pl.json index c8d85790fdd..b2225c4033c 100644 --- a/homeassistant/components/energy/translations/pl.json +++ b/homeassistant/components/energy/translations/pl.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "Nast\u0119puj\u0105ce encje maj\u0105 ujemn\u0105 warto\u015b\u0107, podczas gdy oczekiwana jest warto\u015b\u0107 dodatnia", + "title": "Encja ma ujemn\u0105 warto\u015b\u0107" + }, + "entity_not_defined": { + "description": "Sprawd\u017a integracj\u0119 lub konfiguracj\u0119, kt\u00f3ra zapewnia:", + "title": "Encja niezdefiniowana" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "Nast\u0119puj\u0105ce encje maj\u0105 klas\u0119 stanu \"measurement\", ale brakuje danych o \"last_reset\":", + "title": "Brak danych o ostatnim resecie" + }, + "entity_state_non_numeric": { + "description": "Nast\u0119puj\u0105ce encje maj\u0105 stan, kt\u00f3rego nie mo\u017cna sparsowa\u0107 jako liczby:", + "title": "Encja nie jest numeryczna" + }, + "entity_unavailable": { + "description": "Stany tych skonfigurowanych encji s\u0105 obecnie niedost\u0119pne:", + "title": "Encja niedost\u0119pna" + }, + "entity_unexpected_device_class": { + "description": "Nast\u0119puj\u0105ce encje nie maj\u0105 oczekiwanej klasy urz\u0105dzenia:", + "title": "Nieoczekiwana klasa urz\u0105dzenia" + }, + "entity_unexpected_state_class": { + "description": "Nast\u0119puj\u0105ce encje nie maj\u0105 oczekiwanej klasy stanu:", + "title": "Nieoczekiwana klasa stanu" + }, + "entity_unexpected_unit_energy": { + "description": "Nast\u0119puj\u0105ce encje nie maj\u0105 wymaganej jednostki miary (\u017cadnej z {energy_units}):", + "title": "Niew\u0142a\u015bciwa jednostka miary" + }, + "entity_unexpected_unit_energy_price": { + "description": "Nast\u0119puj\u0105ce encje nie maj\u0105 wymaganej jednostki miary {price_units}:", + "title": "Niew\u0142a\u015bciwa jednostka miary" + }, + "entity_unexpected_unit_gas": { + "description": "Nast\u0119puj\u0105ce encje nie maj\u0105 wymaganej jednostki miary ({energy_units} dla sensora energii lub {gas_units} dla sensora gazu):", + "title": "Niew\u0142a\u015bciwa jednostka miary" + }, + "entity_unexpected_unit_gas_price": { + "description": "Nast\u0119puj\u0105ce encje nie maj\u0105 wymaganej jednostki miary (\u017cadnej z {energy_units}):", + "title": "Niew\u0142a\u015bciwa jednostka miary" + }, + "entity_unexpected_unit_water": { + "description": "Nast\u0119puj\u0105ce encje nie maj\u0105 wymaganej jednostki miary (\u017cadnej z {water_units}):", + "title": "Niew\u0142a\u015bciwa jednostka miary" + }, + "entity_unexpected_unit_water_price": { + "description": "Nast\u0119puj\u0105ce encje nie maj\u0105 wymaganej jednostki miary (\u017cadnej z {energy_units}):", + "title": "Niew\u0142a\u015bciwa jednostka miary" + }, + "recorder_untracked": { + "description": "Rejestrator (recorder) zosta\u0142 skonfigurowany tak, aby wykluczy\u0107 te skonfigurowane encje:", + "title": "Encja nie jest \u015bledzona" + } + }, "title": "Energia" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/pt-BR.json b/homeassistant/components/energy/translations/pt-BR.json index c8d85790fdd..e0a2629a1ef 100644 --- a/homeassistant/components/energy/translations/pt-BR.json +++ b/homeassistant/components/energy/translations/pt-BR.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "As seguintes entidades t\u00eam um estado negativo enquanto um estado positivo \u00e9 esperado:", + "title": "A entidade tem um estado negativo" + }, + "entity_not_defined": { + "description": "Verifique a integra\u00e7\u00e3o ou sua configura\u00e7\u00e3o que fornece:", + "title": "Entidade n\u00e3o definida" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "As seguintes entidades t\u00eam classe de estado 'measurement', mas 'last_reset' est\u00e1 faltando:", + "title": "\u00daltima reinicializa\u00e7\u00e3o ausente" + }, + "entity_state_non_numeric": { + "description": "As seguintes entidades t\u00eam um estado que n\u00e3o pode ser analisado como um n\u00famero:", + "title": "A entidade tem estado n\u00e3o num\u00e9rico" + }, + "entity_unavailable": { + "description": "O estado dessas entidades configuradas n\u00e3o est\u00e1 dispon\u00edvel no momento:", + "title": "Entidade indispon\u00edvel" + }, + "entity_unexpected_device_class": { + "description": "As seguintes entidades n\u00e3o t\u00eam a classe de dispositivo esperada:", + "title": "Classe de dispositivo inesperada" + }, + "entity_unexpected_state_class": { + "description": "As seguintes entidades n\u00e3o t\u00eam a classe de estado esperada:", + "title": "Classe de estado inesperada" + }, + "entity_unexpected_unit_energy": { + "description": "As seguintes entidades n\u00e3o t\u00eam uma unidade de medida esperada (nenhuma das {energy_units}):", + "title": "Unidade de medida inesperada" + }, + "entity_unexpected_unit_energy_price": { + "description": "As seguintes entidades n\u00e3o t\u00eam uma unidade de medida {price_units}:", + "title": "Unidade de medida inesperada" + }, + "entity_unexpected_unit_gas": { + "description": "As seguintes entidades n\u00e3o t\u00eam uma unidade de medida esperada (seja {energy_units} para um sensor de energia ou {gas_units} para um sensor de g\u00e1s):", + "title": "Unidade de medida inesperada" + }, + "entity_unexpected_unit_gas_price": { + "description": "As seguintes entidades n\u00e3o t\u00eam a unidade de medida esperada (nenhuma das {water_units}):", + "title": "Unidade de medida inesperada" + }, + "entity_unexpected_unit_water": { + "description": "As seguintes entidades n\u00e3o t\u00eam a unidade de medida esperada (nenhuma das {water_units}):", + "title": "Unidade de medida inesperada" + }, + "entity_unexpected_unit_water_price": { + "description": "As seguintes entidades n\u00e3o t\u00eam uma unidade de medida esperada (nenhuma das {energy_units}):", + "title": "Unidade de medida inesperada" + }, + "recorder_untracked": { + "description": "O gravador foi configurado para excluir estas entidades configuradas:", + "title": "Entidade n\u00e3o rastreada" + } + }, "title": "Energia" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/ru.json b/homeassistant/components/energy/translations/ru.json index b351e407168..2e281ab7d0a 100644 --- a/homeassistant/components/energy/translations/ru.json +++ b/homeassistant/components/energy/translations/ru.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u043c\u0435\u044e\u0442 \u043e\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0447\u0438\u0441\u043b\u043e\u0432\u043e\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435, \u0432 \u0442\u043e \u0432\u0440\u0435\u043c\u044f \u043a\u0430\u043a \u043e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u043e\u0436\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435:", + "title": "\u0423 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u043e\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435" + }, + "entity_not_defined": { + "description": "\u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0438\u043b\u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442:", + "title": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u043e\u0431\u044a\u0435\u043a\u0442" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0442 \u043a\u043b\u0430\u0441\u0441\u0443 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f 'measurement', \u043d\u043e \u043d\u0435 \u0438\u043c\u0435\u044e\u0442 \u0430\u0442\u0440\u0438\u0431\u0443\u0442\u0430 'last_reset':", + "title": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0430\u0442\u0440\u0438\u0431\u0443\u0442 'last_reset'" + }, + "entity_state_non_numeric": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u043c\u0435\u044e\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0435 \u0440\u0430\u0441\u043f\u043e\u0437\u043d\u0430\u044e\u0442\u0441\u044f \u043a\u0430\u043a \u0447\u0438\u0441\u043b\u043e:", + "title": "\u0423 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u043d\u0435\u0447\u0438\u0441\u043b\u043e\u0432\u043e\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435" + }, + "entity_unavailable": { + "description": "\u0421\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u044d\u0442\u0438\u0445 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0445 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e:", + "title": "\u041e\u0431\u044a\u0435\u043a\u0442 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d" + }, + "entity_unexpected_device_class": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u043d\u0435 \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u043c\u0443 \u043a\u043b\u0430\u0441\u0441\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430:", + "title": "\u041a\u043b\u0430\u0441\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u0433\u043e" + }, + "entity_unexpected_state_class": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u043d\u0435 \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u043c\u0443 \u043a\u043b\u0430\u0441\u0441\u0443 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f:", + "title": "\u041a\u043b\u0430\u0441\u0441 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u0433\u043e" + }, + "entity_unexpected_unit_energy": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u043c\u0435\u044e\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u0435\u0434\u0438\u043d\u0438\u0446\u0430\u0445 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f, \u043e\u0442\u043b\u0438\u0447\u0430\u044e\u0449\u0438\u0445\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0445 \u0435\u0434\u0438\u043d\u0438\u0446 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f ({energy_units}):", + "title": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u0439" + }, + "entity_unexpected_unit_energy_price": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u043c\u0435\u044e\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u0435\u0434\u0438\u043d\u0438\u0446\u0430\u0445 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f, \u043e\u0442\u043b\u0438\u0447\u0430\u044e\u0449\u0438\u0445\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0445 \u0435\u0434\u0438\u043d\u0438\u0446 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f {price_units}:", + "title": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u0439" + }, + "entity_unexpected_unit_gas": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u043c\u0435\u044e\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u0435\u0434\u0438\u043d\u0438\u0446\u0430\u0445 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f, \u043e\u0442\u043b\u0438\u0447\u0430\u044e\u0449\u0438\u0445\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0445 \u0435\u0434\u0438\u043d\u0438\u0446 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 \u0443\u0447\u0451\u0442\u0430 \u044d\u043d\u0435\u0440\u0433\u0438\u0438 ({energy_units}), \u0438 \u0434\u043b\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432 \u0443\u0447\u0451\u0442\u0430 \u0433\u0430\u0437\u0430 ({gas_units}):", + "title": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u0439" + }, + "entity_unexpected_unit_gas_price": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u043c\u0435\u044e\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u0435\u0434\u0438\u043d\u0438\u0446\u0430\u0445 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f, \u043e\u0442\u043b\u0438\u0447\u0430\u044e\u0449\u0438\u0445\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0445 \u0435\u0434\u0438\u043d\u0438\u0446 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f ({energy_units}):", + "title": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u0439" + }, + "entity_unexpected_unit_water": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u043c\u0435\u044e\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u0435\u0434\u0438\u043d\u0438\u0446\u0430\u0445 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f, \u043e\u0442\u043b\u0438\u0447\u0430\u044e\u0449\u0438\u0445\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0445 \u0435\u0434\u0438\u043d\u0438\u0446 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f ({water_units}):", + "title": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u0439" + }, + "entity_unexpected_unit_water_price": { + "description": "\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0438\u043c\u0435\u044e\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0432 \u0435\u0434\u0438\u043d\u0438\u0446\u0430\u0445 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f, \u043e\u0442\u043b\u0438\u0447\u0430\u044e\u0449\u0438\u0445\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u044b\u0445 \u0435\u0434\u0438\u043d\u0438\u0446 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f ({energy_units}):", + "title": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u043e\u0442\u043b\u0438\u0447\u0430\u0435\u0442\u0441\u044f \u043e\u0442 \u043e\u0436\u0438\u0434\u0430\u0435\u043c\u043e\u0439" + }, + "recorder_untracked": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \"Recorder\" \u043d\u0435 \u0441\u043e\u0445\u0440\u0430\u043d\u044f\u0435\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0445 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0432 \u0431\u0430\u0437\u0435 \u0434\u0430\u043d\u043d\u044b\u0445:", + "title": "\u041e\u0431\u044a\u0435\u043a\u0442 \u043d\u0435 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f" + } + }, "title": "\u042d\u043d\u0435\u0440\u0433\u0438\u044f" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/sk.json b/homeassistant/components/energy/translations/sk.json index c8d85790fdd..e491762008c 100644 --- a/homeassistant/components/energy/translations/sk.json +++ b/homeassistant/components/energy/translations/sk.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "Nasleduj\u00face entity maj\u00fa z\u00e1porn\u00fd stav, pri\u010dom sa o\u010dak\u00e1va kladn\u00fd stav:", + "title": "Entita m\u00e1 negat\u00edvny stav" + }, + "entity_not_defined": { + "description": "Skontrolujte integr\u00e1ciu alebo konfigur\u00e1ciu, ktor\u00e1 poskytuje:", + "title": "Entita nie je definovan\u00e1" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "Nasleduj\u00face entity maj\u00fa stavov\u00fa triedu \"measurement\", ale \"last_reset\" ch\u00fdba:", + "title": "Ch\u00fdba posledn\u00fd reset" + }, + "entity_state_non_numeric": { + "description": "Nasleduj\u00face entity maj\u00fa stav, ktor\u00fd nemo\u017eno analyzova\u0165 ako \u010d\u00edslo:", + "title": "Entita nem\u00e1 \u010d\u00edseln\u00fd stav" + }, + "entity_unavailable": { + "description": "Stav t\u00fdchto nakonfigurovan\u00fdch ent\u00edt moment\u00e1lne nie je k dispoz\u00edcii:", + "title": "Entita nie je k dispoz\u00edcii" + }, + "entity_unexpected_device_class": { + "description": "Nasleduj\u00face entity nepatria do o\u010dak\u00e1vanej triedy zariaden\u00ed:", + "title": "Nespr\u00e1vna trieda zariadenia" + }, + "entity_unexpected_state_class": { + "description": "Nasleduj\u00face entity nepatria do o\u010dak\u00e1vanej stavovej triedy:", + "title": "Nespr\u00e1vna stavov\u00e1 trieda" + }, + "entity_unexpected_unit_energy": { + "description": "Nasleduj\u00face entity nemaj\u00fa o\u010dak\u00e1van\u00fa jednotku merania (ani jedna z {energy_units}):", + "title": "Neo\u010dak\u00e1van\u00e1 meracia jednotka" + }, + "entity_unexpected_unit_energy_price": { + "description": "Nasleduj\u00face entity nemaj\u00fa o\u010dak\u00e1van\u00fa mern\u00fa jednotku {price_units}:", + "title": "Neo\u010dak\u00e1van\u00e1 meracia jednotka" + }, + "entity_unexpected_unit_gas": { + "description": "Nasleduj\u00face entity nemaj\u00fa o\u010dak\u00e1van\u00fa jednotku merania (bu\u010f {energy_units} pre senzor energie alebo niektor\u00fa z {gas_units} pre senzor plynu):", + "title": "Neo\u010dak\u00e1van\u00e1 meracia jednotka" + }, + "entity_unexpected_unit_gas_price": { + "description": "Nasleduj\u00face entity nemaj\u00fa o\u010dak\u00e1van\u00fa jednotku merania (ani jedna z {energy_units}):", + "title": "Neo\u010dak\u00e1van\u00e1 meracia jednotka" + }, + "entity_unexpected_unit_water": { + "description": "Nasleduj\u00face entity nemaj\u00fa o\u010dak\u00e1van\u00fa jednotku merania (ani jedna z {water_units}):", + "title": "Neo\u010dak\u00e1van\u00e1 meracia jednotka" + }, + "entity_unexpected_unit_water_price": { + "description": "Nasleduj\u00face entity nemaj\u00fa o\u010dak\u00e1van\u00fa jednotku merania (ani jedna z {energy_units}):", + "title": "Neo\u010dak\u00e1van\u00e1 meracia jednotka" + }, + "recorder_untracked": { + "description": "Z\u00e1znamn\u00edk bol nastaven\u00fd tak, aby vyl\u00fa\u010dil tieto nakonfigurovan\u00e9 entity:", + "title": "Entita nie je sledovan\u00e1" + } + }, "title": "Energia" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/sv.json b/homeassistant/components/energy/translations/sv.json index 168ae4ae877..c6ab499a3b3 100644 --- a/homeassistant/components/energy/translations/sv.json +++ b/homeassistant/components/energy/translations/sv.json @@ -1,3 +1,54 @@ { + "issues": { + "entity_negative_state": { + "description": "F\u00f6ljande entiteter har ett negativt tillst\u00e5nd medan ett positivt tillst\u00e5nd f\u00f6rv\u00e4ntas:", + "title": "Enheten har ett negativt tillst\u00e5nd" + }, + "entity_not_defined": { + "description": "Kontrollera integreringen eller konfigurationen som tillhandah\u00e5ller:", + "title": "Entiteten \u00e4r inte definierad" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "F\u00f6ljande entiteter har tillst\u00e5ndsklassen 'measurement' men 'last_reset' saknas:", + "title": "Senaste \u00e5terst\u00e4llningstid (last_reset) saknas" + }, + "entity_state_non_numeric": { + "description": "F\u00f6ljande entiteter har ett tillst\u00e5nd som inte kan tolkas som ett tal:", + "title": "Entiteten har icke-numeriskt v\u00e4rde" + }, + "entity_unavailable": { + "description": "Tillst\u00e5ndet f\u00f6r dessa konfigurerade entiteter \u00e4r f\u00f6r n\u00e4rvarande inte tillg\u00e4ngligt:", + "title": "Entiteten \u00e4r inte tillg\u00e4nglig" + }, + "entity_unexpected_device_class": { + "description": "F\u00f6ljande entiteter har inte den f\u00f6rv\u00e4ntade enhetsklassen:", + "title": "Ov\u00e4ntad enhetsklass" + }, + "entity_unexpected_state_class": { + "description": "F\u00f6ljande entiteter har inte den f\u00f6rv\u00e4ntade tillst\u00e5ndsklassen:", + "title": "Ov\u00e4ntad tillst\u00e5ndsklass" + }, + "entity_unexpected_unit_energy": { + "description": "F\u00f6ljande entittet har inte den f\u00f6rv\u00e4ntade m\u00e5ttenheten (n\u00e5gon av {energy_units}):", + "title": "Ov\u00e4ntad m\u00e5ttenhet" + }, + "entity_unexpected_unit_energy_price": { + "description": "F\u00f6ljande entittet har inte den f\u00f6rv\u00e4ntade m\u00e5ttenheten (n\u00e5gon av {price_units}):", + "title": "Ov\u00e4ntad m\u00e5ttenhet" + }, + "entity_unexpected_unit_gas": { + "description": "F\u00f6ljande entittet har inte den f\u00f6rv\u00e4ntade m\u00e5ttenheten (n\u00e5gon av {energy_units} f\u00f6r en energisensor eller n\u00e5gon av {gas_units} f\u00f6r en gassensor):" + }, + "entity_unexpected_unit_water": { + "description": "F\u00f6ljande entittet har inte den f\u00f6rv\u00e4ntade m\u00e5ttenheten (n\u00e5gon av {water_units}):" + }, + "entity_unexpected_unit_water_price": { + "title": "Ov\u00e4ntad m\u00e5ttenhet" + }, + "recorder_untracked": { + "description": "Inspelaren har konfigurerats f\u00f6r att undanta dessa konfigurerade entiteter:", + "title": "Entiteten sp\u00e5ras inte" + } + }, "title": "Energi" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/tr.json b/homeassistant/components/energy/translations/tr.json index 4198959715c..0e7cc5d8592 100644 --- a/homeassistant/components/energy/translations/tr.json +++ b/homeassistant/components/energy/translations/tr.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar negatif bir duruma sahiptir, ancak pozitif bir durum beklenir:", + "title": "Varl\u0131k olumsuz bir duruma sahip" + }, + "entity_not_defined": { + "description": "A\u015fa\u011f\u0131dakileri sa\u011flayan entegrasyonu veya yap\u0131land\u0131rman\u0131z\u0131 kontrol edin:", + "title": "Varl\u0131k tan\u0131mlanmad\u0131" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar \"measurement\" durum s\u0131n\u0131f\u0131na sahip ancak \"last_reset\" eksik:", + "title": "Son s\u0131f\u0131rlama kay\u0131p" + }, + "entity_state_non_numeric": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar, say\u0131 olarak ayr\u0131\u015ft\u0131r\u0131lamayan bir duruma sahiptir:", + "title": "Varl\u0131k say\u0131sal olmayan bir duruma sahip" + }, + "entity_unavailable": { + "description": "Bu yap\u0131land\u0131r\u0131lm\u0131\u015f varl\u0131klar\u0131n durumu \u015fu anda mevcut de\u011fil:", + "title": "Varl\u0131k kullan\u0131lam\u0131yor" + }, + "entity_unexpected_device_class": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar, beklenen cihaz s\u0131n\u0131f\u0131na sahip de\u011fil:", + "title": "Beklenmeyen cihaz s\u0131n\u0131f\u0131" + }, + "entity_unexpected_state_class": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar beklenen durum s\u0131n\u0131f\u0131na sahip de\u011fil:", + "title": "Beklenmedik durum s\u0131n\u0131f\u0131" + }, + "entity_unexpected_unit_energy": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar\u0131n beklenen bir \u00f6l\u00e7\u00fc birimi yoktur ( {energy_units} biri):", + "title": "Beklenmedik \u00f6l\u00e7\u00fc birimi" + }, + "entity_unexpected_unit_energy_price": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar\u0131n beklenen bir \u00f6l\u00e7\u00fcm birimi {price_units} :", + "title": "Beklenmedik \u00f6l\u00e7\u00fc birimi" + }, + "entity_unexpected_unit_gas": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar\u0131n beklenen bir \u00f6l\u00e7\u00fc birimi yok (bir enerji sens\u00f6r\u00fc i\u00e7in {energy_units} {gas_units} :", + "title": "Beklenmedik \u00f6l\u00e7\u00fc birimi" + }, + "entity_unexpected_unit_gas_price": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar\u0131n beklenen bir \u00f6l\u00e7\u00fc birimi yoktur ( {energy_units} biri):", + "title": "Beklenmedik \u00f6l\u00e7\u00fc birimi" + }, + "entity_unexpected_unit_water": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar beklenen \u00f6l\u00e7\u00fc birimine sahip de\u011fil ( {water_units} biri):", + "title": "Beklenmedik \u00f6l\u00e7\u00fc birimi" + }, + "entity_unexpected_unit_water_price": { + "description": "A\u015fa\u011f\u0131daki varl\u0131klar\u0131n beklenen bir \u00f6l\u00e7\u00fc birimi yoktur ( {energy_units} biri):", + "title": "Beklenmedik \u00f6l\u00e7\u00fc birimi" + }, + "recorder_untracked": { + "description": "Kay\u0131t cihaz\u0131, \u015fu yap\u0131land\u0131r\u0131lm\u0131\u015f varl\u0131klar\u0131 hari\u00e7 tutacak \u015fekilde yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r:", + "title": "Varl\u0131k izlenmedi" + } + }, "title": "Enerji" } \ No newline at end of file diff --git a/homeassistant/components/energy/translations/uk.json b/homeassistant/components/energy/translations/uk.json new file mode 100644 index 00000000000..dd4162cc0eb --- /dev/null +++ b/homeassistant/components/energy/translations/uk.json @@ -0,0 +1,34 @@ +{ + "issues": { + "entity_state_class_measurement_no_last_reset": { + "title": "\u0412\u0456\u0434\u0441\u0443\u0442\u043d\u0454 \u043e\u0441\u0442\u0430\u043d\u043d\u0454 \u0441\u043a\u0438\u0434\u0430\u043d\u043d\u044f" + }, + "entity_unavailable": { + "title": "\u0421\u0443\u0442\u043d\u0456\u0441\u0442\u044c \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430" + }, + "entity_unexpected_device_class": { + "title": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0438\u0439 \u043a\u043b\u0430\u0441 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "entity_unexpected_unit_energy": { + "title": "\u041d\u0435\u0441\u043f\u043e\u0434\u0456\u0432\u0430\u043d\u0430 \u043e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "entity_unexpected_unit_energy_price": { + "title": "\u041d\u0435\u0441\u043f\u043e\u0434\u0456\u0432\u0430\u043d\u0430 \u043e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "entity_unexpected_unit_gas": { + "title": "\u041d\u0435\u0441\u043f\u043e\u0434\u0456\u0432\u0430\u043d\u0430 \u043e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "entity_unexpected_unit_gas_price": { + "title": "\u041d\u0435\u0441\u043f\u043e\u0434\u0456\u0432\u0430\u043d\u0430 \u043e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "entity_unexpected_unit_water": { + "title": "\u041d\u0435\u0441\u043f\u043e\u0434\u0456\u0432\u0430\u043d\u0430 \u043e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "entity_unexpected_unit_water_price": { + "title": "\u041d\u0435\u0441\u043f\u043e\u0434\u0456\u0432\u0430\u043d\u0430 \u043e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "recorder_untracked": { + "title": "\u041e\u0431\u2019\u0454\u043a\u0442 \u043d\u0435 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energy/translations/zh-Hant.json b/homeassistant/components/energy/translations/zh-Hant.json index bae50fae66e..0d048e6ddb5 100644 --- a/homeassistant/components/energy/translations/zh-Hant.json +++ b/homeassistant/components/energy/translations/zh-Hant.json @@ -1,3 +1,61 @@ { + "issues": { + "entity_negative_state": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u5305\u542b\u8ca0\u72c0\u614b\u503c\u3001\u800c\u9810\u671f\u5177\u6709\u6b63\u503c\u72c0\u614b\uff1a", + "title": "\u5305\u542b\u8ca0\u72c0\u614b\u5be6\u9ad4" + }, + "entity_not_defined": { + "description": "\u8acb\u6aa2\u67e5\u6574\u5408\u6216\u6240\u63d0\u4f9b\u7684\u8a2d\u5b9a\uff1a", + "title": "\u5be6\u9ad4\u672a\u5b9a\u7fa9" + }, + "entity_state_class_measurement_no_last_reset": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u5305\u542b\u72c0\u614b\u985e\u5225 'measurement'\u3001\u4f46\u7f3a\u5c11 'last_reset'\uff1a", + "title": "\u7f3a\u5c11\u4e0a\u6b21\u91cd\u7f6e" + }, + "entity_state_non_numeric": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u5305\u542b\u7121\u6cd5\u5206\u6790\u70ba\u6578\u5b57\u7684\u72c0\u614b\uff1a", + "title": "\u5be6\u9ad4\u70ba\u975e\u6578\u5b57" + }, + "entity_unavailable": { + "description": "\u6b64\u4e9b\u8a2d\u5b9a\u5be6\u9ad4\u72c0\u614b\u76ee\u524d\u4e0d\u53ef\u7528\uff1a", + "title": "\u5be6\u9ad4\u4e0d\u53ef\u7528" + }, + "entity_unexpected_device_class": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u672a\u5305\u542b\u6240\u9810\u671f\u7684\u88dd\u7f6e\u985e\u5225\uff1a", + "title": "\u672a\u9810\u671f\u88dd\u7f6e\u985e\u5225" + }, + "entity_unexpected_state_class": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u672a\u5305\u542b\u6240\u9810\u671f\u7684\u72c0\u614b\u985e\u5225\uff1a", + "title": "\u672a\u9810\u671f\u72c0\u614b\u985e\u5225" + }, + "entity_unexpected_unit_energy": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u672a\u5305\u542b\u6240\u9810\u671f\u7684\u55ae\u4f4d\u503c\uff08{energy_units} \u4e4b\u4e00\uff09\uff1a", + "title": "\u672a\u9810\u671f\u55ae\u4f4d\u503c" + }, + "entity_unexpected_unit_energy_price": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u672a\u5305\u542b\u6240\u9810\u671f\u7684\u55ae\u4f4d\u503c {price_units}\uff1a", + "title": "\u672a\u9810\u671f\u55ae\u4f4d\u503c" + }, + "entity_unexpected_unit_gas": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u672a\u5305\u542b\u6240\u9810\u671f\u7684\u55ae\u4f4d\u503c\uff08\u80fd\u6e90\u611f\u6e2c\u5668 {energy_units} \u4e4b\u4e00\u6216\u5929\u7136\u6c23\u611f\u6e2c\u5668 {gas_units} \u4e4b\u4e00 \uff09\uff1a", + "title": "\u672a\u9810\u671f\u55ae\u4f4d\u503c" + }, + "entity_unexpected_unit_gas_price": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u672a\u5305\u542b\u6240\u9810\u671f\u7684\u55ae\u4f4d\u503c\uff08{energy_units} \u4e4b\u4e00\uff09\uff1a", + "title": "\u672a\u9810\u671f\u55ae\u4f4d\u503c" + }, + "entity_unexpected_unit_water": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u672a\u5305\u542b\u6240\u9810\u671f\u7684\u55ae\u4f4d\u503c\uff08{water_units}\u4e4b\u4e00\uff09\uff1a", + "title": "\u672a\u9810\u671f\u55ae\u4f4d\u503c" + }, + "entity_unexpected_unit_water_price": { + "description": "\u4ee5\u4e0b\u5be6\u9ad4\u672a\u5305\u542b\u6240\u9810\u671f\u7684\u55ae\u4f4d\u503c\uff08{energy_units} \u4e4b\u4e00\uff09\uff1a", + "title": "\u672a\u9810\u671f\u55ae\u4f4d\u503c" + }, + "recorder_untracked": { + "description": "\u65e5\u8a8c\u5df2\u7d93\u8a2d\u5b9a\u70ba\u6392\u9664\u6b64\u4e9b\u8a2d\u5b9a\u5be6\u9ad4\uff1a", + "title": "\u5be6\u9ad4\u672a\u8ffd\u8e64" + } + }, "title": "\u80fd\u6e90" } \ No newline at end of file diff --git a/homeassistant/components/energy/validate.py b/homeassistant/components/energy/validate.py index 55d11f5f04d..a2c3ad094da 100644 --- a/homeassistant/components/energy/validate.py +++ b/homeassistant/components/energy/validate.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Mapping, Sequence import dataclasses import functools -from typing import Any from homeassistant.components import recorder, sensor from homeassistant.const import ( @@ -24,6 +23,7 @@ ENERGY_USAGE_UNITS = { sensor.SensorDeviceClass.ENERGY: ( UnitOfEnergy.GIGA_JOULE, UnitOfEnergy.KILO_WATT_HOUR, + UnitOfEnergy.MEGA_JOULE, UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.WATT_HOUR, ) @@ -41,6 +41,7 @@ GAS_USAGE_UNITS = { sensor.SensorDeviceClass.ENERGY: ( UnitOfEnergy.GIGA_JOULE, UnitOfEnergy.KILO_WATT_HOUR, + UnitOfEnergy.MEGA_JOULE, UnitOfEnergy.MEGA_WATT_HOUR, UnitOfEnergy.WATT_HOUR, ), @@ -72,29 +73,94 @@ WATER_UNIT_ERROR = "entity_unexpected_unit_water" WATER_PRICE_UNIT_ERROR = "entity_unexpected_unit_water_price" +def _get_placeholders(hass: HomeAssistant, issue_type: str) -> dict[str, str] | None: + currency = hass.config.currency + if issue_type == ENERGY_UNIT_ERROR: + return { + "energy_units": ", ".join( + ENERGY_USAGE_UNITS[sensor.SensorDeviceClass.ENERGY] + ), + } + if issue_type == ENERGY_PRICE_UNIT_ERROR: + return { + "price_units": ", ".join( + f"{currency}{unit}" for unit in ENERGY_PRICE_UNITS + ), + } + if issue_type == GAS_UNIT_ERROR: + return { + "energy_units": ", ".join(GAS_USAGE_UNITS[sensor.SensorDeviceClass.ENERGY]), + "gas_units": ", ".join(GAS_USAGE_UNITS[sensor.SensorDeviceClass.GAS]), + } + if issue_type == GAS_PRICE_UNIT_ERROR: + return { + "price_units": ", ".join(f"{currency}{unit}" for unit in GAS_PRICE_UNITS), + } + if issue_type == WATER_UNIT_ERROR: + return { + "water_units": ", ".join(WATER_USAGE_UNITS[sensor.SensorDeviceClass.WATER]), + } + if issue_type == WATER_PRICE_UNIT_ERROR: + return { + "price_units": ", ".join(f"{currency}{unit}" for unit in WATER_PRICE_UNITS), + } + return None + + @dataclasses.dataclass class ValidationIssue: """Error or warning message.""" type: str - identifier: str - value: Any | None = None + affected_entities: set[tuple[str, float | str | None]] = dataclasses.field( + default_factory=set + ) + translation_placeholders: dict[str, str] | None = None + + +@dataclasses.dataclass +class ValidationIssues: + """Container for validation issues.""" + + issues: dict[str, ValidationIssue] = dataclasses.field(default_factory=dict) + + def __init__(self) -> None: + """Container for validiation issues.""" + self.issues = {} + + def add_issue( + self, + hass: HomeAssistant, + issue_type: str, + affected_entity: str, + detail: float | str | None = None, + ) -> None: + """Add an issue for an entity.""" + if not (issue := self.issues.get(issue_type)): + self.issues[issue_type] = issue = ValidationIssue(issue_type) + issue.translation_placeholders = _get_placeholders(hass, issue_type) + issue.affected_entities.add((affected_entity, detail)) @dataclasses.dataclass class EnergyPreferencesValidation: """Dictionary holding validation information.""" - energy_sources: list[list[ValidationIssue]] = dataclasses.field( - default_factory=list - ) - device_consumption: list[list[ValidationIssue]] = dataclasses.field( - default_factory=list - ) + energy_sources: list[ValidationIssues] = dataclasses.field(default_factory=list) + device_consumption: list[ValidationIssues] = dataclasses.field(default_factory=list) def as_dict(self) -> dict: """Return dictionary version.""" - return dataclasses.asdict(self) + return { + "energy_sources": [ + [dataclasses.asdict(issue) for issue in issues.issues.values()] + for issues in self.energy_sources + ], + "device_consumption": [ + [dataclasses.asdict(issue) for issue in issues.issues.values()] + for issues in self.device_consumption + ], + } @callback @@ -105,11 +171,11 @@ def _async_validate_usage_stat( allowed_device_classes: Sequence[str], allowed_units: Mapping[str, Sequence[str]], unit_error: str, - result: list[ValidationIssue], + issues: ValidationIssues, ) -> None: """Validate a statistic.""" if stat_id not in metadata: - result.append(ValidationIssue("statistics_not_defined", stat_id)) + issues.add_issue(hass, "statistics_not_defined", stat_id) has_entity_source = valid_entity_id(stat_id) @@ -119,54 +185,36 @@ def _async_validate_usage_stat( entity_id = stat_id if not recorder.is_entity_recorded(hass, entity_id): - result.append( - ValidationIssue( - "recorder_untracked", - entity_id, - ) - ) + issues.add_issue(hass, "recorder_untracked", entity_id) return if (state := hass.states.get(entity_id)) is None: - result.append( - ValidationIssue( - "entity_not_defined", - entity_id, - ) - ) + issues.add_issue(hass, "entity_not_defined", entity_id) return if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): - result.append(ValidationIssue("entity_unavailable", entity_id, state.state)) + issues.add_issue(hass, "entity_unavailable", entity_id, state.state) return try: current_value: float | None = float(state.state) except ValueError: - result.append( - ValidationIssue("entity_state_non_numeric", entity_id, state.state) - ) + issues.add_issue(hass, "entity_state_non_numeric", entity_id, state.state) return if current_value is not None and current_value < 0: - result.append( - ValidationIssue("entity_negative_state", entity_id, current_value) - ) + issues.add_issue(hass, "entity_negative_state", entity_id, current_value) device_class = state.attributes.get(ATTR_DEVICE_CLASS) if device_class not in allowed_device_classes: - result.append( - ValidationIssue( - "entity_unexpected_device_class", - entity_id, - device_class, - ) + issues.add_issue( + hass, "entity_unexpected_device_class", entity_id, device_class ) else: unit = state.attributes.get("unit_of_measurement") if device_class and unit not in allowed_units.get(device_class, []): - result.append(ValidationIssue(unit_error, entity_id, unit)) + issues.add_issue(hass, unit_error, entity_id, unit) state_class = state.attributes.get(sensor.ATTR_STATE_CLASS) @@ -176,20 +224,14 @@ def _async_validate_usage_stat( sensor.SensorStateClass.TOTAL_INCREASING, ] if state_class not in allowed_state_classes: - result.append( - ValidationIssue( - "entity_unexpected_state_class", - entity_id, - state_class, - ) - ) + issues.add_issue(hass, "entity_unexpected_state_class", entity_id, state_class) if ( state_class == sensor.SensorStateClass.MEASUREMENT and sensor.ATTR_LAST_RESET not in state.attributes ): - result.append( - ValidationIssue("entity_state_class_measurement_no_last_reset", entity_id) + issues.add_issue( + hass, "entity_state_class_measurement_no_last_reset", entity_id ) @@ -197,32 +239,25 @@ def _async_validate_usage_stat( def _async_validate_price_entity( hass: HomeAssistant, entity_id: str, - result: list[ValidationIssue], + issues: ValidationIssues, allowed_units: tuple[str, ...], unit_error: str, ) -> None: """Validate that the price entity is correct.""" if (state := hass.states.get(entity_id)) is None: - result.append( - ValidationIssue( - "entity_not_defined", - entity_id, - ) - ) + issues.add_issue(hass, "entity_not_defined", entity_id) return try: float(state.state) except ValueError: - result.append( - ValidationIssue("entity_state_non_numeric", entity_id, state.state) - ) + issues.add_issue(hass, "entity_state_non_numeric", entity_id, state.state) return unit = state.attributes.get("unit_of_measurement") if unit is None or not unit.endswith(allowed_units): - result.append(ValidationIssue(unit_error, entity_id, unit)) + issues.add_issue(hass, unit_error, entity_id, unit) @callback @@ -230,11 +265,11 @@ def _async_validate_cost_stat( hass: HomeAssistant, metadata: dict[str, tuple[int, recorder.models.StatisticMetaData]], stat_id: str, - result: list[ValidationIssue], + issues: ValidationIssues, ) -> None: """Validate that the cost stat is correct.""" if stat_id not in metadata: - result.append(ValidationIssue("statistics_not_defined", stat_id)) + issues.add_issue(hass, "statistics_not_defined", stat_id) has_entity = valid_entity_id(stat_id) @@ -242,10 +277,10 @@ def _async_validate_cost_stat( return if not recorder.is_entity_recorded(hass, stat_id): - result.append(ValidationIssue("recorder_untracked", stat_id)) + issues.add_issue(hass, "recorder_untracked", stat_id) if (state := hass.states.get(stat_id)) is None: - result.append(ValidationIssue("entity_not_defined", stat_id)) + issues.add_issue(hass, "entity_not_defined", stat_id) return state_class = state.attributes.get("state_class") @@ -256,22 +291,18 @@ def _async_validate_cost_stat( sensor.SensorStateClass.TOTAL_INCREASING, ] if state_class not in supported_state_classes: - result.append( - ValidationIssue("entity_unexpected_state_class", stat_id, state_class) - ) + issues.add_issue(hass, "entity_unexpected_state_class", stat_id, state_class) if ( state_class == sensor.SensorStateClass.MEASUREMENT and sensor.ATTR_LAST_RESET not in state.attributes ): - result.append( - ValidationIssue("entity_state_class_measurement_no_last_reset", stat_id) - ) + issues.add_issue(hass, "entity_state_class_measurement_no_last_reset", stat_id) @callback def _async_validate_auto_generated_cost_entity( - hass: HomeAssistant, energy_entity_id: str, result: list[ValidationIssue] + hass: HomeAssistant, energy_entity_id: str, issues: ValidationIssues ) -> None: """Validate that the auto generated cost entity is correct.""" if energy_entity_id not in hass.data[DOMAIN]["cost_sensors"]: @@ -280,7 +311,7 @@ def _async_validate_auto_generated_cost_entity( cost_entity_id = hass.data[DOMAIN]["cost_sensors"][energy_entity_id] if not recorder.is_entity_recorded(hass, cost_entity_id): - result.append(ValidationIssue("recorder_untracked", cost_entity_id)) + issues.add_issue(hass, "recorder_untracked", cost_entity_id) async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: @@ -297,7 +328,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: # Create a list of validation checks for source in manager.data["energy_sources"]: - source_result: list[ValidationIssue] = [] + source_result = ValidationIssues() result.energy_sources.append(source_result) if source["type"] == "grid": @@ -550,7 +581,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation: ) for device in manager.data["device_consumption"]: - device_result: list[ValidationIssue] = [] + device_result = ValidationIssues() result.device_consumption.append(device_result) wanted_statistics_metadata.add(device["stat_consumption"]) validate_calls.append( diff --git a/homeassistant/components/energyzero/__init__.py b/homeassistant/components/energyzero/__init__.py new file mode 100644 index 00000000000..096e312efc0 --- /dev/null +++ b/homeassistant/components/energyzero/__init__.py @@ -0,0 +1,35 @@ +"""The EnergyZero integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN +from .coordinator import EnergyZeroDataUpdateCoordinator + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up EnergyZero from a config entry.""" + + coordinator = EnergyZeroDataUpdateCoordinator(hass) + try: + await coordinator.async_config_entry_first_refresh() + except ConfigEntryNotReady: + await coordinator.energyzero.close() + raise + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload EnergyZero 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/energyzero/config_flow.py b/homeassistant/components/energyzero/config_flow.py new file mode 100644 index 00000000000..55fffbdec91 --- /dev/null +++ b/homeassistant/components/energyzero/config_flow.py @@ -0,0 +1,31 @@ +"""Config flow for EnergyZero 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 EnergyZeroFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow for EnergyZero integration.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + + await self.async_set_unique_id(DOMAIN) + self._abort_if_unique_id_configured() + + if user_input is None: + return self.async_show_form(step_id="user") + + return self.async_create_entry( + title="EnergyZero", + data={}, + ) diff --git a/homeassistant/components/energyzero/const.py b/homeassistant/components/energyzero/const.py new file mode 100644 index 00000000000..03d94facf3b --- /dev/null +++ b/homeassistant/components/energyzero/const.py @@ -0,0 +1,16 @@ +"""Constants for the EnergyZero integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Final + +DOMAIN: Final = "energyzero" +LOGGER = logging.getLogger(__package__) +SCAN_INTERVAL = timedelta(minutes=10) +THRESHOLD_HOUR: Final = 14 + +SERVICE_TYPE_DEVICE_NAMES = { + "today_energy": "Energy market price", + "today_gas": "Gas market price", +} diff --git a/homeassistant/components/energyzero/coordinator.py b/homeassistant/components/energyzero/coordinator.py new file mode 100644 index 00000000000..284ae37ce22 --- /dev/null +++ b/homeassistant/components/energyzero/coordinator.py @@ -0,0 +1,80 @@ +"""The Coordinator for EnergyZero.""" +from __future__ import annotations + +from datetime import timedelta +from typing import NamedTuple + +from energyzero import ( + Electricity, + EnergyZero, + EnergyZeroConnectionError, + EnergyZeroNoDataError, + Gas, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt + +from .const import DOMAIN, LOGGER, SCAN_INTERVAL, THRESHOLD_HOUR + + +class EnergyZeroData(NamedTuple): + """Class for defining data in dict.""" + + energy_today: Electricity + energy_tomorrow: Electricity | None + gas_today: Gas | None + + +class EnergyZeroDataUpdateCoordinator(DataUpdateCoordinator[EnergyZeroData]): + """Class to manage fetching EnergyZero data from single endpoint.""" + + config_entry: ConfigEntry + + def __init__(self, hass) -> None: + """Initialize global EnergyZero data updater.""" + super().__init__( + hass, + LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + + self.energyzero = EnergyZero(session=async_get_clientsession(hass)) + + async def _async_update_data(self) -> EnergyZeroData: + """Fetch data from EnergyZero.""" + today = dt.now().date() + gas_today = None + energy_tomorrow = None + + try: + energy_today = await self.energyzero.energy_prices( + start_date=today, end_date=today + ) + try: + gas_today = await self.energyzero.gas_prices( + start_date=today, end_date=today + ) + except EnergyZeroNoDataError: + LOGGER.debug("No data for gas prices for EnergyZero integration") + # Energy for tomorrow only after 14:00 UTC + if dt.utcnow().hour >= THRESHOLD_HOUR: + tomorrow = today + timedelta(days=1) + try: + energy_tomorrow = await self.energyzero.energy_prices( + start_date=tomorrow, end_date=tomorrow + ) + except EnergyZeroNoDataError: + LOGGER.debug("No data for tomorrow for EnergyZero integration") + + except EnergyZeroConnectionError as err: + raise UpdateFailed("Error communicating with EnergyZero API") from err + + return EnergyZeroData( + energy_today=energy_today, + energy_tomorrow=energy_tomorrow, + gas_today=gas_today, + ) diff --git a/homeassistant/components/energyzero/diagnostics.py b/homeassistant/components/energyzero/diagnostics.py new file mode 100644 index 00000000000..5e3e402efbf --- /dev/null +++ b/homeassistant/components/energyzero/diagnostics.py @@ -0,0 +1,58 @@ +"""Diagnostics support for EnergyZero.""" +from __future__ import annotations + +from datetime import timedelta +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import EnergyZeroDataUpdateCoordinator +from .const import DOMAIN +from .coordinator import EnergyZeroData + + +def get_gas_price(data: EnergyZeroData, hours: int) -> float | None: + """Get the gas price for a given hour. + + Args: + data: The data object. + hours: The number of hours to add to the current time. + + Returns: + The gas market price value. + """ + if not data.gas_today: + return None + return data.gas_today.price_at_time( + data.gas_today.utcnow() + timedelta(hours=hours) + ) + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: EnergyZeroDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + return { + "entry": { + "title": entry.title, + }, + "energy": { + "current_hour_price": coordinator.data.energy_today.current_price, + "next_hour_price": coordinator.data.energy_today.price_at_time( + coordinator.data.energy_today.utcnow() + timedelta(hours=1) + ), + "average_price": coordinator.data.energy_today.average_price, + "max_price": coordinator.data.energy_today.extreme_prices[1], + "min_price": coordinator.data.energy_today.extreme_prices[0], + "highest_price_time": coordinator.data.energy_today.highest_price_time, + "lowest_price_time": coordinator.data.energy_today.lowest_price_time, + "percentage_of_max": coordinator.data.energy_today.pct_of_max_price, + }, + "gas": { + "current_hour_price": get_gas_price(coordinator.data, 0), + "next_hour_price": get_gas_price(coordinator.data, 1), + }, + } diff --git a/homeassistant/components/energyzero/manifest.json b/homeassistant/components/energyzero/manifest.json new file mode 100644 index 00000000000..f0a76ab3baf --- /dev/null +++ b/homeassistant/components/energyzero/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "energyzero", + "name": "EnergyZero", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/energyzero", + "requirements": ["energyzero==0.3.1"], + "codeowners": ["@klaasnicolaas"], + "iot_class": "cloud_polling", + "quality_scale": "platinum" +} diff --git a/homeassistant/components/energyzero/sensor.py b/homeassistant/components/energyzero/sensor.py new file mode 100644 index 00000000000..75b5fa6fea6 --- /dev/null +++ b/homeassistant/components/energyzero/sensor.py @@ -0,0 +1,189 @@ +"""Support for EnergyZero sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime, timedelta + +from homeassistant.components.sensor import ( + DOMAIN as SENSOR_DOMAIN, + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CURRENCY_EURO, PERCENTAGE, UnitOfEnergy, UnitOfVolume +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, SERVICE_TYPE_DEVICE_NAMES +from .coordinator import EnergyZeroData, EnergyZeroDataUpdateCoordinator + + +@dataclass +class EnergyZeroSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[EnergyZeroData], float | datetime | None] + service_type: str + + +@dataclass +class EnergyZeroSensorEntityDescription( + SensorEntityDescription, EnergyZeroSensorEntityDescriptionMixin +): + """Describes a Pure Energie sensor entity.""" + + +SENSORS: tuple[EnergyZeroSensorEntityDescription, ...] = ( + EnergyZeroSensorEntityDescription( + key="current_hour_price", + name="Current hour", + service_type="today_gas", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfVolume.CUBIC_METERS}", + value_fn=lambda data: data.gas_today.current_price if data.gas_today else None, + ), + EnergyZeroSensorEntityDescription( + key="next_hour_price", + name="Next hour", + service_type="today_gas", + native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfVolume.CUBIC_METERS}", + value_fn=lambda data: get_gas_price(data, 1), + ), + EnergyZeroSensorEntityDescription( + key="current_hour_price", + name="Current hour", + service_type="today_energy", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", + value_fn=lambda data: data.energy_today.current_price, + ), + EnergyZeroSensorEntityDescription( + key="next_hour_price", + name="Next hour", + service_type="today_energy", + native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", + value_fn=lambda data: data.energy_today.price_at_time( + data.energy_today.utcnow() + timedelta(hours=1) + ), + ), + EnergyZeroSensorEntityDescription( + key="average_price", + name="Average - today", + service_type="today_energy", + native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", + value_fn=lambda data: data.energy_today.average_price, + ), + EnergyZeroSensorEntityDescription( + key="max_price", + name="Highest price - today", + service_type="today_energy", + native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", + value_fn=lambda data: data.energy_today.extreme_prices[1], + ), + EnergyZeroSensorEntityDescription( + key="min_price", + name="Lowest price - today", + service_type="today_energy", + native_unit_of_measurement=f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}", + value_fn=lambda data: data.energy_today.extreme_prices[0], + ), + EnergyZeroSensorEntityDescription( + key="highest_price_time", + name="Time of highest price - today", + service_type="today_energy", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda data: data.energy_today.highest_price_time, + ), + EnergyZeroSensorEntityDescription( + key="lowest_price_time", + name="Time of lowest price - today", + service_type="today_energy", + device_class=SensorDeviceClass.TIMESTAMP, + value_fn=lambda data: data.energy_today.lowest_price_time, + ), + EnergyZeroSensorEntityDescription( + key="percentage_of_max", + name="Current percentage of highest price - today", + service_type="today_energy", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:percent", + value_fn=lambda data: data.energy_today.pct_of_max_price, + ), +) + + +def get_gas_price(data: EnergyZeroData, hours: int) -> float | None: + """Return the gas value. + + Args: + data: The data object. + hours: The number of hours to add to the current time. + + Returns: + The gas market price value. + """ + if data.gas_today is None: + return None + return data.gas_today.price_at_time( + data.gas_today.utcnow() + timedelta(hours=hours) + ) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up EnergyZero Sensors based on a config entry.""" + coordinator: EnergyZeroDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + EnergyZeroSensorEntity( + coordinator=coordinator, + description=description, + ) + for description in SENSORS + ) + + +class EnergyZeroSensorEntity( + CoordinatorEntity[EnergyZeroDataUpdateCoordinator], SensorEntity +): + """Defines a EnergyZero sensor.""" + + _attr_has_entity_name = True + _attr_attribution = "Data provided by EnergyZero" + entity_description: EnergyZeroSensorEntityDescription + + def __init__( + self, + *, + coordinator: EnergyZeroDataUpdateCoordinator, + description: EnergyZeroSensorEntityDescription, + ) -> None: + """Initialize EnergyZero sensor.""" + super().__init__(coordinator=coordinator) + self.entity_description = description + self.entity_id = ( + f"{SENSOR_DOMAIN}.{DOMAIN}_{description.service_type}_{description.key}" + ) + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.service_type}_{description.key}" + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={ + ( + DOMAIN, + f"{coordinator.config_entry.entry_id}_{description.service_type}", + ) + }, + manufacturer="EnergyZero", + name=SERVICE_TYPE_DEVICE_NAMES[self.entity_description.service_type], + ) + + @property + def native_value(self) -> float | datetime | None: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/energyzero/strings.json b/homeassistant/components/energyzero/strings.json new file mode 100644 index 00000000000..ed89e0068d4 --- /dev/null +++ b/homeassistant/components/energyzero/strings.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "description": "[%key:common::config_flow::description::confirm_setup%]" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/energyzero/translations/bg.json b/homeassistant/components/energyzero/translations/bg.json new file mode 100644 index 00000000000..c5c8f46cbe7 --- /dev/null +++ b/homeassistant/components/energyzero/translations/bg.json @@ -0,0 +1,12 @@ +{ + "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": { + "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\u043a\u0430\u0442\u0430?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/ca.json b/homeassistant/components/energyzero/translations/ca.json new file mode 100644 index 00000000000..402171b5577 --- /dev/null +++ b/homeassistant/components/energyzero/translations/ca.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "step": { + "user": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/de.json b/homeassistant/components/energyzero/translations/de.json new file mode 100644 index 00000000000..441cbe25d2c --- /dev/null +++ b/homeassistant/components/energyzero/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "step": { + "user": { + "description": "M\u00f6chtest Du mit der Einrichtung beginnen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/el.json b/homeassistant/components/energyzero/translations/el.json new file mode 100644 index 00000000000..92c90dd4ced --- /dev/null +++ b/homeassistant/components/energyzero/translations/el.json @@ -0,0 +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" + }, + "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/energyzero/translations/en.json b/homeassistant/components/energyzero/translations/en.json new file mode 100644 index 00000000000..9384d0b5f96 --- /dev/null +++ b/homeassistant/components/energyzero/translations/en.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "step": { + "user": { + "description": "Do you want to start setup?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/es.json b/homeassistant/components/energyzero/translations/es.json new file mode 100644 index 00000000000..4c7255093fe --- /dev/null +++ b/homeassistant/components/energyzero/translations/es.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "step": { + "user": { + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/et.json b/homeassistant/components/energyzero/translations/et.json new file mode 100644 index 00000000000..45166a3793e --- /dev/null +++ b/homeassistant/components/energyzero/translations/et.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "step": { + "user": { + "description": "Kas soovid alustada seadistamist?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/fr.json b/homeassistant/components/energyzero/translations/fr.json new file mode 100644 index 00000000000..573ad7da7d0 --- /dev/null +++ b/homeassistant/components/energyzero/translations/fr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "step": { + "user": { + "description": "Voulez-vous commencer la configuration\u00a0?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/he.json b/homeassistant/components/energyzero/translations/he.json new file mode 100644 index 00000000000..6a541d69122 --- /dev/null +++ b/homeassistant/components/energyzero/translations/he.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4" + }, + "step": { + "user": { + "description": "\u05d4\u05d0\u05dd \u05d1\u05e8\u05e6\u05d5\u05e0\u05da \u05dc\u05d4\u05ea\u05d7\u05d9\u05dc \u05d1\u05d4\u05d2\u05d3\u05e8\u05d4?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/hu.json b/homeassistant/components/energyzero/translations/hu.json new file mode 100644 index 00000000000..15df14d9025 --- /dev/null +++ b/homeassistant/components/energyzero/translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "user": { + "description": "El szeretn\u00e9 kezdeni a be\u00e1ll\u00edt\u00e1st?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/id.json b/homeassistant/components/energyzero/translations/id.json new file mode 100644 index 00000000000..e28c2c9aeaf --- /dev/null +++ b/homeassistant/components/energyzero/translations/id.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "step": { + "user": { + "description": "Ingin memulai penyiapan?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/it.json b/homeassistant/components/energyzero/translations/it.json new file mode 100644 index 00000000000..87319e01469 --- /dev/null +++ b/homeassistant/components/energyzero/translations/it.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "step": { + "user": { + "description": "Vuoi avviare la configurazione?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/ja.json b/homeassistant/components/energyzero/translations/ja.json new file mode 100644 index 00000000000..662f91bf14a --- /dev/null +++ b/homeassistant/components/energyzero/translations/ja.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "step": { + "user": { + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3092\u958b\u59cb\u3057\u307e\u3059\u304b\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/lv.json b/homeassistant/components/energyzero/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/energyzero/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/nl.json b/homeassistant/components/energyzero/translations/nl.json new file mode 100644 index 00000000000..ab5e684681d --- /dev/null +++ b/homeassistant/components/energyzero/translations/nl.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "step": { + "user": { + "description": "Wil je beginnen met instellen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/no.json b/homeassistant/components/energyzero/translations/no.json new file mode 100644 index 00000000000..f4c4da3ea02 --- /dev/null +++ b/homeassistant/components/energyzero/translations/no.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "step": { + "user": { + "description": "Vil du starte oppsettet?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/pl.json b/homeassistant/components/energyzero/translations/pl.json new file mode 100644 index 00000000000..c510f49c98a --- /dev/null +++ b/homeassistant/components/energyzero/translations/pl.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "step": { + "user": { + "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/pt-BR.json b/homeassistant/components/energyzero/translations/pt-BR.json new file mode 100644 index 00000000000..310d21fdbd1 --- /dev/null +++ b/homeassistant/components/energyzero/translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "description": "Deseja iniciar a configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/ru.json b/homeassistant/components/energyzero/translations/ru.json new file mode 100644 index 00000000000..926075cc118 --- /dev/null +++ b/homeassistant/components/energyzero/translations/ru.json @@ -0,0 +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." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/sk.json b/homeassistant/components/energyzero/translations/sk.json new file mode 100644 index 00000000000..1417acb1b50 --- /dev/null +++ b/homeassistant/components/energyzero/translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "step": { + "user": { + "description": "Chcete za\u010da\u0165 nastavova\u0165?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/tr.json b/homeassistant/components/energyzero/translations/tr.json new file mode 100644 index 00000000000..6942419b0a1 --- /dev/null +++ b/homeassistant/components/energyzero/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/uk.json b/homeassistant/components/energyzero/translations/uk.json new file mode 100644 index 00000000000..426cc55b43b --- /dev/null +++ b/homeassistant/components/energyzero/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + }, + "step": { + "user": { + "description": "\u0411\u0430\u0436\u0430\u0454\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/energyzero/translations/zh-Hant.json b/homeassistant/components/energyzero/translations/zh-Hant.json new file mode 100644 index 00000000000..e7f8152534e --- /dev/null +++ b/homeassistant/components/energyzero/translations/zh-Hant.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/__init__.py b/homeassistant/components/enphase_envoy/__init__.py index 61c2fd86c77..147eddacf81 100644 --- a/homeassistant/components/enphase_envoy/__init__.py +++ b/homeassistant/components/enphase_envoy/__init__.py @@ -78,8 +78,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady( f"Could not obtain serial number from envoy: {ex}" ) from ex - else: - hass.config_entries.async_update_entry(entry, unique_id=serial) + + hass.config_entries.async_update_entry(entry, unique_id=serial) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { COORDINATOR: coordinator, diff --git a/homeassistant/components/enphase_envoy/translations/lv.json b/homeassistant/components/enphase_envoy/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enphase_envoy/translations/sk.json b/homeassistant/components/enphase_envoy/translations/sk.json index a0f360ba85f..fd295f12e88 100644 --- a/homeassistant/components/enphase_envoy/translations/sk.json +++ b/homeassistant/components/enphase_envoy/translations/sk.json @@ -17,7 +17,7 @@ "password": "Heslo", "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" }, - "description": "V pr\u00edpade nov\u0161\u00edch modelov zadajte pou\u017e\u00edvate\u013esk\u00e9 meno \u201eenvoy\u201c bez hesla. Pri star\u0161\u00edch modeloch zadajte pou\u017e\u00edvate\u013esk\u00e9 meno `installer` bez hesla. Pre v\u0161etky ostatn\u00e9 modely zadajte platn\u00e9 pou\u017e\u00edvate\u013esk\u00e9 meno a heslo." + "description": "V pr\u00edpade nov\u0161\u00edch modelov zadajte pou\u017e\u00edvate\u013esk\u00e9 meno 'envoy' bez hesla. Pri star\u0161\u00edch modeloch zadajte pou\u017e\u00edvate\u013esk\u00e9 meno `installer` bez hesla. Pre v\u0161etky ostatn\u00e9 modely zadajte platn\u00e9 pou\u017e\u00edvate\u013esk\u00e9 meno a heslo." } } } diff --git a/homeassistant/components/enphase_envoy/translations/uk.json b/homeassistant/components/enphase_envoy/translations/uk.json new file mode 100644 index 00000000000..273e935a348 --- /dev/null +++ b/homeassistant/components/enphase_envoy/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0414\u043b\u044f \u043d\u043e\u0432\u0456\u0448\u0438\u0445 \u043c\u043e\u0434\u0435\u043b\u0435\u0439 \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 `envoy` \u0431\u0435\u0437 \u043f\u0430\u0440\u043e\u043b\u044f. \u0414\u043b\u044f \u0441\u0442\u0430\u0440\u0456\u0448\u0438\u0445 \u043c\u043e\u0434\u0435\u043b\u0435\u0439 \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 `installer` \u0431\u0435\u0437 \u043f\u0430\u0440\u043e\u043b\u044f. \u0414\u043b\u044f \u0432\u0441\u0456\u0445 \u0456\u043d\u0448\u0438\u0445 \u043c\u043e\u0434\u0435\u043b\u0435\u0439 \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0434\u0456\u0439\u0441\u043d\u0435 \u0456\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/diagnostics.py b/homeassistant/components/environment_canada/diagnostics.py index f1064cea52e..297f4664fb0 100644 --- a/homeassistant/components/environment_canada/diagnostics.py +++ b/homeassistant/components/environment_canada/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Environment Canada.""" 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_LATITUDE, CONF_LONGITUDE @@ -13,7 +15,7 @@ TO_REDACT = {CONF_LATITUDE, CONF_LONGITUDE} async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinators = hass.data[DOMAIN][config_entry.entry_id] weather_coord = coordinators["weather_coordinator"] diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 83c7ca455cb..b7d3644d481 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,7 +2,7 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": ["env_canada==0.5.22"], + "requirements": ["env_canada==0.5.27"], "codeowners": ["@gwww", "@michaeldavie"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/environment_canada/strings.json b/homeassistant/components/environment_canada/strings.json index 49686cba123..4c6d75cfeb6 100644 --- a/homeassistant/components/environment_canada/strings.json +++ b/homeassistant/components/environment_canada/strings.json @@ -12,6 +12,9 @@ } } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, "error": { "bad_station_id": "Station ID is invalid, missing, or not found in the station ID database", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", diff --git a/homeassistant/components/environment_canada/translations/bg.json b/homeassistant/components/environment_canada/translations/bg.json index 28c4730e5cd..6662d60d180 100644 --- a/homeassistant/components/environment_canada/translations/bg.json +++ b/homeassistant/components/environment_canada/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "too_many_attempts": "\u0412\u0440\u044a\u0437\u043a\u0438\u0442\u0435 \u0441 Environment Canada \u0441\u0430 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438; \u041e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e \u0441\u043b\u0435\u0434 60 \u0441\u0435\u043a\u0443\u043d\u0434\u0438", diff --git a/homeassistant/components/environment_canada/translations/ca.json b/homeassistant/components/environment_canada/translations/ca.json index f847b2dc5ac..b00fe106215 100644 --- a/homeassistant/components/environment_canada/translations/ca.json +++ b/homeassistant/components/environment_canada/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, "error": { "bad_station_id": "L'ID d'estaci\u00f3 no \u00e9s v\u00e0lid, no est\u00e0 present o no es troba a la base de dades d'IDs d'estacions", "cannot_connect": "Ha fallat la connexi\u00f3", diff --git a/homeassistant/components/environment_canada/translations/de.json b/homeassistant/components/environment_canada/translations/de.json index 6c9be14be27..8adc7d6c83c 100644 --- a/homeassistant/components/environment_canada/translations/de.json +++ b/homeassistant/components/environment_canada/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, "error": { "bad_station_id": "Die Stations-ID ist ung\u00fcltig, fehlt oder wurde in der Stations-ID-Datenbank nicht gefunden", "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/environment_canada/translations/en.json b/homeassistant/components/environment_canada/translations/en.json index 94c0b947fa4..be30ffded77 100644 --- a/homeassistant/components/environment_canada/translations/en.json +++ b/homeassistant/components/environment_canada/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Service is already configured" + }, "error": { "bad_station_id": "Station ID is invalid, missing, or not found in the station ID database", "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/environment_canada/translations/et.json b/homeassistant/components/environment_canada/translations/et.json index af93b060144..895d9c0b804 100644 --- a/homeassistant/components/environment_canada/translations/et.json +++ b/homeassistant/components/environment_canada/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud" + }, "error": { "bad_station_id": "Jaama ID ei sobi, puudub v\u00f5i seda ei leitud jaamade ID andmebaasist", "cannot_connect": "\u00dchendamine nurjus", diff --git a/homeassistant/components/environment_canada/translations/no.json b/homeassistant/components/environment_canada/translations/no.json index 8d0fb1f201b..425f6530def 100644 --- a/homeassistant/components/environment_canada/translations/no.json +++ b/homeassistant/components/environment_canada/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, "error": { "bad_station_id": "Stasjons -ID er ugyldig, mangler eller finnes ikke i stasjons -ID -databasen", "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/environment_canada/translations/ru.json b/homeassistant/components/environment_canada/translations/ru.json index 26c0108ed3a..adb58caf1c3 100644 --- a/homeassistant/components/environment_canada/translations/ru.json +++ b/homeassistant/components/environment_canada/translations/ru.json @@ -1,5 +1,8 @@ { "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": { "bad_station_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d, \u043b\u0438\u0431\u043e \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", diff --git a/homeassistant/components/environment_canada/translations/zh-Hant.json b/homeassistant/components/environment_canada/translations/zh-Hant.json index 59fe99e8ead..e6aebc801f4 100644 --- a/homeassistant/components/environment_canada/translations/zh-Hant.json +++ b/homeassistant/components/environment_canada/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "bad_station_id": "\u6c23\u8c61\u7ad9 ID \u7121\u6548\u3001\u907a\u5931\u6216\u8cc7\u6599\u5eab\u4e2d\u627e\u4e0d\u5230\u8a72 ID", "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/epson/strings.json b/homeassistant/components/epson/strings.json index 41a5f175ee7..9716153958b 100644 --- a/homeassistant/components/epson/strings.json +++ b/homeassistant/components/epson/strings.json @@ -8,6 +8,9 @@ } } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "powered_off": "Is projector turned on? You need to turn on projector for initial configuration." diff --git a/homeassistant/components/epson/translations/bg.json b/homeassistant/components/epson/translations/bg.json index d2c9013bcc5..c5fc0654c51 100644 --- a/homeassistant/components/epson/translations/bg.json +++ b/homeassistant/components/epson/translations/bg.json @@ -1,5 +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" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, diff --git a/homeassistant/components/epson/translations/ca.json b/homeassistant/components/epson/translations/ca.json index eae6b1329d5..a881febcb16 100644 --- a/homeassistant/components/epson/translations/ca.json +++ b/homeassistant/components/epson/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "powered_off": "El projector est\u00e0 enc\u00e8s? Per fer la configuraci\u00f3 inicial has d'activar el projector." diff --git a/homeassistant/components/epson/translations/de.json b/homeassistant/components/epson/translations/de.json index 2d53861fb75..a21daeea16d 100644 --- a/homeassistant/components/epson/translations/de.json +++ b/homeassistant/components/epson/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "powered_off": "Ist der Projektor eingeschaltet? Du musst den Projektor f\u00fcr die Erstkonfiguration einschalten." diff --git a/homeassistant/components/epson/translations/en.json b/homeassistant/components/epson/translations/en.json index 931bbcf557e..ad741ff1c24 100644 --- a/homeassistant/components/epson/translations/en.json +++ b/homeassistant/components/epson/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Device is already configured" + }, "error": { "cannot_connect": "Failed to connect", "powered_off": "Is projector turned on? You need to turn on projector for initial configuration." diff --git a/homeassistant/components/epson/translations/et.json b/homeassistant/components/epson/translations/et.json index 755e5810c25..75cbc090ee4 100644 --- a/homeassistant/components/epson/translations/et.json +++ b/homeassistant/components/epson/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, "error": { "cannot_connect": "\u00dchendamine nurjus", "powered_off": "Kas projektor on sisse l\u00fclitatud? Esmaseks seadistamiseks pead projektori sisse l\u00fclitama." diff --git a/homeassistant/components/epson/translations/no.json b/homeassistant/components/epson/translations/no.json index 882b12801d4..7384d1b3f19 100644 --- a/homeassistant/components/epson/translations/no.json +++ b/homeassistant/components/epson/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, "error": { "cannot_connect": "Tilkobling mislyktes", "powered_off": "Er projektoren sl\u00e5tt p\u00e5? Du m\u00e5 sl\u00e5 p\u00e5 projektoren for \u00e5 f\u00e5 den f\u00f8rste konfigurasjonen." diff --git a/homeassistant/components/epson/translations/ru.json b/homeassistant/components/epson/translations/ru.json index e800f033c3e..c96650abbc3 100644 --- a/homeassistant/components/epson/translations/ru.json +++ b/homeassistant/components/epson/translations/ru.json @@ -1,5 +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." + }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "powered_off": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u043b\u0438 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0440? \u0414\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 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u0440 \u0434\u043e\u043b\u0436\u0435\u043d \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438." diff --git a/homeassistant/components/epson/translations/zh-Hant.json b/homeassistant/components/epson/translations/zh-Hant.json index 4831db6c564..d2c8a753ed4 100644 --- a/homeassistant/components/epson/translations/zh-Hant.json +++ b/homeassistant/components/epson/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "powered_off": "\u6295\u5f71\u6a5f\u662f\u5426\u70ba\u95dc\u9589\u72c0\u614b\uff1f\u5fc5\u9808\u958b\u555f\u6295\u5f71\u6a5f\u624d\u80fd\u9032\u884c\u521d\u59cb\u8a2d\u5b9a\u3002" diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index ade3bc0d912..6db659f61ae 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -3,7 +3,7 @@ "name": "eQ-3 Bluetooth Smart Thermostats", "documentation": "https://www.home-assistant.io/integrations/eq3btsmart", "requirements": ["construct==2.10.56", "python-eq3bt==0.2"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@rytilahti"], "iot_class": "local_polling", "loggers": ["bleak", "eq3bt"] diff --git a/homeassistant/components/escea/__init__.py b/homeassistant/components/escea/__init__.py index 95e6765fa95..98d86ead76b 100644 --- a/homeassistant/components/escea/__init__.py +++ b/homeassistant/components/escea/__init__.py @@ -12,7 +12,7 @@ PLATFORMS = [CLIMATE_DOMAIN] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up from a config entry.""" await async_start_discovery_service(hass) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index b0f046ef1eb..d2c987d56ba 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -17,6 +17,7 @@ from aioesphomeapi import ( EntityInfo, EntityState, HomeassistantServiceCall, + InvalidAuthAPIError, InvalidEncryptionKeyAPIError, ReconnectLogic, RequiresEncryptionAPIError, @@ -26,7 +27,6 @@ from aioesphomeapi import ( from awesomeversion import AwesomeVersion import voluptuous as vol -from homeassistant import const from homeassistant.components import tag, zeroconf from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -36,6 +36,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, EVENT_HOMEASSISTANT_STOP, + __version__ as ha_version, ) from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback from homeassistant.exceptions import TemplateError @@ -56,11 +57,14 @@ from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.template import Template from .bluetooth import async_connect_scanner -from .domain_data import DOMAIN, DomainData +from .const import DOMAIN +from .dashboard import async_get_dashboard +from .domain_data import DomainData # Import config flow so that it's added to the registry from .entry_data import RuntimeEntryData +CONF_DEVICE_NAME = "device_name" CONF_NOISE_PSK = "noise_psk" _LOGGER = logging.getLogger(__name__) _R = TypeVar("_R") @@ -104,6 +108,30 @@ def _async_check_firmware_version( ) +@callback +def _async_check_using_api_password( + hass: HomeAssistant, device_info: EsphomeDeviceInfo, has_password: bool +) -> None: + """Create or delete an the api_password_deprecated issue.""" + # ESPHome device_info.mac_address is the unique_id + issue = f"api_password_deprecated-{device_info.mac_address}" + if not has_password: + async_delete_issue(hass, DOMAIN, issue) + return + async_create_issue( + hass, + DOMAIN, + issue, + is_fixable=False, + severity=IssueSeverity.WARNING, + learn_more_url="https://esphome.io/components/api.html", + translation_key="api_password_deprecated", + translation_placeholders={ + "name": device_info.name, + }, + ) + + async def async_setup_entry( # noqa: C901 hass: HomeAssistant, entry: ConfigEntry ) -> bool: @@ -120,7 +148,7 @@ async def async_setup_entry( # noqa: C901 host, port, password, - client_info=f"Home Assistant {const.__version__}", + client_info=f"Home Assistant {ha_version}", zeroconf_instance=zeroconf_instance, noise_psk=noise_psk, ) @@ -139,7 +167,8 @@ async def async_setup_entry( # noqa: C901 # Use async_listen instead of async_listen_once so that we don't deregister # the callback twice when shutting down Home Assistant. - # "Unable to remove unknown listener .onetime_listener>" + # "Unable to remove unknown listener + # .onetime_listener>" entry_data.cleanup_callbacks.append( hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop) ) @@ -267,6 +296,13 @@ async def async_setup_entry( # noqa: C901 entry, unique_id=format_mac(device_info.mac_address) ) + # Make sure we have the correct device name stored + # so we can map the device to ESPHome Dashboard config + if entry.data.get(CONF_DEVICE_NAME) != device_info.name: + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_DEVICE_NAME: device_info.name} + ) + entry_data.device_info = device_info assert cli.api_version is not None entry_data.api_version = cli.api_version @@ -298,6 +334,7 @@ async def async_setup_entry( # noqa: C901 await cli.disconnect() else: _async_check_firmware_version(hass, device_info) + _async_check_using_api_password(hass, device_info, bool(password)) async def on_disconnect() -> None: """Run disconnect callbacks on API disconnect.""" @@ -311,7 +348,14 @@ async def async_setup_entry( # noqa: C901 async def on_connect_error(err: Exception) -> None: """Start reauth flow if appropriate connect error type.""" - if isinstance(err, (RequiresEncryptionAPIError, InvalidEncryptionKeyAPIError)): + if isinstance( + err, + ( + RequiresEncryptionAPIError, + InvalidEncryptionKeyAPIError, + InvalidAuthAPIError, + ), + ): entry.async_start_reauth(hass) reconnect_logic = ReconnectLogic( @@ -352,6 +396,8 @@ def _async_setup_device_registry( configuration_url = None if device_info.webserver_port > 0: configuration_url = f"http://{entry.data['host']}:{device_info.webserver_port}" + elif dashboard := async_get_dashboard(hass): + configuration_url = f"homeassistant://hassio/ingress/{dashboard.addon_slug}" manufacturer = "espressif" if device_info.manufacturer: @@ -369,7 +415,7 @@ def _async_setup_device_registry( config_entry_id=entry.entry_id, configuration_url=configuration_url, connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}, - name=device_info.name, + name=device_info.friendly_name or device_info.name, manufacturer=manufacturer, model=model, sw_version=sw_version, @@ -600,9 +646,10 @@ async def platform_async_setup_entry( # Add entities to Home Assistant async_add_entities(add_entities) - signal = f"esphome_{entry.entry_id}_on_list" entry_data.cleanup_callbacks.append( - async_dispatcher_connect(hass, signal, async_list_entities) + async_dispatcher_connect( + hass, entry_data.signal_static_info_updated, async_list_entities + ) ) @@ -640,7 +687,9 @@ class EsphomeEnumMapper(Generic[_EnumT, _ValT]): def __init__(self, mapping: dict[_EnumT, _ValT]) -> None: """Construct a EsphomeEnumMapper.""" # Add none mapping - augmented_mapping: dict[_EnumT | None, _ValT | None] = mapping # type: ignore[assignment] + augmented_mapping: dict[ + _EnumT | None, _ValT | None + ] = mapping # type: ignore[assignment] augmented_mapping[None] = None self._mapping = augmented_mapping @@ -694,6 +743,8 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): self._component_key = component_key self._key = key self._state_type = state_type + if entry_data.device_info is not None and entry_data.device_info.friendly_name: + self._attr_has_entity_name = True async def async_added_to_hass(self) -> None: """Register callbacks.""" @@ -810,7 +861,10 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]): @property def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ return not self._static_info.disabled_by_default @property diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 6aa21c315a3..4ef36849e32 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -68,20 +68,24 @@ def verify_connected(func: _WrapFuncType) -> _WrapFuncType: ) if not disconnected_event: raise BleakError("Not connected") - task = asyncio.create_task(func(self, *args, **kwargs)) - done, _ = await asyncio.wait( - (task, disconnected_event.wait()), + action_task = asyncio.create_task(func(self, *args, **kwargs)) + disconnect_task = asyncio.create_task(disconnected_event.wait()) + await asyncio.wait( + (action_task, disconnect_task), return_when=asyncio.FIRST_COMPLETED, ) - if disconnected_event.is_set(): - task.cancel() + if disconnect_task.done(): + action_task.cancel() with contextlib.suppress(asyncio.CancelledError): - await task + await action_task + raise BleakError( - f"{self._source_name}: {self._ble_device.name} - {self._ble_device.address}: " # pylint: disable=protected-access + f"{self._source_name}: " # pylint: disable=protected-access + f"{self._ble_device.name} - " # pylint: disable=protected-access + f" {self._ble_device.address}: " # pylint: disable=protected-access "Disconnected during operation" ) - return next(iter(done)).result() + return action_task.result() return cast(_WrapFuncType, _async_wrap_bluetooth_connected_operation) @@ -105,8 +109,8 @@ def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType: # that we find out about the disconnection during the operation # before the callback is delivered. - # pylint: disable=protected-access if ex.error.error == -1: + # pylint: disable=protected-access _LOGGER.debug( "%s: %s - %s: BLE device disconnected during %s operation", self._source_name, @@ -228,7 +232,8 @@ class ESPHomeClient(BaseBleakClient): """Connect to a specified Peripheral. Keyword Args: - timeout (float): Timeout for required ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. + timeout (float): Timeout for required + ``BleakScanner.find_device_by_address`` call. Defaults to 10.0. Returns: Boolean representing connection status. """ @@ -395,7 +400,8 @@ class ESPHomeClient(BaseBleakClient): """Get all services registered for this GATT server. Returns: - A :py:class:`bleak.backends.service.BleakGATTServiceCollection` with this device's services tree. + A :py:class:`bleak.backends.service.BleakGATTServiceCollection` + with this device's services tree. """ address_as_int = self._address_as_int domain_data = self.domain_data @@ -494,9 +500,10 @@ class ESPHomeClient(BaseBleakClient): """Perform read operation on the specified GATT characteristic. Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to read from, - specified by either integer handle, UUID or directly by the - BleakGATTCharacteristic object representing it. + char_specifier (BleakGATTCharacteristic, int, str or UUID): + The characteristic to read from, specified by either integer + handle, UUID or directly by the BleakGATTCharacteristic + object representing it. Returns: (bytearray) The read data. """ @@ -530,11 +537,13 @@ class ESPHomeClient(BaseBleakClient): """Perform a write operation of the specified GATT characteristic. Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to write - to, specified by either integer handle, UUID or directly by the - BleakGATTCharacteristic object representing it. + char_specifier (BleakGATTCharacteristic, int, str or UUID): + The characteristic to write to, specified by either integer + handle, UUID or directly by the BleakGATTCharacteristic object + representing it. data (bytes or bytearray): The data to send. - response (bool): If write-with-response operation should be done. Defaults to `False`. + response (bool): If write-with-response operation should be done. + Defaults to `False`. """ characteristic = self._resolve_characteristic(char_specifier) await self._client.bluetooth_gatt_write( @@ -566,17 +575,21 @@ class ESPHomeClient(BaseBleakClient): ) -> None: """Activate notifications/indications on a characteristic. - Callbacks must accept two inputs. The first will be a integer handle of the characteristic generating the - data and the second will be a ``bytearray`` containing the data sent from the connected server. + Callbacks must accept two inputs. The first will be a integer handle of the + characteristic generating the data and the second will be a ``bytearray`` + containing the data sent from the connected server. + .. code-block:: python def callback(sender: int, data: bytearray): print(f"{sender}: {data}") client.start_notify(char_uuid, callback) Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to activate - notifications/indications on a characteristic, specified by either integer handle, - UUID or directly by the BleakGATTCharacteristic object representing it. + characteristic (BleakGATTCharacteristic): + The characteristic to activate notifications/indications on a + characteristic, specified by either integer handle, UUID or + directly by the BleakGATTCharacteristic object representing it. callback (function): The function to be called on notification. + kwargs: Unused. """ ble_handle = characteristic.handle if ble_handle in self._notify_cancels: @@ -645,9 +658,10 @@ class ESPHomeClient(BaseBleakClient): """Deactivate notification/indication on a specified characteristic. Args: - char_specifier (BleakGATTCharacteristic, int, str or UUID): The characteristic to deactivate - notification/indication on, specified by either integer handle, UUID or - directly by the BleakGATTCharacteristic object representing it. + char_specifier (BleakGATTCharacteristic, int, str or UUID): + The characteristic to deactivate notification/indication on, + specified by either integer handle, UUID or directly by the + BleakGATTCharacteristic object representing it. """ characteristic = self._resolve_characteristic(char_specifier) # Do not raise KeyError if notifications are not enabled on this characteristic diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 7186eda039b..acc94bc7ea0 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections import OrderedDict from collections.abc import Mapping +import logging from typing import Any from aioesphomeapi import ( @@ -14,19 +15,25 @@ from aioesphomeapi import ( RequiresEncryptionAPIError, ResolveAPIError, ) +import aiohttp import voluptuous as vol from homeassistant.components import dhcp, zeroconf +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.device_registry import format_mac -from . import CONF_NOISE_PSK, DOMAIN +from . import CONF_DEVICE_NAME, CONF_NOISE_PSK +from .const import DOMAIN +from .dashboard import async_get_dashboard, async_set_dashboard_info ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key" +ERROR_INVALID_ENCRYPTION_KEY = "invalid_psk" ESPHOME_URL = "https://esphome.io/" +_LOGGER = logging.getLogger(__name__) class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): @@ -42,12 +49,16 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._noise_psk: str | None = None self._device_info: DeviceInfo | None = None self._reauth_entry: ConfigEntry | None = None + # The ESPHome name as per its config + self._device_name: str | None = None async def _async_step_user_base( self, user_input: dict[str, Any] | None = None, error: str | None = None ) -> FlowResult: if user_input is not None: - return await self._async_try_fetch_device_info(user_input) + self._host = user_input[CONF_HOST] + self._port = user_input[CONF_PORT] + return await self._async_try_fetch_device_info() fields: dict[Any, type] = OrderedDict() fields[vol.Required(CONF_HOST, default=self._host or vol.UNDEFINED)] = str @@ -78,8 +89,22 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._host = entry.data[CONF_HOST] self._port = entry.data[CONF_PORT] self._password = entry.data[CONF_PASSWORD] - self._noise_psk = entry.data.get(CONF_NOISE_PSK) self._name = entry.title + self._device_name = entry.data.get(CONF_DEVICE_NAME) + + # Device without encryption allows fetching device info. We can then check + # if the device is no longer using a password. If we did try with a password, + # we know setting password to empty will allow us to authenticate. + error = await self.fetch_device_info() + if ( + error is None + and self._password + and self._device_info + and not self._device_info.uses_password + ): + self._password = "" + return await self._async_authenticate_or_add() + return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( @@ -88,6 +113,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): """Handle reauthorization flow.""" errors = {} + if await self._retrieve_encryption_key_from_dashboard(): + error = await self.fetch_device_info() + if error is None: + return await self._async_authenticate_or_add() + if user_input is not None: self._noise_psk = user_input[CONF_NOISE_PSK] error = await self.fetch_device_info() @@ -111,17 +141,19 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self.context[CONF_NAME] = value self.context["title_placeholders"] = {"name": self._name} - def _set_user_input(self, user_input: dict[str, Any] | None) -> None: - if user_input is None: - return - self._host = user_input[CONF_HOST] - self._port = user_input[CONF_PORT] - - async def _async_try_fetch_device_info( - self, user_input: dict[str, Any] | None - ) -> FlowResult: - self._set_user_input(user_input) + async def _async_try_fetch_device_info(self) -> FlowResult: error = await self.fetch_device_info() + + if ( + error == ERROR_REQUIRES_ENCRYPTION_KEY + and await self._retrieve_encryption_key_from_dashboard() + ): + error = await self.fetch_device_info() + # If the fetched key is invalid, unset it again. + if error == ERROR_INVALID_ENCRYPTION_KEY: + self._noise_psk = None + error = ERROR_REQUIRES_ENCRYPTION_KEY + if error == ERROR_REQUIRES_ENCRYPTION_KEY: return await self.async_step_encryption_key() if error is not None: @@ -134,6 +166,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): if self._device_info.uses_password: return await self.async_step_authenticate() + self._password = "" return self._async_get_entry() async def async_step_discovery_confirm( @@ -141,7 +174,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle user-confirmation of discovered node.""" if user_input is not None: - return await self._async_try_fetch_device_info(None) + return await self._async_try_fetch_device_info() return self.async_show_form( step_id="discovery_confirm", description_placeholders={"name": self._name} ) @@ -161,7 +194,10 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): mac_address = format_mac(mac_address) # Hostname is format: livingroom.local. - self._name = discovery_info.hostname[: -len(".local.")] + device_name = discovery_info.hostname.removesuffix(".local.") + + self._name = discovery_info.properties.get("friendly_name", device_name) + self._device_name = device_name self._host = discovery_info.host self._port = discovery_info.port @@ -181,6 +217,16 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): # for configured devices. return self.async_abort(reason="already_configured") + async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: + """Handle Supervisor service discovery.""" + await async_set_dashboard_info( + self.hass, + discovery_info.slug, + discovery_info.config["host"], + discovery_info.config["port"], + ) + return self.async_abort(reason="service_received") + @callback def _async_get_entry(self) -> FlowResult: config_data = { @@ -189,6 +235,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): # The API uses protobuf, so empty string denotes absence CONF_PASSWORD: self._password or "", CONF_NOISE_PSK: self._noise_psk or "", + CONF_DEVICE_NAME: self._device_name, } if self._reauth_entry: entry = self._reauth_entry @@ -268,7 +315,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): except RequiresEncryptionAPIError: return ERROR_REQUIRES_ENCRYPTION_KEY except InvalidEncryptionKeyAPIError: - return "invalid_psk" + return ERROR_INVALID_ENCRYPTION_KEY except ResolveAPIError: return "resolve_error" except APIConnectionError: @@ -276,7 +323,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): finally: await cli.disconnect(force=True) - self._name = self._device_info.name + self._name = self._device_info.friendly_name or self._device_info.name + self._device_name = self._device_info.name await self.async_set_unique_id( self._device_info.mac_address, raise_on_progress=False ) @@ -310,3 +358,33 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): await cli.disconnect(force=True) return None + + async def _retrieve_encryption_key_from_dashboard(self) -> bool: + """Try to retrieve the encryption key from the dashboard. + + Return boolean if a key was retrieved. + """ + if self._device_name is None: + return False + + if (dashboard := async_get_dashboard(self.hass)) is None: + return False + + await dashboard.async_request_refresh() + + if not dashboard.last_update_success: + return False + + device = dashboard.data.get(self._device_name) + + if device is None: + return False + + try: + noise_psk = await dashboard.api.get_encryption_key(device["configuration"]) + except aiohttp.ClientError as err: + _LOGGER.error("Error talking to the dashboard: %s", err) + return False + + self._noise_psk = noise_psk + return True diff --git a/homeassistant/components/esphome/const.py b/homeassistant/components/esphome/const.py new file mode 100644 index 00000000000..617c817924b --- /dev/null +++ b/homeassistant/components/esphome/const.py @@ -0,0 +1,3 @@ +"""ESPHome constants.""" + +DOMAIN = "esphome" diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py new file mode 100644 index 00000000000..7439f8946f6 --- /dev/null +++ b/homeassistant/components/esphome/dashboard.py @@ -0,0 +1,104 @@ +"""Files to interact with a the ESPHome dashboard.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +import logging + +import aiohttp +from awesomeversion import AwesomeVersion +from esphome_dashboard_api import ConfiguredDevice, ESPHomeDashboardAPI + +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +KEY_DASHBOARD = "esphome_dashboard" + + +@callback +def async_get_dashboard(hass: HomeAssistant) -> ESPHomeDashboard | None: + """Get an instance of the dashboard if set.""" + return hass.data.get(KEY_DASHBOARD) + + +async def async_set_dashboard_info( + hass: HomeAssistant, addon_slug: str, host: str, port: int +) -> None: + """Set the dashboard info.""" + url = f"http://{host}:{port}" + + # Do nothing if we already have this data. + if ( + (cur_dashboard := hass.data.get(KEY_DASHBOARD)) + and cur_dashboard.addon_slug == addon_slug + and cur_dashboard.url == url + ): + return + + dashboard = ESPHomeDashboard(hass, addon_slug, url, async_get_clientsession(hass)) + try: + await dashboard.async_request_refresh() + except UpdateFailed as err: + logging.getLogger(__name__).error("Ignoring dashboard info: %s", err) + return + + hass.data[KEY_DASHBOARD] = dashboard + + reloads = [ + hass.config_entries.async_reload(entry.entry_id) + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + # Re-auth flows will check the dashboard for encryption key when the form is requested + reauths = [ + hass.config_entries.flow.async_configure(flow["flow_id"]) + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN and flow["context"]["source"] == SOURCE_REAUTH + ] + if reloads or reauths: + await asyncio.gather(*reloads, *reauths) + + +class ESPHomeDashboard(DataUpdateCoordinator[dict[str, ConfiguredDevice]]): + """Class to interact with the ESPHome dashboard.""" + + def __init__( + self, + hass: HomeAssistant, + addon_slug: str, + url: str, + session: aiohttp.ClientSession, + ) -> None: + """Initialize.""" + super().__init__( + hass, + logging.getLogger(__name__), + name="ESPHome Dashboard", + update_interval=timedelta(minutes=5), + ) + self.addon_slug = addon_slug + self.url = url + self.api = ESPHomeDashboardAPI(url, session) + + @property + def supports_update(self) -> bool: + """Return whether the dashboard supports updates.""" + if self.data is None: + raise RuntimeError("Data needs to be loaded first") + + if len(self.data) == 0: + return False + + esphome_version: str = next(iter(self.data.values()))["current_version"] + + # There is no January release + return AwesomeVersion(esphome_version) > AwesomeVersion("2023.1.0") + + async def _async_update_data(self) -> dict: + """Fetch device data.""" + devices = await self.api.get_devices() + return {dev["name"]: dev for dev in devices["configured"]} diff --git a/homeassistant/components/esphome/diagnostics.py b/homeassistant/components/esphome/diagnostics.py index 68f195a23fb..8de1501bc43 100644 --- a/homeassistant/components/esphome/diagnostics.py +++ b/homeassistant/components/esphome/diagnostics.py @@ -10,6 +10,7 @@ from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant from . import CONF_NOISE_PSK, DomainData +from .dashboard import async_get_dashboard CONF_MAC_ADDRESS = "mac_address" @@ -39,4 +40,7 @@ async def async_get_config_entry_diagnostics( "scanner": await scanner.async_diagnostics(), } + if dashboard := async_get_dashboard(hass): + diag["dashboard"] = dashboard.addon_slug + return async_redact_data(diag, REDACT_KEYS) diff --git a/homeassistant/components/esphome/domain_data.py b/homeassistant/components/esphome/domain_data.py index 93ff69852a0..07029e2610a 100644 --- a/homeassistant/components/esphome/domain_data.py +++ b/homeassistant/components/esphome/domain_data.py @@ -13,10 +13,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.storage import Store +from .const import DOMAIN from .entry_data import RuntimeEntryData STORAGE_VERSION = 1 -DOMAIN = "esphome" MAX_CACHED_SERVICES = 128 _DomainDataSelfT = TypeVar("_DomainDataSelfT", bound="DomainData") diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 959bf2f2877..b7443eea211 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -37,11 +37,13 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store +from .dashboard import async_get_dashboard + SAVE_DELAY = 120 _LOGGER = logging.getLogger(__name__) # Mapping from ESPHome info type to HA platform -INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], str] = { +INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = { BinarySensorInfo: Platform.BINARY_SENSOR, ButtonInfo: Platform.BUTTON, CameraInfo: Platform.CAMERA, @@ -84,7 +86,7 @@ class RuntimeEntryData: state_subscriptions: dict[ tuple[type[EntityState], int], Callable[[], None] ] = field(default_factory=dict) - loaded_platforms: set[str] = field(default_factory=set) + loaded_platforms: set[Platform] = field(default_factory=set) platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock) _storage_contents: dict[str, Any] | None = None ble_connections_free: int = 0 @@ -98,6 +100,18 @@ class RuntimeEntryData: """Return the name of the device.""" return self.device_info.name if self.device_info else self.entry_id + @property + def friendly_name(self) -> str: + """Return the friendly name of the device.""" + if self.device_info and self.device_info.friendly_name: + return self.device_info.friendly_name + return self.name + + @property + def signal_static_info_updated(self) -> str: + """Return the signal to listen to for updates on static info.""" + return f"esphome_{self.entry_id}_on_list" + @callback def async_update_ble_connection_limits(self, free: int, limit: int) -> None: """Update the BLE connection limits.""" @@ -133,7 +147,7 @@ class RuntimeEntryData: async_dispatcher_send(hass, signal) async def _ensure_platforms_loaded( - self, hass: HomeAssistant, entry: ConfigEntry, platforms: set[str] + self, hass: HomeAssistant, entry: ConfigEntry, platforms: set[Platform] ) -> None: async with self.platform_load_lock: needed = platforms - self.loaded_platforms @@ -147,6 +161,10 @@ class RuntimeEntryData: """Distribute an update of static infos to all platforms.""" # First, load all platforms needed_platforms = set() + + if async_get_dashboard(hass): + needed_platforms.add(Platform.UPDATE) + for info in infos: for info_type, platform in INFO_TYPE_TO_PLATFORM.items(): if isinstance(info, info_type): @@ -155,8 +173,7 @@ class RuntimeEntryData: await self._ensure_platforms_loaded(hass, entry, needed_platforms) # Then send dispatcher event - signal = f"esphome_{self.entry_id}_on_list" - async_dispatcher_send(hass, signal, infos) + async_dispatcher_send(hass, self.signal_static_info_updated, infos) @callback def async_subscribe_state_update( diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 76de857a863..880d94a5f55 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -118,7 +118,10 @@ def _color_mode_to_ha(mode: int) -> str: def _filter_color_modes( supported: list[int], features: LightColorCapability ) -> list[int]: - """Filter the given supported color modes, excluding all values that don't have the requested features.""" + """Filter the given supported color modes. + + Excluding all values that don't have the requested features. + """ return [mode for mode in supported if mode & features] @@ -161,7 +164,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): try_keep_current_mode = False if (rgbw_ha := kwargs.get(ATTR_RGBW_COLOR)) is not None: - # pylint: disable=invalid-name + # pylint: disable-next=invalid-name *rgb, w = tuple(x / 255 for x in rgbw_ha) # type: ignore[assignment] color_bri = max(rgb) # normalize rgb @@ -174,7 +177,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): try_keep_current_mode = False if (rgbww_ha := kwargs.get(ATTR_RGBWW_COLOR)) is not None: - # pylint: disable=invalid-name + # pylint: disable-next=invalid-name *rgb, cw, ww = tuple(x / 255 for x in rgbww_ha) # type: ignore[assignment] color_bri = max(rgb) # normalize rgb @@ -226,7 +229,8 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): if (white_ha := kwargs.get(ATTR_WHITE)) is not None: # ESPHome multiplies brightness and white together for final brightness - # HA only sends `white` in turn_on, and reads total brightness through brightness property + # HA only sends `white` in turn_on, and reads total brightness + # through brightness property. data["brightness"] = white_ha / 255 data["white"] = 1.0 color_modes = _filter_color_modes( @@ -244,8 +248,10 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): # if possible, stay with the color mode that is already set data["color_mode"] = self._state.color_mode else: - # otherwise try the color mode with the least complexity (fewest capabilities set) - # popcount with bin() function because it appears to be the best way: https://stackoverflow.com/a/9831671 + # otherwise try the color mode with the least complexity + # (fewest capabilities set) + # popcount with bin() function because it appears + # to be the best way: https://stackoverflow.com/a/9831671 color_modes.sort(key=lambda mode: bin(mode).count("1")) data["color_mode"] = color_modes[0] @@ -332,9 +338,9 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): @property @esphome_state_property - def color_temp(self) -> float | None: # type: ignore[override] + def color_temp(self) -> int: """Return the CT color value in mireds.""" - return self._state.color_temperature + return round(self._state.color_temperature) @property @esphome_state_property @@ -377,11 +383,11 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity): return self._static_info.effects @property - def min_mireds(self) -> float: # type: ignore[override] + def min_mireds(self) -> int: """Return the coldest color_temp that this light supports.""" - return self._static_info.min_mireds + return round(self._static_info.min_mireds) @property - def max_mireds(self) -> float: # type: ignore[override] + def max_mireds(self) -> int: """Return the warmest color_temp that this light supports.""" - return self._static_info.max_mireds + return round(self._static_info.max_mireds) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index ce3dc116715..2ae066266e9 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==13.0.2"], + "requirements": ["aioesphomeapi==13.1.0", "esphome-dashboard-api==1.2.3"], "zeroconf": ["_esphomelib._tcp.local."], "dhcp": [{ "registered_devices": true }], "codeowners": ["@OttoWinter", "@jesserockz"], diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index 7f90f4e27d8..f8566e863c6 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -17,6 +17,7 @@ from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, MediaPlayerState, + MediaType, async_process_play_media_url, ) from homeassistant.config_entries import ConfigEntry @@ -97,7 +98,7 @@ class EsphomeMediaPlayer( return flags async def async_play_media( - self, media_type: str, media_id: str, **kwargs: Any + self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Send the play command with media url to the media player.""" if media_source.is_media_source_id(media_id): diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 29c661f0984..282bcb1fbee 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -113,7 +113,8 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity): state_class == EsphomeSensorStateClass.MEASUREMENT and reset_type == LastResetType.AUTO ): - # Legacy, last_reset_type auto was the equivalent to the TOTAL_INCREASING state class + # Legacy, last_reset_type auto was the equivalent to the + # TOTAL_INCREASING state class return SensorStateClass.TOTAL_INCREASING return _STATE_CLASSES.from_esphome(self._static_info.state_class) diff --git a/homeassistant/components/esphome/strings.json b/homeassistant/components/esphome/strings.json index c403de8d881..ebbc97374c2 100644 --- a/homeassistant/components/esphome/strings.json +++ b/homeassistant/components/esphome/strings.json @@ -4,7 +4,8 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "mdns_missing_mac": "Missing MAC address in MDNS properties." + "mdns_missing_mac": "Missing MAC address in MDNS properties.", + "service_received": "Service received" }, "error": { "resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address", @@ -30,13 +31,13 @@ "data": { "noise_psk": "Encryption key" }, - "description": "Please enter the encryption key you set in your configuration for {name}." + "description": "Please enter the encryption key for {name}. You can find it in the ESPHome Dashboard or in your device configuration." }, "reauth_confirm": { "data": { "noise_psk": "Encryption key" }, - "description": "The ESPHome device {name} enabled transport encryption or changed the encryption key. Please enter the updated key." + "description": "The ESPHome device {name} enabled transport encryption or changed the encryption key. Please enter the updated key. You can find it in the ESPHome Dashboard or in your device configuration." }, "discovery_confirm": { "description": "Do you want to add the ESPHome node `{name}` to Home Assistant?", @@ -49,6 +50,10 @@ "ble_firmware_outdated": { "title": "Update {name} with ESPHome {version} or later", "description": "To improve Bluetooth reliability and performance, we highly recommend updating {name} with ESPHome {version} or later. When updating the device to ESPHome {version}, it is recommended to use a serial cable instead of an over-the-air update to take advantage of the new partition scheme." + }, + "api_password_deprecated": { + "title": "API Password deprecated on {name}", + "description": "The API password for ESPHome is deprecated and the use of an API encryption key is recommended instead.\n\nRemove the API password and add an encryption key to your ESPHome device to resolve this issue." } } } diff --git a/homeassistant/components/esphome/translations/ca.json b/homeassistant/components/esphome/translations/ca.json index fe5a4799616..8650ba426c1 100644 --- a/homeassistant/components/esphome/translations/ca.json +++ b/homeassistant/components/esphome/translations/ca.json @@ -4,7 +4,8 @@ "already_configured": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "mdns_missing_mac": "Falta l'adre\u00e7a MAC a les propietats MDNS.", - "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "service_received": "Servei rebut" }, "error": { "connection_error": "No s'ha pogut connectar amb ESP. Verifica que l'arxiu YAML cont\u00e9 la l\u00ednia 'api:'.", @@ -28,13 +29,13 @@ "data": { "noise_psk": "Clau de xifrat" }, - "description": "Introdueix la clau de xifrat de {name} establerta a la configuraci\u00f3." + "description": "Introdueix la clau de xifrat de {name}. La pots trobar al panell d'usuari d'ESPHome o a la configuraci\u00f3 del teu dispositiu." }, "reauth_confirm": { "data": { "noise_psk": "Clau de xifrat" }, - "description": "El dispositiu ESPHome {name} ha activat el xifratge de transport o ha canviat la clau de xifrat. Introdueix la clau actualitzada." + "description": "El dispositiu ESPHome {name} ha activat el transport xifrat o ha canviat la clau de xifrat. Introdueix la clau actualitzada. La pots trobar al panell d'usuari d'ESPHome o a la configuraci\u00f3 del teu dispositiu." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/de.json b/homeassistant/components/esphome/translations/de.json index 29f702c47b0..90e6bfe9612 100644 --- a/homeassistant/components/esphome/translations/de.json +++ b/homeassistant/components/esphome/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "mdns_missing_mac": "Fehlende MAC-Adresse in MDNS-Eigenschaften.", - "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "service_received": "Dienst erhalten" }, "error": { "connection_error": "Keine Verbindung zum ESP m\u00f6glich. Achte darauf, dass deine YAML-Datei eine Zeile 'api:' enth\u00e4lt.", @@ -28,13 +29,13 @@ "data": { "noise_psk": "Verschl\u00fcsselungsschl\u00fcssel" }, - "description": "Bitte gib den Verschl\u00fcsselungsschl\u00fcssel ein, den du in deiner Konfiguration f\u00fcr {name} festgelegt hast." + "description": "Bitte gib den Verschl\u00fcsselungsschl\u00fcssel f\u00fcr {name} ein. Du findest ihn im ESPHome Dashboard oder in deiner Ger\u00e4tekonfiguration." }, "reauth_confirm": { "data": { "noise_psk": "Verschl\u00fcsselungsschl\u00fcssel" }, - "description": "Das ESPHome-Ger\u00e4t {name} hat die Transportverschl\u00fcsselung aktiviert oder den Verschl\u00fcsselungscode ge\u00e4ndert. Bitte gib den aktualisierten Schl\u00fcssel ein." + "description": "Das ESPHome-Ger\u00e4t {name} hat die Transportverschl\u00fcsselung aktiviert oder den Verschl\u00fcsselungsschl\u00fcssel ge\u00e4ndert. Bitte gib den aktualisierten Schl\u00fcssel ein. Du findest ihn im ESPHome Dashboard oder in deiner Ger\u00e4tekonfiguration." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/el.json b/homeassistant/components/esphome/translations/el.json index b477a7fa485..5a8849e54d2 100644 --- a/homeassistant/components/esphome/translations/el.json +++ b/homeassistant/components/esphome/translations/el.json @@ -4,13 +4,14 @@ "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\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", "mdns_missing_mac": "\u039b\u03b5\u03af\u03c0\u03b5\u03b9 \u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 MAC \u03c3\u03c4\u03b9\u03c2 \u03b9\u03b4\u03b9\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 MDNS.", - "reauth_successful": "\u0397 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + "reauth_successful": "\u0397 \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "service_received": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03b5\u03bb\u03ae\u03c6\u03b8\u03b7" }, "error": { "connection_error": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf ESP. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf YAML \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03b3\u03c1\u03b1\u03bc\u03bc\u03ae \"api:\".", "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", "invalid_psk": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03b5\u03b9 \u03bc\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c0\u03bf\u03c5 \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2", - "resolve_error": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03c0\u03af\u03bb\u03c5\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 ESP. \u0395\u03ac\u03bd \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03c0\u03b9\u03bc\u03ad\u03bd\u03b5\u03b9, \u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c4\u03b1\u03c4\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03c0\u03af\u03bb\u03c5\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 ESP. \u0395\u03ac\u03bd \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03c0\u03b9\u03bc\u03ad\u03bd\u03b5\u03b9, \u03bf\u03c1\u03af\u03c3\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c4\u03b1\u03c4\u03b9\u03ba\u03ae \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" }, "flow_title": "{name}", "step": { @@ -28,27 +29,27 @@ "data": { "noise_psk": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2" }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \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}." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {name} . \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf\u03bd \u03a0\u03af\u03bd\u03b1\u03ba\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 ESPHome \u03ae \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03c3\u03b1\u03c2." }, "reauth_confirm": { "data": { "noise_psk": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2" }, - "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae ESPHome {name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b5 \u03c4\u03b7\u03bd \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 \u03ae \u03ac\u03bb\u03bb\u03b1\u03be\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2. \u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03bc\u03ad\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af." + "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae ESPHome {name} \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b5 \u03c4\u03b7\u03bd \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03bc\u03b5\u03c4\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 \u03ae \u03ac\u03bb\u03bb\u03b1\u03be\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2. \u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03bc\u03ad\u03bd\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af. \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03c4\u03bf \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf\u03bd \u03a0\u03af\u03bd\u03b1\u03ba\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 ESPHome \u03ae \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03c3\u03b1\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" }, - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03cc\u03bc\u03b2\u03bf\u03c5 [ESPHome](https://esphomelib.com/)." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03cc\u03bc\u03b2\u03bf\u03c5 [ESPHome]( {esphome_url} ) \u03c3\u03b1\u03c2." } } }, "issues": { "ble_firmware_outdated": { - "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03b2\u03b5\u03bb\u03c4\u03b9\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03be\u03b9\u03bf\u03c0\u03b9\u03c3\u03c4\u03af\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7\u03bd \u03b1\u03c0\u03cc\u03b4\u03bf\u03c3\u03b7 \u03c4\u03bf\u03c5 Bluetooth, \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03bf\u03cd\u03bc\u03b5 \u03b1\u03bd\u03b5\u03c0\u03b9\u03c6\u03cd\u03bb\u03b1\u03ba\u03c4\u03b1 \u03c4\u03b7\u03bd \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 {name} \u03bc\u03b5 ESPHome 2022.11.0 \u03ae \u03bd\u03b5\u03cc\u03c4\u03b5\u03c1\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7.", - "title": "\u0395\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 {name} \u03bc\u03b5 ESPHome 2022.11.0 \u03ae \u03bd\u03b5\u03cc\u03c4\u03b5\u03c1\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7" + "description": "\u0393\u03b9\u03b1 \u03bd\u03b1 \u03b2\u03b5\u03bb\u03c4\u03b9\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03be\u03b9\u03bf\u03c0\u03b9\u03c3\u03c4\u03af\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03b7\u03bd \u03b1\u03c0\u03cc\u03b4\u03bf\u03c3\u03b7 \u03c4\u03bf\u03c5 Bluetooth, \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03bf\u03cd\u03bc\u03b5 \u03b1\u03bd\u03b5\u03c0\u03b9\u03c6\u03cd\u03bb\u03b1\u03ba\u03c4\u03b1 \u03c4\u03b7\u03bd \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 {name} \u03bc\u03b5 ESPHome {version} \u03ae \u03bd\u03b5\u03cc\u03c4\u03b5\u03c1\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7. \u039a\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03c3\u03b5 ESPHome {version} , \u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b7 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03bf\u03cd \u03ba\u03b1\u03bb\u03c9\u03b4\u03af\u03bf\u03c5 \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 over-the-air \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03c9\u03c6\u03b5\u03bb\u03b7\u03b8\u03b5\u03af\u03c4\u03b5 \u03b1\u03c0\u03cc \u03c4\u03bf \u03bd\u03ad\u03bf \u03c3\u03c7\u03ae\u03bc\u03b1 \u03b4\u03b9\u03b1\u03bc\u03b5\u03c1\u03b9\u03c3\u03bc\u03ac\u03c4\u03c9\u03bd.", + "title": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03bf {name} \u03bc\u03b5 ESPHome {version} \u03ae \u03bd\u03b5\u03cc\u03c4\u03b5\u03c1\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7" } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/en.json b/homeassistant/components/esphome/translations/en.json index 63fc3ed0573..b11792dcd78 100644 --- a/homeassistant/components/esphome/translations/en.json +++ b/homeassistant/components/esphome/translations/en.json @@ -4,7 +4,8 @@ "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "mdns_missing_mac": "Missing MAC address in MDNS properties.", - "reauth_successful": "Re-authentication was successful" + "reauth_successful": "Re-authentication was successful", + "service_received": "Service received" }, "error": { "connection_error": "Can't connect to ESP. Please make sure your YAML file contains an 'api:' line.", @@ -28,13 +29,13 @@ "data": { "noise_psk": "Encryption key" }, - "description": "Please enter the encryption key you set in your configuration for {name}." + "description": "Please enter the encryption key for {name}. You can find it in the ESPHome Dashboard or in your device configuration." }, "reauth_confirm": { "data": { "noise_psk": "Encryption key" }, - "description": "The ESPHome device {name} enabled transport encryption or changed the encryption key. Please enter the updated key." + "description": "The ESPHome device {name} enabled transport encryption or changed the encryption key. Please enter the updated key. You can find it in the ESPHome Dashboard or in your device configuration." }, "user": { "data": { @@ -46,6 +47,10 @@ } }, "issues": { + "api_password_deprecated": { + "description": "The API password for ESPHome is deprecated and the use of an API encryption key is recommended instead.\n\nRemove the API password and add an encryption key to your ESPHome device to resolve this issue.", + "title": "API Password deprecated on {name}" + }, "ble_firmware_outdated": { "description": "To improve Bluetooth reliability and performance, we highly recommend updating {name} with ESPHome {version} or later. When updating the device to ESPHome {version}, it is recommended to use a serial cable instead of an over-the-air update to take advantage of the new partition scheme.", "title": "Update {name} with ESPHome {version} or later" diff --git a/homeassistant/components/esphome/translations/es.json b/homeassistant/components/esphome/translations/es.json index 0e6f96a2803..2165ae12d72 100644 --- a/homeassistant/components/esphome/translations/es.json +++ b/homeassistant/components/esphome/translations/es.json @@ -4,7 +4,8 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", "mdns_missing_mac": "Falta la direcci\u00f3n MAC en las propiedades de mDNS.", - "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", + "service_received": "Servicio recibido" }, "error": { "connection_error": "No se puede conectar a ESP. Por favor, aseg\u00farate de que tu archivo YAML contiene una l\u00ednea 'api:'.", @@ -28,13 +29,13 @@ "data": { "noise_psk": "Clave de cifrado" }, - "description": "Por favor, introduce la clave de cifrado que estableciste en tu configuraci\u00f3n para {name}." + "description": "Por favor, introduce la clave de cifrado para {name}. Puedes encontrarla en el panel de ESPHome o en la configuraci\u00f3n de tu dispositivo." }, "reauth_confirm": { "data": { "noise_psk": "Clave de cifrado" }, - "description": "El dispositivo ESPHome {name} habilit\u00f3 el cifrado de transporte o cambi\u00f3 la clave de cifrado. Por favor, introduce la clave actualizada." + "description": "El dispositivo ESPHome {name} habilit\u00f3 el cifrado de transporte o cambi\u00f3 la clave de cifrado. Por favor, introduce la clave actualizada. Puedes encontrarla en el panel de ESPHome o en la configuraci\u00f3n de tu dispositivo." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/et.json b/homeassistant/components/esphome/translations/et.json index 4e7304ac45c..a12544ef053 100644 --- a/homeassistant/components/esphome/translations/et.json +++ b/homeassistant/components/esphome/translations/et.json @@ -4,7 +4,8 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Seadistamine on juba k\u00e4imas", "mdns_missing_mac": "MDNS-i atribuutides puudub MAC-aadress.", - "reauth_successful": "Taastuvastamine \u00f5nnestus" + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "service_received": "Saadud teenus" }, "error": { "connection_error": "ESP-ga ei saa \u00fchendust luua. Veendu, et YAML-fail sisaldab rida 'api:'.", @@ -28,13 +29,13 @@ "data": { "noise_psk": "Kr\u00fcptimisv\u00f5ti" }, - "description": "Sisesta kr\u00fcptimisv\u00f5ti mille m\u00e4\u00e4rasid oma {name} seadetes." + "description": "Sisesta {name} kr\u00fcpteerimisv\u00f5ti. Leiad selle ESPHome'i juhtpaneelilt v\u00f5i oma seadme konfiguratsioonist." }, "reauth_confirm": { "data": { "noise_psk": "Kr\u00fcptimisv\u00f5ti" }, - "description": "ESPHome seade {name} lubas \u00fclekande kr\u00fcptimise v\u00f5i muutis kr\u00fcpteerimisv\u00f5tit. Palun sisesta uuendatud v\u00f5ti." + "description": "ESPHome seade {name} lubas transpordi kr\u00fcpteerimise v\u00f5i muutis kr\u00fcpteerimisv\u00f5tit. Sisesta uuendatud v\u00f5ti. Selle leiad ESPHome'i juhtpaneelilt v\u00f5i seadme konfiguratsioonist." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/hu.json b/homeassistant/components/esphome/translations/hu.json index 62b71d79c04..e1febcbb93d 100644 --- a/homeassistant/components/esphome/translations/hu.json +++ b/homeassistant/components/esphome/translations/hu.json @@ -4,7 +4,8 @@ "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", "mdns_missing_mac": "Hi\u00e1nyz\u00f3 MAC-c\u00edm az MDNS-tulajdons\u00e1gokban.", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "service_received": "Szolg\u00e1ltat\u00e1s meg\u00e9rkezett" }, "error": { "connection_error": "Nem lehet csatlakozni az ESP-hez. K\u00e9rem, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy a YAML konfigur\u00e1ci\u00f3 tartalmaz egy \"api:\" sort.", @@ -28,13 +29,13 @@ "data": { "noise_psk": "Titkos\u00edt\u00e1si kulcs" }, - "description": "K\u00e9rem, adja meg a bekonfigur\u00e1lt titkos\u00edt\u00e1si kulcsot: {name}" + "description": "K\u00e9rem, adja meg a {name} titkos\u00edt\u00e1si kulcs\u00e1t, amit az ESPHome Dashboardon vagy az eszk\u00f6z konfigur\u00e1ci\u00f3j\u00e1ban tal\u00e1l." }, "reauth_confirm": { "data": { "noise_psk": "Titkos\u00edt\u00e1si kulcs" }, - "description": "{name} ESPHome v\u00e9gpont aktiv\u00e1lta az adat\u00e1tviteli titkos\u00edt\u00e1st vagy megv\u00e1ltoztatta a titkos\u00edt\u00e1si kulcsot. K\u00e9rem, adja meg az aktu\u00e1lis titkos\u00edt\u00e1si kulcsot." + "description": "Az ESPHome {name} eszk\u00f6z\u00f6n be lett \u00e1ll\u00edtva az adat\u00e1tviteli titkos\u00edt\u00e1s, vagy meg lett v\u00e1ltoztatva a titkos\u00edt\u00e1si kulcs. K\u00e9rem, adja meg az \u00e9rv\u00e9nyes kulcsot. Ezt az ESPHome Dashboardon vagy az eszk\u00f6z konfigur\u00e1ci\u00f3j\u00e1ban tal\u00e1lja meg." }, "user": { "data": { @@ -48,7 +49,7 @@ "issues": { "ble_firmware_outdated": { "description": "A Bluetooth megb\u00edzhat\u00f3s\u00e1g\u00e1nak \u00e9s teljes\u00edtm\u00e9ny\u00e9nek jav\u00edt\u00e1sa \u00e9rdek\u00e9ben javasoljuk {name} v\u00e9gpont friss\u00edt\u00e9s\u00e9t az ESPHome 2022.11.0 vagy \u00fajabb verzi\u00f3ra.", - "title": "{name} v\u00e9gpont friss\u00edt\u00e9se ESPHome 2022.11.0 vagy \u00fajabb verzi\u00f3j\u00e1val" + "title": "Friss\u00edtse a {name} v\u00e9gpontot ESPHome {version} vagy \u00fajabb verzi\u00f3j\u00e1val" } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/id.json b/homeassistant/components/esphome/translations/id.json index d1b61034c4b..b2f59657f37 100644 --- a/homeassistant/components/esphome/translations/id.json +++ b/homeassistant/components/esphome/translations/id.json @@ -4,7 +4,8 @@ "already_configured": "Perangkat sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", "mdns_missing_mac": "Alamat MAC tidak ada dalam properti MDNS.", - "reauth_successful": "Autentikasi ulang berhasil" + "reauth_successful": "Autentikasi ulang berhasil", + "service_received": "Layanan diterima" }, "error": { "connection_error": "Tidak dapat terhubung ke ESP. Pastikan file YAML Anda mengandung baris 'api:'.", @@ -28,13 +29,13 @@ "data": { "noise_psk": "Kunci enkripsi" }, - "description": "Masukkan kunci enkripsi yang Anda atur dalam konfigurasi Anda untuk {name}." + "description": "Masukkan kunci enkripsi untuk {name}. Anda dapat menemukannya di Dasbor ESPHome atau di konfigurasi perangkat Anda." }, "reauth_confirm": { "data": { "noise_psk": "Kunci enkripsi" }, - "description": "Perangkat ESPHome {name} mengaktifkan enkripsi transport atau telah mengubah kunci enkripsi. Masukkan kunci yang diperbarui." + "description": "Perangkat ESPHome {name} mengaktifkan enkripsi transport atau mengubah kunci enkripsi. Masukkan kunci yang diperbarui. Anda dapat menemukannya di Dasbor ESPHome atau di konfigurasi perangkat Anda." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/it.json b/homeassistant/components/esphome/translations/it.json index 627f172a136..d1d84f8aa0d 100644 --- a/homeassistant/components/esphome/translations/it.json +++ b/homeassistant/components/esphome/translations/it.json @@ -4,7 +4,8 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "mdns_missing_mac": "Indirizzo MAC mancante nelle propriet\u00e0 MDNS.", - "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "service_received": "Servizio ricevuto" }, "error": { "connection_error": "Impossibile connettersi a ESP. Assicurati che il tuo file YAML contenga una riga \"api:\".", @@ -28,13 +29,13 @@ "data": { "noise_psk": "Chiave di cifratura" }, - "description": "Inserisci la chiave di cifratura che hai impostato nella configurazione per {name}." + "description": "Inserisci la chiave di crittografia per {name}. Puoi trovarlo nella plancia di ESPHome o nella configurazione del tuo dispositivo." }, "reauth_confirm": { "data": { "noise_psk": "Chiave di cifratura" }, - "description": "Il dispositivo ESPHome {name} ha abilitato la cifratura del trasporto o ha modificato la chiave di cifratura. Inserisci la chiave aggiornata." + "description": "Il dispositivo ESPHome {name} ha abilitato la crittografia del trasporto o ha modificato la chiave di crittografia. Inserisci la chiave aggiornata. Puoi trovarlo nella plancia di ESPHome o nella configurazione del tuo dispositivo." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/lt.json b/homeassistant/components/esphome/translations/lt.json new file mode 100644 index 00000000000..9fcd4994865 --- /dev/null +++ b/homeassistant/components/esphome/translations/lt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u012erenginys jau sukonfig\u016bruotas" + }, + "step": { + "authenticate": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/lv.json b/homeassistant/components/esphome/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/esphome/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/nl.json b/homeassistant/components/esphome/translations/nl.json index 410850916ea..6581a127d41 100644 --- a/homeassistant/components/esphome/translations/nl.json +++ b/homeassistant/components/esphome/translations/nl.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "already_in_progress": "De configuratie is momenteel al bezig", - "reauth_successful": "Herauthenticatie geslaagd" + "reauth_successful": "Herauthenticatie geslaagd", + "service_received": "Service ontvangen" }, "error": { "connection_error": "Kan geen verbinding maken met ESP. Zorg ervoor dat uw YAML-bestand een regel 'api:' bevat.", diff --git a/homeassistant/components/esphome/translations/no.json b/homeassistant/components/esphome/translations/no.json index 061855915ce..0e605aba675 100644 --- a/homeassistant/components/esphome/translations/no.json +++ b/homeassistant/components/esphome/translations/no.json @@ -4,7 +4,8 @@ "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "mdns_missing_mac": "Manglende MAC-adresse i MDNS-egenskaper.", - "reauth_successful": "Re-autentisering var vellykket" + "reauth_successful": "Re-autentisering var vellykket", + "service_received": "Service mottatt" }, "error": { "connection_error": "Kan ikke koble til ESP. Kontroller at YAML filen din inneholder en \"api:\" linje.", @@ -28,13 +29,13 @@ "data": { "noise_psk": "Krypteringsn\u00f8kkel" }, - "description": "Skriv inn krypteringsn\u00f8kkelen du angav i konfigurasjonen for {name} ." + "description": "Skriv inn krypteringsn\u00f8kkelen for {name} . Du finner den i ESPHome-dashbordet eller i enhetskonfigurasjonen." }, "reauth_confirm": { "data": { "noise_psk": "Krypteringsn\u00f8kkel" }, - "description": "ESPHome -enheten {name} aktiverte transportkryptering eller endret krypteringsn\u00f8kkelen. Skriv inn den oppdaterte n\u00f8kkelen." + "description": "ESPHome-enheten {name} aktiverte transportkryptering eller endret krypteringsn\u00f8kkelen. Vennligst skriv inn den oppdaterte n\u00f8kkelen. Du finner den i ESPHome-dashbordet eller i enhetskonfigurasjonen." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/pl.json b/homeassistant/components/esphome/translations/pl.json index ab0f43c6969..05ce0bf4d03 100644 --- a/homeassistant/components/esphome/translations/pl.json +++ b/homeassistant/components/esphome/translations/pl.json @@ -4,7 +4,8 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", "mdns_missing_mac": "Brak adresu MAC we w\u0142a\u015bciwo\u015bciach MDNS.", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "service_received": "Us\u0142uga otrzymana" }, "error": { "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z ESP. Upewnij si\u0119, \u017ce Tw\u00f3j plik YAML zawiera lini\u0119 'api:'.", @@ -28,13 +29,13 @@ "data": { "noise_psk": "Klucz szyfruj\u0105cy" }, - "description": "Wprowad\u017a klucz szyfruj\u0105cy ustawiony w konfiguracji dla {name}." + "description": "Wprowad\u017a klucz szyfrowania dla {name} . Mo\u017cesz go znale\u017a\u0107 w dashboardzie ESPHome lub w konfiguracji swojego urz\u0105dzenia." }, "reauth_confirm": { "data": { "noise_psk": "Klucz szyfruj\u0105cy" }, - "description": "Urz\u0105dzenie ESPHome {name} w\u0142\u0105czy\u0142o szyfrowanie transportu lub zmieni\u0142o klucz szyfruj\u0105cy. Wprowad\u017a zaktualizowany klucz." + "description": "Urz\u0105dzenie ESPHome {name} w\u0142\u0105czy\u0142o szyfrowanie transportu lub zmieni\u0142o klucz szyfruj\u0105cy. Wprowad\u017a zaktualizowany klucz. Mo\u017cesz go znale\u017a\u0107 w dashboardzie ESPHome lub w konfiguracji swojego urz\u0105dzenia." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/pt-BR.json b/homeassistant/components/esphome/translations/pt-BR.json index e0ad60b490b..14d0e7cebed 100644 --- a/homeassistant/components/esphome/translations/pt-BR.json +++ b/homeassistant/components/esphome/translations/pt-BR.json @@ -4,7 +4,8 @@ "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O fluxo de configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", "mdns_missing_mac": "Endere\u00e7o MAC ausente nas propriedades MDNS.", - "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "service_received": "Servi\u00e7o recebido" }, "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:'.", @@ -28,13 +29,13 @@ "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} ." + "description": "Insira a chave de criptografia para {name}. Voc\u00ea pode encontr\u00e1-lo no ESPHome Dashboard ou na configura\u00e7\u00e3o do seu dispositivo." }, "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." + "description": "O dispositivo ESPHome {name} ativou a criptografia de transporte ou alterou a chave de criptografia. Insira a chave atualizada. Voc\u00ea pode encontr\u00e1-lo no ESPHome Dashboard ou na configura\u00e7\u00e3o do seu dispositivo." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/pt.json b/homeassistant/components/esphome/translations/pt.json index aca210dbdee..1bcd3e549bc 100644 --- a/homeassistant/components/esphome/translations/pt.json +++ b/homeassistant/components/esphome/translations/pt.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", - "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", + "service_received": "Servi\u00e7o recebido" }, "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:'.", diff --git a/homeassistant/components/esphome/translations/ru.json b/homeassistant/components/esphome/translations/ru.json index 937857ac9a2..fbacebf20a7 100644 --- a/homeassistant/components/esphome/translations/ru.json +++ b/homeassistant/components/esphome/translations/ru.json @@ -4,7 +4,8 @@ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "mdns_missing_mac": "\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 MAC-\u0430\u0434\u0440\u0435\u0441 \u0432 \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430\u0445 MDNS.", - "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_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.", + "service_received": "\u0423\u0441\u043b\u0443\u0433\u0430 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0430." }, "error": { "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", @@ -28,13 +29,13 @@ "data": { "noise_psk": "\u041a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f, \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 {name}." + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0434\u043b\u044f {name}. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0439\u0442\u0438 \u0435\u0433\u043e \u0432 ESPHome Dashboard \u0438\u043b\u0438 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." }, "reauth_confirm": { "data": { "noise_psk": "\u041a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f" }, - "description": "\u0414\u043b\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 {name} \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u043d\u043e \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u043e\u0433\u043e \u0443\u0440\u043e\u0432\u043d\u044f \u0438\u043b\u0438 \u0438\u0437\u043c\u0435\u043d\u0451\u043d \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044b\u0439 \u043a\u043b\u044e\u0447." + "description": "\u041d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435 ESPHome {\u0438\u043c\u044f} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u043e\u0433\u043e \u0443\u0440\u043e\u0432\u043d\u044f \u0438\u043b\u0438 \u0438\u0437\u043c\u0435\u043d\u0438\u043b\u0441\u044f \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u0439\u0442\u0438 \u043a\u043b\u044e\u0447 \u0432 ESPHome Dashboard \u0438\u043b\u0438 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/sk.json b/homeassistant/components/esphome/translations/sk.json index e222676e018..893990a545d 100644 --- a/homeassistant/components/esphome/translations/sk.json +++ b/homeassistant/components/esphome/translations/sk.json @@ -4,10 +4,11 @@ "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", "mdns_missing_mac": "Ch\u00fdba MAC adresa vo vlastnostiach MDNS.", - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "service_received": "Slu\u017eba prijat\u00e1" }, "error": { - "connection_error": "Ned\u00e1 sa pripoji\u0165 k ESP. Uistite sa, \u017ee v\u00e1\u0161 s\u00fabor YAML obsahuje riadok \u201eapi:\u201c.", + "connection_error": "Ned\u00e1 sa pripoji\u0165 k ESP. Uistite sa, \u017ee v\u00e1\u0161 s\u00fabor YAML obsahuje riadok 'api:'.", "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" @@ -28,13 +29,13 @@ "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}." + "description": "Zadajte \u0161ifrovac\u00ed k\u013e\u00fa\u010d pre {name}. N\u00e1jdete ho v ESPHome Dashboard alebo v konfigur\u00e1cii v\u00e1\u0161ho zariadenia." }, "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." + "description": "Zariadenie ESPHome {name} povolilo prenosov\u00e9 \u0161ifrovanie alebo zmenilo \u0161ifrovac\u00ed k\u013e\u00fa\u010d. Zadajte aktualizovan\u00fd k\u013e\u00fa\u010d. N\u00e1jdete ho v ESPHome Dashboard alebo v konfigur\u00e1cii v\u00e1\u0161ho zariadenia." }, "user": { "data": { diff --git a/homeassistant/components/esphome/translations/tr.json b/homeassistant/components/esphome/translations/tr.json index d4388dde8b8..a22858dc185 100644 --- a/homeassistant/components/esphome/translations/tr.json +++ b/homeassistant/components/esphome/translations/tr.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", - "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + "mdns_missing_mac": "MDNS \u00f6zelliklerinde eksik MAC adresi.", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "service_received": "Hizmet al\u0131nd\u0131" }, "error": { "connection_error": "ESP'ye ba\u011flan\u0131lam\u0131yor. L\u00fctfen YAML dosyan\u0131z\u0131n bir 'api:' sat\u0131r\u0131 i\u00e7erdi\u011finden emin olun.", @@ -27,13 +29,13 @@ "data": { "noise_psk": "\u015eifreleme anahtar\u0131" }, - "description": "{name} i\u00e7in yap\u0131land\u0131rman\u0131zda belirledi\u011finiz \u015fifreleme anahtar\u0131n\u0131 girin." + "description": "L\u00fctfen {name} i\u00e7in \u015fifreleme anahtar\u0131n\u0131 girin. Bunu ESPHome Dashboard'da veya cihaz yap\u0131land\u0131rman\u0131zda bulabilirsiniz." }, "reauth_confirm": { "data": { "noise_psk": "\u015eifreleme anahtar\u0131" }, - "description": "ESPHome cihaz\u0131 {name} aktar\u0131m \u015fifrelemesini etkinle\u015ftirdi veya \u015fifreleme anahtar\u0131n\u0131 de\u011fi\u015ftirdi. L\u00fctfen g\u00fcncellenmi\u015f anahtar\u0131 girin." + "description": "{name} ESPHome cihaz\u0131 aktar\u0131m \u015fifrelemesini etkinle\u015ftirdi veya \u015fifreleme anahtar\u0131n\u0131 de\u011fi\u015ftirdi. L\u00fctfen g\u00fcncellenmi\u015f anahtar\u0131 girin. Bunu ESPHome Dashboard'da veya cihaz yap\u0131land\u0131rman\u0131zda bulabilirsiniz." }, "user": { "data": { @@ -43,5 +45,11 @@ "description": "L\u00fctfen [ESPHome]( {esphome_url} ) d\u00fc\u011f\u00fcm\u00fcn\u00fcz\u00fcn ba\u011flant\u0131 ayarlar\u0131n\u0131 girin." } } + }, + "issues": { + "ble_firmware_outdated": { + "description": "Bluetooth g\u00fcvenilirli\u011fini ve performans\u0131n\u0131 art\u0131rmak i\u00e7in {name} \u00fcr\u00fcn\u00fcn\u00fc ESPHome {version} veya sonraki bir s\u00fcr\u00fcmle g\u00fcncellemenizi \u00f6nemle tavsiye ederiz. Cihaz\u0131 ESPHome {version} olarak g\u00fcncellerken, yeni b\u00f6l\u00fcm \u015femas\u0131ndan yararlanmak i\u00e7in kablosuz g\u00fcncelleme yerine seri kablo kullan\u0131lmas\u0131 \u00f6nerilir.", + "title": "{name} \u00fcr\u00fcn\u00fcn\u00fc ESPHome {version} veya sonraki bir s\u00fcr\u00fcmle g\u00fcncelleyin" + } } } \ No newline at end of file diff --git a/homeassistant/components/esphome/translations/zh-Hant.json b/homeassistant/components/esphome/translations/zh-Hant.json index 4de09120be0..78a4bc7d8dd 100644 --- a/homeassistant/components/esphome/translations/zh-Hant.json +++ b/homeassistant/components/esphome/translations/zh-Hant.json @@ -4,7 +4,8 @@ "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "mdns_missing_mac": "MDNS \u5c6c\u6027\u4e2d\u7f3a\u5c11 MAC \u4f4d\u5740\u3002", - "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "service_received": "\u6536\u5230\u670d\u52d9" }, "error": { "connection_error": "\u7121\u6cd5\u9023\u7dda\u81f3 ESP\uff0c\u8acb\u78ba\u5b9a\u60a8\u7684 YAML \u6a94\u6848\u5305\u542b\u300capi:\u300d\u8a2d\u5b9a\u5217\u3002", @@ -28,13 +29,13 @@ "data": { "noise_psk": "\u91d1\u9470" }, - "description": "\u8acb\u8f38\u5165 {name} \u8a2d\u5b9a\u4e2d\u6240\u8a2d\u5b9a\u4e4b\u91d1\u9470\u3002" + "description": "\u8acb\u8f38\u5165 {name} \u8a2d\u5b9a\u4e2d\u6240\u8a2d\u5b9a\u4e4b\u91d1\u9470\u3002\u53ef\u4ee5\u65bc ESPHome \u4e3b\u9762\u677f\u6216\u88dd\u7f6e\u8a2d\u5b9a\u4e2d\u627e\u5230\u6b64\u8cc7\u8a0a\u3002" }, "reauth_confirm": { "data": { "noise_psk": "\u91d1\u9470" }, - "description": "ESPHome \u88dd\u7f6e {name} \u5df2\u958b\u555f\u50b3\u8f38\u52a0\u5bc6\u6216\u8b8a\u66f4\u91d1\u9470\u3002\u8acb\u8f38\u5165\u66f4\u65b0\u91d1\u9470\u3002" + "description": "ESPHome \u88dd\u7f6e {name} \u5df2\u958b\u555f\u50b3\u8f38\u52a0\u5bc6\u6216\u8b8a\u66f4\u91d1\u9470\u3002\u8acb\u8f38\u5165\u66f4\u65b0\u91d1\u9470\u3002\u53ef\u4ee5\u65bc ESPHome \u4e3b\u9762\u677f\u6216\u88dd\u7f6e\u8a2d\u5b9a\u4e2d\u627e\u5230\u6b64\u8cc7\u8a0a\u3002" }, "user": { "data": { @@ -47,7 +48,7 @@ }, "issues": { "ble_firmware_outdated": { - "description": "\u6b32\u6539\u5584\u85cd\u82bd\u53ef\u9760\u6027\u8207\u6548\u80fd\uff0c\u5f37\u70c8\u5efa\u8b70\u60a8\u66f4\u65b0\u81f3{version} \u7248\u6216\u66f4\u65b0\u7248\u672c ESPHome \u4e4b {name}\u3002\u7576\u9032\u884c\u66f4\u65b0\u81f3 ESPHome {version} \u7248\u6642\uff0c\u5efa\u8b70\u4f7f\u7528\u5e8f\u5217\u7dda\u3001\u800c\u975e\u4f7f\u7528\u7121\u7dda\u7db2\u8def\u4ee5\u4f7f\u7528\u65b0\u5206\u5340\u7684\u65b0\u529f\u80fd\u3002", + "description": "\u70ba\u4e86\u6539\u5584\u85cd\u82bd\u53ef\u9760\u6027\u8207\u6548\u80fd\uff0c\u5f37\u70c8\u5efa\u8b70\u60a8\u5c07 {name} \u4e4b ESPHome \u66f4\u65b0\u81f3{version} \u7248\u6216\u66f4\u65b0\u7248\u672c\u3002\u5efa\u8b70\u4f7f\u7528\u5e8f\u5217\u7dda\u9032\u884c\u66f4\u65b0 ESPHome {version} \u7248\uff0c\u800c\u975e\u4f7f\u7528\u7121\u7dda\u7db2\u8def\u4ee5\u4f7f\u7528\u65b0\u7684\u5206\u5340\u529f\u80fd\u3002", "title": "\u66f4\u65b0\u81f3 {version} \u7248\u6216\u66f4\u65b0\u7248\u672c ESPHome \u4e4b {name}" } } diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py new file mode 100644 index 00000000000..7139c9e937f --- /dev/null +++ b/homeassistant/components/esphome/update.py @@ -0,0 +1,153 @@ +"""Update platform for ESPHome.""" +from __future__ import annotations + +import asyncio +import logging +from typing import Any, cast + +from aioesphomeapi import DeviceInfo as ESPHomeDeviceInfo, EntityInfo + +from homeassistant.components.update import ( + UpdateDeviceClass, + UpdateEntity, + UpdateEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .dashboard import ESPHomeDashboard, async_get_dashboard +from .domain_data import DomainData +from .entry_data import RuntimeEntryData + +KEY_UPDATE_LOCK = "esphome_update_lock" + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ESPHome update based on a config entry.""" + dashboard = async_get_dashboard(hass) + + if dashboard is None: + return + + entry_data = DomainData.get(hass).get_entry_data(entry) + unsub = None + + async def setup_update_entity() -> None: + """Set up the update entity.""" + nonlocal unsub + + # Keep listening until device is available + if not entry_data.available: + return + + if unsub is not None: + unsub() # type: ignore[unreachable] + + assert dashboard is not None + async_add_entities([ESPHomeUpdateEntity(entry_data, dashboard)]) + + if entry_data.available: + await setup_update_entity() + return + + signal = f"esphome_{entry_data.entry_id}_on_device_update" + unsub = async_dispatcher_connect(hass, signal, setup_update_entity) + + +class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): + """Defines an ESPHome update entity.""" + + _attr_has_entity_name = True + _attr_device_class = UpdateDeviceClass.FIRMWARE + _attr_title = "ESPHome" + _attr_name = "Firmware" + + def __init__( + self, entry_data: RuntimeEntryData, coordinator: ESPHomeDashboard + ) -> None: + """Initialize the update entity.""" + super().__init__(coordinator=coordinator) + assert entry_data.device_info is not None + self._entry_data = entry_data + self._attr_unique_id = entry_data.device_info.mac_address + self._attr_device_info = DeviceInfo( + connections={ + (dr.CONNECTION_NETWORK_MAC, entry_data.device_info.mac_address) + } + ) + if coordinator.supports_update: + self._attr_supported_features = UpdateEntityFeature.INSTALL + + @property + def _device_info(self) -> ESPHomeDeviceInfo: + """Return the device info.""" + assert self._entry_data.device_info is not None + return self._entry_data.device_info + + @property + def available(self) -> bool: + """Return if update is available.""" + return super().available and self._device_info.name in self.coordinator.data + + @property + def installed_version(self) -> str | None: + """Version currently installed and in use.""" + return self._device_info.esphome_version + + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + device = self.coordinator.data.get(self._device_info.name) + if device is None: + return None + return cast(str, device["current_version"]) + + @property + def release_url(self) -> str | None: + """URL to the full release notes of the latest version available.""" + return "https://esphome.io/changelog/" + + async def async_added_to_hass(self) -> None: + """Handle entity added to Home Assistant.""" + await super().async_added_to_hass() + + @callback + def _static_info_updated(infos: list[EntityInfo]) -> None: + """Handle static info update.""" + self.async_write_ha_state() + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self._entry_data.signal_static_info_updated, + _static_info_updated, + ) + ) + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update.""" + async with self.hass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock()): + device = self.coordinator.data.get(self._device_info.name) + assert device is not None + if not await self.coordinator.api.compile(device["configuration"]): + logging.getLogger(__name__).error( + "Error compiling %s. Try again in ESPHome dashboard for error", + device["configuration"], + ) + if not await self.coordinator.api.upload(device["configuration"], "OTA"): + logging.getLogger(__name__).error( + "Error OTA updating %s. Try again in ESPHome dashboard for error", + device["configuration"], + ) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/eufy/__init__.py b/homeassistant/components/eufy/__init__.py index f2198dc7046..52d6fead3eb 100644 --- a/homeassistant/components/eufy/__init__.py +++ b/homeassistant/components/eufy/__init__.py @@ -1,4 +1,4 @@ -"""Support for Eufy devices.""" +"""Support for EufyHome devices.""" import lakeside import voluptuous as vol @@ -55,7 +55,7 @@ PLATFORMS = { def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up Eufy devices.""" + """Set up EufyHome devices.""" if CONF_USERNAME in config[DOMAIN] and CONF_PASSWORD in config[DOMAIN]: data = lakeside.get_devices( diff --git a/homeassistant/components/eufy/light.py b/homeassistant/components/eufy/light.py index f3d5fe58e7d..625b5cda0ba 100644 --- a/homeassistant/components/eufy/light.py +++ b/homeassistant/components/eufy/light.py @@ -1,4 +1,4 @@ -"""Support for Eufy lights.""" +"""Support for EufyHome lights.""" from __future__ import annotations from typing import Any @@ -21,8 +21,8 @@ from homeassistant.util.color import ( color_temperature_mired_to_kelvin as mired_to_kelvin, ) -EUFY_MAX_KELVIN = 6500 -EUFY_MIN_KELVIN = 2700 +EUFYHOME_MAX_KELVIN = 6500 +EUFYHOME_MIN_KELVIN = 2700 def setup_platform( @@ -31,14 +31,14 @@ def setup_platform( add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up Eufy bulbs.""" + """Set up EufyHome bulbs.""" if discovery_info is None: return - add_entities([EufyLight(discovery_info)], True) + add_entities([EufyHomeLight(discovery_info)], True) -class EufyLight(LightEntity): - """Representation of a Eufy light.""" +class EufyHomeLight(LightEntity): + """Representation of a EufyHome light.""" def __init__(self, device): """Initialize the light.""" @@ -97,18 +97,19 @@ class EufyLight(LightEntity): @property def min_mireds(self) -> int: """Return minimum supported color temperature.""" - return kelvin_to_mired(EUFY_MAX_KELVIN) + return kelvin_to_mired(EUFYHOME_MAX_KELVIN) @property def max_mireds(self) -> int: """Return maximum supported color temperature.""" - return kelvin_to_mired(EUFY_MIN_KELVIN) + return kelvin_to_mired(EUFYHOME_MIN_KELVIN) @property def color_temp(self): """Return the color temperature of this light.""" temp_in_k = int( - EUFY_MIN_KELVIN + (self._temp * (EUFY_MAX_KELVIN - EUFY_MIN_KELVIN) / 100) + EUFYHOME_MIN_KELVIN + + (self._temp * (EUFYHOME_MAX_KELVIN - EUFYHOME_MIN_KELVIN) / 100) ) return kelvin_to_mired(temp_in_k) @@ -133,7 +134,7 @@ class EufyLight(LightEntity): """Turn the specified light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) colortemp = kwargs.get(ATTR_COLOR_TEMP) - # pylint: disable=invalid-name + # pylint: disable-next=invalid-name hs = kwargs.get(ATTR_HS_COLOR) if brightness is not None: @@ -146,8 +147,10 @@ class EufyLight(LightEntity): if colortemp is not None: self._colormode = False temp_in_k = mired_to_kelvin(colortemp) - relative_temp = temp_in_k - EUFY_MIN_KELVIN - temp = int(relative_temp * 100 / (EUFY_MAX_KELVIN - EUFY_MIN_KELVIN)) + relative_temp = temp_in_k - EUFYHOME_MIN_KELVIN + temp = int( + relative_temp * 100 / (EUFYHOME_MAX_KELVIN - EUFYHOME_MIN_KELVIN) + ) else: temp = None diff --git a/homeassistant/components/eufy/manifest.json b/homeassistant/components/eufy/manifest.json index 29b0f89cd4b..87932455518 100644 --- a/homeassistant/components/eufy/manifest.json +++ b/homeassistant/components/eufy/manifest.json @@ -1,6 +1,6 @@ { "domain": "eufy", - "name": "eufy", + "name": "EufyHome", "documentation": "https://www.home-assistant.io/integrations/eufy", "requirements": ["lakeside==0.12"], "codeowners": [], diff --git a/homeassistant/components/eufy/switch.py b/homeassistant/components/eufy/switch.py index a252f43a8ca..324133354fb 100644 --- a/homeassistant/components/eufy/switch.py +++ b/homeassistant/components/eufy/switch.py @@ -1,4 +1,4 @@ -"""Support for Eufy switches.""" +"""Support for EufyHome switches.""" from __future__ import annotations from typing import Any @@ -17,14 +17,14 @@ def setup_platform( add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up Eufy switches.""" + """Set up EufyHome switches.""" if discovery_info is None: return - add_entities([EufySwitch(discovery_info)], True) + add_entities([EufyHomeSwitch(discovery_info)], True) -class EufySwitch(SwitchEntity): - """Representation of a Eufy switch.""" +class EufyHomeSwitch(SwitchEntity): + """Representation of a EufyHome switch.""" def __init__(self, device): """Initialize the light.""" diff --git a/homeassistant/components/eufylife_ble/__init__.py b/homeassistant/components/eufylife_ble/__init__.py new file mode 100644 index 00000000000..49370c2efcf --- /dev/null +++ b/homeassistant/components/eufylife_ble/__init__.py @@ -0,0 +1,70 @@ +"""The EufyLife integration.""" +from __future__ import annotations + +from eufylife_ble_client import EufyLifeBLEDevice + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.core import Event, HomeAssistant, callback + +from .const import CONF_MODEL, DOMAIN +from .models import EufyLifeData + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up EufyLife device from a config entry.""" + address = entry.unique_id + assert address is not None + + model = entry.data[CONF_MODEL] + client = EufyLifeBLEDevice(model=model) + + @callback + def _async_update_ble( + service_info: bluetooth.BluetoothServiceInfoBleak, + change: bluetooth.BluetoothChange, + ) -> None: + """Update from a ble callback.""" + client.set_ble_device_and_advertisement_data( + service_info.device, service_info.advertisement + ) + if not client.advertisement_data_contains_state: + hass.async_create_task(client.connect()) + + entry.async_on_unload( + bluetooth.async_register_callback( + hass, + _async_update_ble, + BluetoothCallbackMatcher({ADDRESS: address}), + bluetooth.BluetoothScanningMode.ACTIVE, + ) + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = EufyLifeData( + address, + model, + client, + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + async def _async_stop(event: Event) -> None: + """Close the connection.""" + await client.stop() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop) + ) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/eufylife_ble/config_flow.py b/homeassistant/components/eufylife_ble/config_flow.py new file mode 100644 index 00000000000..9e1ff4af7a8 --- /dev/null +++ b/homeassistant/components/eufylife_ble/config_flow.py @@ -0,0 +1,99 @@ +"""Config flow for the EufyLife integration.""" +from __future__ import annotations + +from typing import Any + +from eufylife_ble_client import MODEL_TO_NAME +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import CONF_MODEL, DOMAIN + + +class EufyLifeConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for EufyLife.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + + if discovery_info.name not in MODEL_TO_NAME: + return self.async_abort(reason="not_supported") + + self._discovery_info = discovery_info + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovery_info is not None + discovery_info = self._discovery_info + + model_name = MODEL_TO_NAME.get(discovery_info.name) + assert model_name is not None + + if user_input is not None: + return self.async_create_entry( + title=model_name, data={CONF_MODEL: discovery_info.name} + ) + + self._set_confirm_only() + placeholders = {"name": model_name} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + + model = self._discovered_devices[address] + return self.async_create_entry( + title=MODEL_TO_NAME[model], + data={CONF_MODEL: model}, + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass, False): + address = discovery_info.address + if ( + address in current_addresses + or address in self._discovered_devices + or discovery_info.name not in MODEL_TO_NAME + ): + continue + self._discovered_devices[address] = discovery_info.name + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/eufylife_ble/const.py b/homeassistant/components/eufylife_ble/const.py new file mode 100644 index 00000000000..dac0afc9109 --- /dev/null +++ b/homeassistant/components/eufylife_ble/const.py @@ -0,0 +1,5 @@ +"""Constants for the EufyLife integration.""" + +DOMAIN = "eufylife_ble" + +CONF_MODEL = "model" diff --git a/homeassistant/components/eufylife_ble/manifest.json b/homeassistant/components/eufylife_ble/manifest.json new file mode 100644 index 00000000000..f38389a6341 --- /dev/null +++ b/homeassistant/components/eufylife_ble/manifest.json @@ -0,0 +1,28 @@ +{ + "domain": "eufylife_ble", + "name": "EufyLife", + "integration_type": "device", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/eufylife_ble", + "bluetooth": [ + { + "local_name": "eufy T9140" + }, + { + "local_name": "eufy T9146" + }, + { + "local_name": "eufy T9147" + }, + { + "local_name": "eufy T9148" + }, + { + "local_name": "eufy T9149" + } + ], + "requirements": ["eufylife_ble_client==0.1.7"], + "dependencies": ["bluetooth_adapters"], + "codeowners": ["@bdr99"], + "iot_class": "local_push" +} diff --git a/homeassistant/components/eufylife_ble/models.py b/homeassistant/components/eufylife_ble/models.py new file mode 100644 index 00000000000..62537f22f23 --- /dev/null +++ b/homeassistant/components/eufylife_ble/models.py @@ -0,0 +1,15 @@ +"""Models for the EufyLife integration.""" +from __future__ import annotations + +from dataclasses import dataclass + +from eufylife_ble_client import EufyLifeBLEDevice + + +@dataclass +class EufyLifeData: + """Data for the EufyLife integration.""" + + address: str + model: str + client: EufyLifeBLEDevice diff --git a/homeassistant/components/eufylife_ble/sensor.py b/homeassistant/components/eufylife_ble/sensor.py new file mode 100644 index 00000000000..e57b83687a6 --- /dev/null +++ b/homeassistant/components/eufylife_ble/sensor.py @@ -0,0 +1,215 @@ +"""Support for EufyLife sensors.""" +from __future__ import annotations + +from typing import Any + +from eufylife_ble_client import MODEL_TO_NAME + +from homeassistant import config_entries +from homeassistant.components.bluetooth import async_address_present +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + UnitOfMass, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.util.unit_conversion import MassConverter +from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM + +from .const import DOMAIN +from .models import EufyLifeData + +IGNORED_STATES = {STATE_UNAVAILABLE, STATE_UNKNOWN} + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the EufyLife sensors.""" + data: EufyLifeData = hass.data[DOMAIN][entry.entry_id] + + entities = [ + EufyLifeWeightSensorEntity(data), + EufyLifeRealTimeWeightSensorEntity(data), + ] + + if data.client.supports_heart_rate: + entities.append(EufyLifeHeartRateSensorEntity(data)) + + async_add_entities(entities) + + +class EufyLifeSensorEntity(SensorEntity): + """Representation of an EufyLife sensor.""" + + _attr_has_entity_name = True + + def __init__(self, data: EufyLifeData) -> None: + """Initialize the weight sensor entity.""" + self._data = data + + self._attr_device_info = DeviceInfo( + name=MODEL_TO_NAME[data.model], + connections={(dr.CONNECTION_BLUETOOTH, data.address)}, + ) + + @property + def available(self) -> bool: + """Determine if the entity is available.""" + if self._data.client.advertisement_data_contains_state: + # If the device only uses advertisement data, just check if the address is present. + return async_address_present(self.hass, self._data.address) + + # If the device needs an active connection, availability is based on whether it is connected. + return self._data.client.is_connected + + @callback + def _handle_state_update(self, *args: Any) -> None: + """Handle state update.""" + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Register callback.""" + self.async_on_remove( + self._data.client.register_callback(self._handle_state_update) + ) + + +class EufyLifeRealTimeWeightSensorEntity(EufyLifeSensorEntity): + """Representation of an EufyLife real-time weight sensor.""" + + _attr_name = "Real-time weight" + _attr_native_unit_of_measurement = UnitOfMass.KILOGRAMS + _attr_device_class = SensorDeviceClass.WEIGHT + + def __init__(self, data: EufyLifeData) -> None: + """Initialize the real-time weight sensor entity.""" + super().__init__(data) + self._attr_unique_id = f"{data.address}_real_time_weight" + + @property + def native_value(self) -> float | None: + """Return the native value.""" + if self._data.client.state is not None: + return self._data.client.state.weight_kg + return None + + @property + def suggested_unit_of_measurement(self) -> str | None: + """Set the suggested unit based on the unit system.""" + if self.hass.config.units is US_CUSTOMARY_SYSTEM: + return UnitOfMass.POUNDS + + return UnitOfMass.KILOGRAMS + + +class EufyLifeWeightSensorEntity(RestoreEntity, EufyLifeSensorEntity): + """Representation of an EufyLife weight sensor.""" + + _attr_name = "Weight" + _attr_native_unit_of_measurement = UnitOfMass.KILOGRAMS + _attr_device_class = SensorDeviceClass.WEIGHT + + _weight_kg: float | None = None + + def __init__(self, data: EufyLifeData) -> None: + """Initialize the weight sensor entity.""" + super().__init__(data) + self._attr_unique_id = f"{data.address}_weight" + + @property + def available(self) -> bool: + """Determine if the entity is available.""" + return True + + @property + def native_value(self) -> float | None: + """Return the native value.""" + return self._weight_kg + + @property + def suggested_unit_of_measurement(self) -> str | None: + """Set the suggested unit based on the unit system.""" + if self.hass.config.units is US_CUSTOMARY_SYSTEM: + return UnitOfMass.POUNDS + + return UnitOfMass.KILOGRAMS + + @callback + def _handle_state_update(self, *args: Any) -> None: + """Handle state update.""" + state = self._data.client.state + if state is not None and state.final_weight_kg is not None: + self._weight_kg = state.final_weight_kg + + super()._handle_state_update(args) + + async def async_added_to_hass(self) -> None: + """Restore state on startup.""" + await super().async_added_to_hass() + + last_state = await self.async_get_last_state() + if not last_state or last_state.state in IGNORED_STATES: + return + + last_weight = float(last_state.state) + last_weight_unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + # Since the RestoreEntity stores the state using the displayed unit, + # not the native unit, we need to convert the state back to the native + # unit. + self._weight_kg = MassConverter.convert( + last_weight, last_weight_unit, self.native_unit_of_measurement + ) + + +class EufyLifeHeartRateSensorEntity(RestoreEntity, EufyLifeSensorEntity): + """Representation of an EufyLife heart rate sensor.""" + + _attr_name = "Heart rate" + _attr_icon = "mdi:heart-pulse" + _attr_native_unit_of_measurement = "bpm" + + _heart_rate: int | None = None + + def __init__(self, data: EufyLifeData) -> None: + """Initialize the heart rate sensor entity.""" + super().__init__(data) + self._attr_unique_id = f"{data.address}_heart_rate" + + @property + def available(self) -> bool: + """Determine if the entity is available.""" + return True + + @property + def native_value(self) -> float | None: + """Return the native value.""" + return self._heart_rate + + @callback + def _handle_state_update(self, *args: Any) -> None: + """Handle state update.""" + state = self._data.client.state + if state is not None and state.heart_rate is not None: + self._heart_rate = state.heart_rate + + super()._handle_state_update(args) + + async def async_added_to_hass(self) -> None: + """Restore state on startup.""" + await super().async_added_to_hass() + + last_state = await self.async_get_last_state() + if not last_state or last_state.state in IGNORED_STATES: + return + + self._heart_rate = int(last_state.state) diff --git a/homeassistant/components/eufylife_ble/strings.json b/homeassistant/components/eufylife_ble/strings.json new file mode 100644 index 00000000000..a045d84771e --- /dev/null +++ b/homeassistant/components/eufylife_ble/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "not_supported": "Device not supported", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/eufylife_ble/translations/bg.json b/homeassistant/components/eufylife_ble/translations/bg.json new file mode 100644 index 00000000000..af9a13197df --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/bg.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0437\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/ca.json b/homeassistant/components/eufylife_ble/translations/ca.json new file mode 100644 index 00000000000..c121ff7408c --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "not_supported": "Dispositiu no compatible" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vols configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositiu" + }, + "description": "Tria un dispositiu a configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/de.json b/homeassistant/components/eufylife_ble/translations/de.json new file mode 100644 index 00000000000..4c5720ec6fb --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "not_supported": "Ger\u00e4t nicht unterst\u00fctzt" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "M\u00f6chtest du {name} einrichten?" + }, + "user": { + "data": { + "address": "Ger\u00e4t" + }, + "description": "W\u00e4hle ein Ger\u00e4t zum Einrichten aus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/el.json b/homeassistant/components/eufylife_ble/translations/el.json new file mode 100644 index 00000000000..ea9fd15f28b --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/el.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03ad\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af", + "already_in_progress": "\u0397 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b5\u03be\u03ad\u03bb\u03b9\u03be\u03b7", + "no_devices_found": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf", + "not_supported": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" + }, + "user": { + "data": { + "address": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b3\u03b9\u03b1 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/en.json b/homeassistant/components/eufylife_ble/translations/en.json new file mode 100644 index 00000000000..afe859ca766 --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network", + "not_supported": "Device not supported" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Do you want to set up {name}?" + }, + "user": { + "data": { + "address": "Device" + }, + "description": "Choose a device to set up" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/es-419.json b/homeassistant/components/eufylife_ble/translations/es-419.json new file mode 100644 index 00000000000..31a7dbc222f --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/es-419.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escoja un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/es.json b/homeassistant/components/eufylife_ble/translations/es.json new file mode 100644 index 00000000000..ae0ab01acdf --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red", + "not_supported": "Dispositivo no compatible" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u00bfQuieres configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Elige un dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/et.json b/homeassistant/components/eufylife_ble/translations/et.json new file mode 100644 index 00000000000..8f424097aa5 --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet", + "not_supported": "Seadet ei toetata" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Kas seadistada {name} ?" + }, + "user": { + "data": { + "address": "Seade" + }, + "description": "Vali h\u00e4\u00e4lestatav seade" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/hu.json b/homeassistant/components/eufylife_ble/translations/hu.json new file mode 100644 index 00000000000..4668ffea416 --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/hu.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "not_supported": "Eszk\u00f6z nem t\u00e1mogatott" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" + }, + "user": { + "data": { + "address": "Eszk\u00f6z" + }, + "description": "V\u00e1lasszon egy be\u00e1ll\u00edtand\u00f3 eszk\u00f6zt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/id.json b/homeassistant/components/eufylife_ble/translations/id.json new file mode 100644 index 00000000000..573eb39ed15 --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/id.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "not_supported": "Perangkat tidak didukung" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Ingin menyiapkan {name}?" + }, + "user": { + "data": { + "address": "Perangkat" + }, + "description": "Pilih perangkat untuk disiapkan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/it.json b/homeassistant/components/eufylife_ble/translations/it.json new file mode 100644 index 00000000000..97113c57103 --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "not_supported": "Dispositivo non supportato" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vuoi configurare {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Scegli un dispositivo da configurare" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/ja.json b/homeassistant/components/eufylife_ble/translations/ja.json new file mode 100644 index 00000000000..7e4f5db8e3b --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/ja.json @@ -0,0 +1,22 @@ +{ + "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", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093", + "not_supported": "\u30c7\u30d0\u30a4\u30b9\u304c\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "{name} \u3092\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3057\u307e\u3059\u304b\uff1f" + }, + "user": { + "data": { + "address": "\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u30bb\u30c3\u30c8\u30a2\u30c3\u30d7\u3059\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/lv.json b/homeassistant/components/eufylife_ble/translations/lv.json new file mode 100644 index 00000000000..f5f72e6923e --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/lv.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/nl.json b/homeassistant/components/eufylife_ble/translations/nl.json new file mode 100644 index 00000000000..ce55c570b7c --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "not_supported": "Apparaat wordt niet ondersteund." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Wilt u {name} instellen?" + }, + "user": { + "data": { + "address": "Apparaat" + }, + "description": "Kies een apparaat om in te stellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/no.json b/homeassistant/components/eufylife_ble/translations/no.json new file mode 100644 index 00000000000..38ab3d096f2 --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "not_supported": "Enheten st\u00f8ttes ikke" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Vil du sette opp {name} ?" + }, + "user": { + "data": { + "address": "Enhet" + }, + "description": "Velg en enhet du vil konfigurere" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/pl.json b/homeassistant/components/eufylife_ble/translations/pl.json new file mode 100644 index 00000000000..4715905a2e9 --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "not_supported": "Urz\u0105dzenie nie jest obs\u0142ugiwane" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {name}?" + }, + "user": { + "data": { + "address": "Urz\u0105dzenie" + }, + "description": "Wybierz urz\u0105dzenie do skonfigurowania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/pt-BR.json b/homeassistant/components/eufylife_ble/translations/pt-BR.json new file mode 100644 index 00000000000..0da7639fa2a --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/pt-BR.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "not_supported": "Dispositivo n\u00e3o suportado" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Deseja configurar {name}?" + }, + "user": { + "data": { + "address": "Dispositivo" + }, + "description": "Escolha um dispositivo para configurar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/ru.json b/homeassistant/components/eufylife_ble/translations/ru.json new file mode 100644 index 00000000000..887499e5f2e --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "not_supported": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" + }, + "user": { + "data": { + "address": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/sk.json b/homeassistant/components/eufylife_ble/translations/sk.json new file mode 100644 index 00000000000..8273d877c92 --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "not_supported": "Zariadenie nie je podporovan\u00e9" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "Chcete nastavi\u0165 {name}?" + }, + "user": { + "data": { + "address": "Zaradenie" + }, + "description": "Vyberte zariadenie, ktor\u00e9 chcete nastavi\u0165" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/sv.json b/homeassistant/components/eufylife_ble/translations/sv.json new file mode 100644 index 00000000000..e8940bef26a --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/sv.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{name}" + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/uk.json b/homeassistant/components/eufylife_ble/translations/uk.json new file mode 100644 index 00000000000..c0e5c8da931 --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e", + "already_in_progress": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454", + "no_devices_found": "\u0423 \u043c\u0435\u0440\u0435\u0436\u0456 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432", + "not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f" + }, + "step": { + "user": { + "data": { + "address": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eufylife_ble/translations/zh-Hant.json b/homeassistant/components/eufylife_ble/translations/zh-Hant.json new file mode 100644 index 00000000000..64ae1f19094 --- /dev/null +++ b/homeassistant/components/eufylife_ble/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "not_supported": "\u88dd\u7f6e\u4e0d\u652f\u63f4" + }, + "flow_title": "{name}", + "step": { + "bluetooth_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" + }, + "user": { + "data": { + "address": "\u88dd\u7f6e" + }, + "description": "\u9078\u64c7\u6240\u8981\u8a2d\u5b9a\u7684\u88dd\u7f6e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py index 3ef4627c089..1a177cf8909 100644 --- a/homeassistant/components/everlights/light.py +++ b/homeassistant/components/everlights/light.py @@ -65,9 +65,8 @@ async def async_setup_platform( except pyeverlights.ConnectionError as err: raise PlatformNotReady from err - else: - lights.append(EverLightsLight(api, pyeverlights.ZONE_1, status, effects)) - lights.append(EverLightsLight(api, pyeverlights.ZONE_2, status, effects)) + lights.append(EverLightsLight(api, pyeverlights.ZONE_1, status, effects)) + lights.append(EverLightsLight(api, pyeverlights.ZONE_2, status, effects)) async_add_entities(lights) diff --git a/homeassistant/components/evil_genius_labs/diagnostics.py b/homeassistant/components/evil_genius_labs/diagnostics.py index 18b8c8ed572..a6a15165716 100644 --- a/homeassistant/components/evil_genius_labs/diagnostics.py +++ b/homeassistant/components/evil_genius_labs/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Evil Genius Labs.""" 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 @@ -13,7 +15,7 @@ TO_REDACT = {"wiFiSsidDefault", "wiFiSSID"} async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: EvilGeniusUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/evil_genius_labs/translations/id.json b/homeassistant/components/evil_genius_labs/translations/id.json index 8d550418ce2..78f02d58f20 100644 --- a/homeassistant/components/evil_genius_labs/translations/id.json +++ b/homeassistant/components/evil_genius_labs/translations/id.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "Gagal terhubung", - "timeout": "Tenggang waktu membuat koneksi habis", + "timeout": "Tenggang waktu pembuatan koneksi habis", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/evil_genius_labs/util.py b/homeassistant/components/evil_genius_labs/util.py index 1071b953027..b0e01c1f329 100644 --- a/homeassistant/components/evil_genius_labs/util.py +++ b/homeassistant/components/evil_genius_labs/util.py @@ -3,9 +3,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine from functools import wraps -from typing import Any, TypeVar - -from typing_extensions import Concatenate, ParamSpec +from typing import Any, Concatenate, ParamSpec, TypeVar from . import EvilGeniusEntity diff --git a/homeassistant/components/ezviz/entity.py b/homeassistant/components/ezviz/entity.py index e4debedc640..1c966c7f82e 100644 --- a/homeassistant/components/ezviz/entity.py +++ b/homeassistant/components/ezviz/entity.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -24,6 +25,9 @@ class EzvizEntity(CoordinatorEntity[EzvizDataUpdateCoordinator], Entity): self._camera_name = self.data["name"] self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, serial)}, + connections={ + (CONNECTION_NETWORK_MAC, self.data["mac_address"]), + }, manufacturer=MANUFACTURER, model=self.data["device_sub_category"], name=self.data["name"], diff --git a/homeassistant/components/ezviz/translations/el.json b/homeassistant/components/ezviz/translations/el.json index 1b9fd46f126..6bf914a8a6b 100644 --- a/homeassistant/components/ezviz/translations/el.json +++ b/homeassistant/components/ezviz/translations/el.json @@ -2,7 +2,7 @@ "config": { "abort": { "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", + "ezviz_cloud_account_missing": "\u039b\u03b5\u03af\u03c0\u03b5\u03b9 \u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 cloud EZVIZ. \u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03bf\u03cd\u03bc\u03b5 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf\u03bd \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": { @@ -17,8 +17,8 @@ "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" + "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\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 \u03b7 \u03ba\u03ac\u03bc\u03b5\u03c1\u03b1 EZVIZ" }, "user": { "data": { @@ -26,7 +26,7 @@ "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" + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03c4\u03bf EZVIZ Cloud" }, "user_custom_url": { "data": { @@ -35,7 +35,7 @@ "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" + "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 EZVIZ" } } }, diff --git a/homeassistant/components/ezviz/translations/uk.json b/homeassistant/components/ezviz/translations/uk.json new file mode 100644 index 00000000000..e3d968abc52 --- /dev/null +++ b/homeassistant/components/ezviz/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user_custom_url": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fastdotcom/sensor.py b/homeassistant/components/fastdotcom/sensor.py index 1df431d0bf3..4f00dd5a543 100644 --- a/homeassistant/components/fastdotcom/sensor.py +++ b/homeassistant/components/fastdotcom/sensor.py @@ -32,7 +32,6 @@ class SpeedtestSensor(RestoreEntity, SensorEntity): _attr_native_unit_of_measurement = UnitOfDataRate.MEGABITS_PER_SECOND _attr_icon = "mdi:speedometer" _attr_should_poll = False - _attr_native_value = None def __init__(self, speedtest_data: dict[str, Any]) -> None: """Initialize the sensor.""" diff --git a/homeassistant/components/fibaro/translations/lv.json b/homeassistant/components/fibaro/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/fibaro/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fibaro/translations/uk.json b/homeassistant/components/fibaro/translations/uk.json new file mode 100644 index 00000000000..0f6a22af638 --- /dev/null +++ b/homeassistant/components/fibaro/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041e\u043d\u043e\u0432\u0456\u0442\u044c \u0441\u0432\u0456\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index ce70ed14d19..e4166494ce4 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -3,12 +3,13 @@ from __future__ import annotations from collections import Counter, deque from copy import copy +from dataclasses import dataclass from datetime import datetime, timedelta from functools import partial import logging from numbers import Number import statistics -from typing import Any +from typing import Any, cast import voluptuous as vol @@ -40,7 +41,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.reload import async_setup_reload_service -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from homeassistant.util.decorator import Registry import homeassistant.util.dt as dt_util @@ -211,7 +212,7 @@ class SensorFilter(SensorEntity): self._attr_unique_id = unique_id self._entity = entity_id self._attr_native_unit_of_measurement = None - self._state: str | None = None + self._state: StateType = None self._filters = filters self._attr_icon = None self._attr_device_class = None @@ -242,7 +243,7 @@ class SensorFilter(SensorEntity): self.async_write_ha_state() return - temp_state = new_state + temp_state = _State(new_state.last_updated, new_state.state) try: for filt in self._filters: @@ -302,14 +303,14 @@ class SensorFilter(SensorEntity): for filt in self._filters: if ( filt.window_unit == WINDOW_SIZE_UNIT_NUMBER_EVENTS - and largest_window_items < filt.window_size + and largest_window_items < (size := cast(int, filt.window_size)) ): - largest_window_items = filt.window_size + largest_window_items = size elif ( filt.window_unit == WINDOW_SIZE_UNIT_TIME - and largest_window_time < filt.window_size + and largest_window_time < (val := cast(timedelta, filt.window_size)) ): - largest_window_time = filt.window_size + largest_window_time = val # Retrieve the largest window_size of each type if largest_window_items > 0: @@ -361,10 +362,10 @@ class SensorFilter(SensorEntity): ) @property - def native_value(self) -> datetime | str | None: + def native_value(self) -> datetime | StateType: """Return the state of the sensor.""" if self._state is not None and self.device_class == SensorDeviceClass.TIMESTAMP: - return datetime.fromisoformat(self._state) + return datetime.fromisoformat(str(self._state)) return self._state @@ -372,7 +373,9 @@ class SensorFilter(SensorEntity): class FilterState: """State abstraction for filter usage.""" - def __init__(self, state): + state: str | float | int + + def __init__(self, state: _State) -> None: """Initialize with HA State object.""" self.timestamp = state.last_updated try: @@ -380,31 +383,43 @@ class FilterState: except ValueError: self.state = state.state - def set_precision(self, precision): + def set_precision(self, precision: int) -> None: """Set precision of Number based states.""" if isinstance(self.state, Number): value = round(float(self.state), precision) self.state = int(value) if precision == 0 else value - def __str__(self): + def __str__(self) -> str: """Return state as the string representation of FilterState.""" return str(self.state) - def __repr__(self): + def __repr__(self) -> str: """Return timestamp and state as the representation of FilterState.""" return f"{self.timestamp} : {self.state}" +@dataclass +class _State: + """Simplified State class. + + The standard State class only accepts string in `state`, + and we are only interested in two properties. + """ + + last_updated: datetime + state: str | float | int + + class Filter: """Filter skeleton.""" def __init__( self, - name, - window_size: int = 1, - precision: int | None = None, - entity: str | None = None, - ): + name: str, + window_size: int | timedelta, + precision: int, + entity: str, + ) -> None: """Initialize common attributes. :param window_size: size of the sliding window that holds previous values @@ -412,12 +427,12 @@ class Filter: :param entity: used for debugging only """ if isinstance(window_size, int): - self.states: deque = deque(maxlen=window_size) + self.states: deque[FilterState] = deque(maxlen=window_size) self.window_unit = WINDOW_SIZE_UNIT_NUMBER_EVENTS else: self.states = deque(maxlen=0) self.window_unit = WINDOW_SIZE_UNIT_TIME - self.precision = precision + self.filter_precision = precision self._name = name self._entity = entity self._skip_processing = False @@ -426,32 +441,32 @@ class Filter: self._only_numbers = True @property - def window_size(self): + def window_size(self) -> int | timedelta: """Return window size.""" return self._window_size @property - def name(self): + def name(self) -> str: """Return filter name.""" return self._name @property - def skip_processing(self): + def skip_processing(self) -> bool: """Return whether the current filter_state should be skipped.""" return self._skip_processing - def _filter_state(self, new_state): + def _filter_state(self, new_state: FilterState) -> FilterState: """Implement filter.""" raise NotImplementedError() - def filter_state(self, new_state): + def filter_state(self, new_state: _State) -> _State: """Implement a common interface for filters.""" fstate = FilterState(new_state) if self._only_numbers and not isinstance(fstate.state, Number): raise ValueError(f"State <{fstate.state}> is not a Number") filtered = self._filter_state(fstate) - filtered.set_precision(self.precision) + filtered.set_precision(self.filter_precision) if self._store_raw: self.states.append(copy(FilterState(new_state))) else: @@ -470,25 +485,28 @@ class RangeFilter(Filter, SensorEntity): def __init__( self, - entity, - precision: int | None = DEFAULT_PRECISION, + entity: str, + precision: int, lower_bound: float | None = None, upper_bound: float | None = None, - ): + ) -> None: """Initialize Filter. :param upper_bound: band upper bound :param lower_bound: band lower bound """ - super().__init__(FILTER_NAME_RANGE, precision=precision, entity=entity) + super().__init__(FILTER_NAME_RANGE, DEFAULT_WINDOW_SIZE, precision, entity) self._lower_bound = lower_bound self._upper_bound = upper_bound self._stats_internal: Counter = Counter() - def _filter_state(self, new_state): + def _filter_state(self, new_state: FilterState) -> FilterState: """Implement the range filter.""" - if self._upper_bound is not None and new_state.state > self._upper_bound: + # We can cast safely here thanks to self._only_numbers = True + new_state_value = cast(float, new_state.state) + + if self._upper_bound is not None and new_state_value > self._upper_bound: self._stats_internal["erasures_up"] += 1 @@ -500,7 +518,7 @@ class RangeFilter(Filter, SensorEntity): ) new_state.state = self._upper_bound - elif self._lower_bound is not None and new_state.state < self._lower_bound: + elif self._lower_bound is not None and new_state_value < self._lower_bound: self._stats_internal["erasures_low"] += 1 @@ -522,7 +540,9 @@ class OutlierFilter(Filter, SensorEntity): Determines if new state is in a band around the median. """ - def __init__(self, window_size, precision, entity, radius: float): + def __init__( + self, window_size: int, precision: int, entity: str, radius: float + ) -> None: """Initialize Filter. :param radius: band radius @@ -532,13 +552,17 @@ class OutlierFilter(Filter, SensorEntity): self._stats_internal: Counter = Counter() self._store_raw = True - def _filter_state(self, new_state): + def _filter_state(self, new_state: FilterState) -> FilterState: """Implement the outlier filter.""" - median = statistics.median([s.state for s in self.states]) if self.states else 0 + # We can cast safely here thanks to self._only_numbers = True + previous_state_values = [cast(float, s.state) for s in self.states] + new_state_value = cast(float, new_state.state) + + median = statistics.median(previous_state_values) if self.states else 0 if ( len(self.states) == self.states.maxlen - and abs(new_state.state - median) > self._radius + and abs(new_state_value - median) > self._radius ): self._stats_internal["erasures"] += 1 @@ -557,12 +581,14 @@ class OutlierFilter(Filter, SensorEntity): class LowPassFilter(Filter, SensorEntity): """BASIC Low Pass Filter.""" - def __init__(self, window_size, precision, entity, time_constant: int): + def __init__( + self, window_size: int, precision: int, entity: str, time_constant: int + ) -> None: """Initialize Filter.""" super().__init__(FILTER_NAME_LOWPASS, window_size, precision, entity) self._time_constant = time_constant - def _filter_state(self, new_state): + def _filter_state(self, new_state: FilterState) -> FilterState: """Implement the low pass filter.""" if not self.states: @@ -570,9 +596,10 @@ class LowPassFilter(Filter, SensorEntity): new_weight = 1.0 / self._time_constant prev_weight = 1.0 - new_weight - new_state.state = ( - prev_weight * self.states[-1].state + new_weight * new_state.state - ) + # We can cast safely here thanks to self._only_numbers = True + prev_state_value = cast(float, self.states[-1].state) + new_state_value = cast(float, new_state.state) + new_state.state = prev_weight * prev_state_value + new_weight * new_state_value return new_state @@ -585,18 +612,22 @@ class TimeSMAFilter(Filter, SensorEntity): """ def __init__( - self, window_size, precision, entity, type - ): # pylint: disable=redefined-builtin + self, + window_size: timedelta, + precision: int, + entity: str, + type: str, # pylint: disable=redefined-builtin + ) -> None: """Initialize Filter. :param type: type of algorithm used to connect discrete values """ super().__init__(FILTER_NAME_TIME_SMA, window_size, precision, entity) self._time_window = window_size - self.last_leak = None - self.queue = deque() + self.last_leak: FilterState | None = None + self.queue = deque[FilterState]() - def _leak(self, left_boundary): + def _leak(self, left_boundary: datetime) -> None: """Remove timeouted elements.""" while self.queue: if self.queue[0].timestamp + self._time_window <= left_boundary: @@ -604,17 +635,19 @@ class TimeSMAFilter(Filter, SensorEntity): else: return - def _filter_state(self, new_state): + def _filter_state(self, new_state: FilterState) -> FilterState: """Implement the Simple Moving Average filter.""" self._leak(new_state.timestamp) self.queue.append(copy(new_state)) - moving_sum = 0 + moving_sum: float = 0 start = new_state.timestamp - self._time_window prev_state = self.last_leak if self.last_leak is not None else self.queue[0] for state in self.queue: - moving_sum += (state.timestamp - start).total_seconds() * prev_state.state + # We can cast safely here thanks to self._only_numbers = True + prev_state_value = cast(float, prev_state.state) + moving_sum += (state.timestamp - start).total_seconds() * prev_state_value start = state.timestamp prev_state = state @@ -630,12 +663,12 @@ class ThrottleFilter(Filter, SensorEntity): One sample per window. """ - def __init__(self, window_size, precision, entity): + def __init__(self, window_size: int, precision: int, entity: str) -> None: """Initialize Filter.""" super().__init__(FILTER_NAME_THROTTLE, window_size, precision, entity) self._only_numbers = False - def _filter_state(self, new_state): + def _filter_state(self, new_state: FilterState) -> FilterState: """Implement the throttle filter.""" if not self.states or len(self.states) == self.states.maxlen: self.states.clear() @@ -653,14 +686,14 @@ class TimeThrottleFilter(Filter, SensorEntity): One sample per time period. """ - def __init__(self, window_size, precision, entity): + def __init__(self, window_size: timedelta, precision: int, entity: str) -> None: """Initialize Filter.""" super().__init__(FILTER_NAME_TIME_THROTTLE, window_size, precision, entity) self._time_window = window_size - self._last_emitted_at = None + self._last_emitted_at: datetime | None = None self._only_numbers = False - def _filter_state(self, new_state): + def _filter_state(self, new_state: FilterState) -> FilterState: """Implement the filter.""" window_start = new_state.timestamp - self._time_window if not self._last_emitted_at or self._last_emitted_at <= window_start: diff --git a/homeassistant/components/fire_tv/__init__.py b/homeassistant/components/fire_tv/__init__.py new file mode 100644 index 00000000000..ff139ece644 --- /dev/null +++ b/homeassistant/components/fire_tv/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Amazon Fire TV.""" diff --git a/homeassistant/components/fire_tv/manifest.json b/homeassistant/components/fire_tv/manifest.json new file mode 100644 index 00000000000..397ea28b008 --- /dev/null +++ b/homeassistant/components/fire_tv/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "fire_tv", + "name": "Amazon Fire TV", + "integration_type": "virtual", + "supported_by": "androidtv" +} diff --git a/homeassistant/components/fireservicerota/translations/tr.json b/homeassistant/components/fireservicerota/translations/tr.json index 155dcaaf43d..62e1194ccc4 100644 --- a/homeassistant/components/fireservicerota/translations/tr.json +++ b/homeassistant/components/fireservicerota/translations/tr.json @@ -11,6 +11,12 @@ "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "Kimlik do\u011frulama belirte\u00e7leri ge\u00e7ersiz oldu, bunlar\u0131 yeniden olu\u015fturmak i\u00e7in oturum a\u00e7\u0131n." + }, "user": { "data": { "password": "Parola", diff --git a/homeassistant/components/fireservicerota/translations/uk.json b/homeassistant/components/fireservicerota/translations/uk.json index 199120a54ae..521aad73323 100644 --- a/homeassistant/components/fireservicerota/translations/uk.json +++ b/homeassistant/components/fireservicerota/translations/uk.json @@ -11,6 +11,11 @@ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/firmata/board.py b/homeassistant/components/firmata/board.py index 8b5c76e2582..c309676c8d6 100644 --- a/homeassistant/components/firmata/board.py +++ b/homeassistant/components/firmata/board.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Mapping import logging -from typing import Literal, Union +from typing import Literal from pymata_express.pymata_express import PymataExpress from pymata_express.pymata_express_serial import serial @@ -29,7 +29,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -FirmataPinType = Union[int, str] +FirmataPinType = int | str class FirmataBoard: diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 0f3f4c22ff7..bc6931a29c6 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -449,7 +449,7 @@ class FitbitSensor(SensorEntity): self._attr_native_value = raw_state else: try: - self._attr_native_value = f"{int(raw_state):,}" + self._attr_native_value = int(raw_state) except TypeError: self._attr_native_value = raw_state diff --git a/homeassistant/components/fivem/translations/uk.json b/homeassistant/components/fivem/translations/uk.json index b932679af93..d222e9454f5 100644 --- a/homeassistant/components/fivem/translations/uk.json +++ b/homeassistant/components/fivem/translations/uk.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0445\u043e\u0441\u0442 \u0456 \u043f\u043e\u0440\u0442 \u0456 \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0442\u044c \u0441\u043f\u0440\u043e\u0431\u0443. \u0422\u0430\u043a\u043e\u0436 \u043f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0432\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u0435 \u043e\u0441\u0442\u0430\u043d\u043d\u044e \u0432\u0435\u0440\u0441\u0456\u044e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 FiveM.", "unknown_error": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" } } diff --git a/homeassistant/components/fjaraskupan/__init__.py b/homeassistant/components/fjaraskupan/__init__.py index 66de2ddb8a6..e009aeabe77 100644 --- a/homeassistant/components/fjaraskupan/__init__.py +++ b/homeassistant/components/fjaraskupan/__init__.py @@ -174,7 +174,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/fjaraskupan/manifest.json b/homeassistant/components/fjaraskupan/manifest.json index 443991b5d70..e1d79887abe 100644 --- a/homeassistant/components/fjaraskupan/manifest.json +++ b/homeassistant/components/fjaraskupan/manifest.json @@ -7,7 +7,7 @@ "codeowners": ["@elupus"], "iot_class": "local_polling", "loggers": ["bleak", "fjaraskupan"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "bluetooth": [ { "connectable": false, diff --git a/homeassistant/components/flick_electric/config_flow.py b/homeassistant/components/flick_electric/config_flow.py index 7f21397d5a7..5fac5cdb83a 100644 --- a/homeassistant/components/flick_electric/config_flow.py +++ b/homeassistant/components/flick_electric/config_flow.py @@ -51,8 +51,8 @@ class FlickConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): raise CannotConnect() from err except AuthException as err: raise InvalidAuth() from err - else: - return token is not None + + return token is not None async def async_step_user(self, user_input=None): """Handle gathering login info.""" diff --git a/homeassistant/components/flick_electric/translations/lt.json b/homeassistant/components/flick_electric/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/lv.json b/homeassistant/components/flipr/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/flipr/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flipr/translations/uk.json b/homeassistant/components/flipr/translations/uk.json new file mode 100644 index 00000000000..5c722c2a338 --- /dev/null +++ b/homeassistant/components/flipr/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/lt.json b/homeassistant/components/flo/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/flo/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/binary_sensor.py b/homeassistant/components/flume/binary_sensor.py index 2f5ea4323d9..0ec3576b2f9 100644 --- a/homeassistant/components/flume/binary_sensor.py +++ b/homeassistant/components/flume/binary_sensor.py @@ -125,11 +125,12 @@ async def async_setup_entry( async_add_entities(flume_entity_list) -class FlumeNotificationBinarySensor(FlumeEntity, BinarySensorEntity): +class FlumeNotificationBinarySensor( + FlumeEntity[FlumeNotificationDataUpdateCoordinator], BinarySensorEntity +): """Binary sensor class.""" entity_description: FlumeBinarySensorEntityDescription - coordinator: FlumeNotificationDataUpdateCoordinator @property def is_on(self) -> bool: @@ -144,11 +145,12 @@ class FlumeNotificationBinarySensor(FlumeEntity, BinarySensorEntity): ) -class FlumeConnectionBinarySensor(FlumeEntity, BinarySensorEntity): +class FlumeConnectionBinarySensor( + FlumeEntity[FlumeDeviceConnectionUpdateCoordinator], BinarySensorEntity +): """Binary Sensor class for WIFI Connection status.""" entity_description: FlumeBinarySensorEntityDescription - coordinator: FlumeDeviceConnectionUpdateCoordinator _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY diff --git a/homeassistant/components/flume/const.py b/homeassistant/components/flume/const.py index b9192207e75..1889cca8fa5 100644 --- a/homeassistant/components/flume/const.py +++ b/homeassistant/components/flume/const.py @@ -15,10 +15,10 @@ PLATFORMS = [ DEFAULT_NAME = "Flume Sensor" -# Flume API limits individual endpoints to 120 queries per hour -NOTIFICATION_SCAN_INTERVAL = timedelta(minutes=1) -DEVICE_SCAN_INTERVAL = timedelta(minutes=5) -DEVICE_CONNECTION_SCAN_INTERVAL = timedelta(minutes=1) +# Flume API limits queries to 120 per hour +NOTIFICATION_SCAN_INTERVAL = timedelta(minutes=5) +DEVICE_SCAN_INTERVAL = timedelta(minutes=1) +DEVICE_CONNECTION_SCAN_INTERVAL = timedelta(minutes=60) _LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/flume/entity.py b/homeassistant/components/flume/entity.py index 7cd84127c64..ef63eeff1d7 100644 --- a/homeassistant/components/flume/entity.py +++ b/homeassistant/components/flume/entity.py @@ -1,16 +1,29 @@ """Platform for shared base classes for sensors.""" from __future__ import annotations +from typing import TypeVar + from homeassistant.helpers.entity import DeviceInfo, EntityDescription -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN +from .coordinator import ( + FlumeDeviceConnectionUpdateCoordinator, + FlumeDeviceDataUpdateCoordinator, + FlumeNotificationDataUpdateCoordinator, +) + +_FlumeCoordinatorT = TypeVar( + "_FlumeCoordinatorT", + bound=( + FlumeDeviceDataUpdateCoordinator + | FlumeDeviceConnectionUpdateCoordinator + | FlumeNotificationDataUpdateCoordinator + ), +) -class FlumeEntity(CoordinatorEntity[DataUpdateCoordinator[None]]): +class FlumeEntity(CoordinatorEntity[_FlumeCoordinatorT]): """Base entity class.""" _attr_attribution = "Data provided by Flume API" @@ -18,7 +31,7 @@ class FlumeEntity(CoordinatorEntity[DataUpdateCoordinator[None]]): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: _FlumeCoordinatorT, description: EntityDescription, device_id: str, location_name: str, diff --git a/homeassistant/components/flume/sensor.py b/homeassistant/components/flume/sensor.py index c7ebbd1f456..4b5fe5bc8e9 100644 --- a/homeassistant/components/flume/sensor.py +++ b/homeassistant/components/flume/sensor.py @@ -130,11 +130,9 @@ async def async_setup_entry( async_add_entities(flume_entity_list) -class FlumeSensor(FlumeEntity, SensorEntity): +class FlumeSensor(FlumeEntity[FlumeDeviceDataUpdateCoordinator], SensorEntity): """Representation of the Flume sensor.""" - coordinator: FlumeDeviceDataUpdateCoordinator - @property def native_value(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/flume/translations/sk.json b/homeassistant/components/flume/translations/sk.json index b9a883e81df..e2670962d4d 100644 --- a/homeassistant/components/flume/translations/sk.json +++ b/homeassistant/components/flume/translations/sk.json @@ -24,7 +24,7 @@ "password": "Heslo", "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" }, - "description": "Na pr\u00edstup k osobn\u00e9mu API Flume si budete musie\u0165 vy\u017eiada\u0165 \u201eClient ID\u201c a \u201eClient Secret\u201c na https://portal.flumetech.com/settings#token", + "description": "Na pr\u00edstup k osobn\u00e9mu API Flume si budete musie\u0165 vy\u017eiada\u0165 'Client ID' a 'Client Secret' na https://portal.flumetech.com/settings#token", "title": "Pripoji\u0165 sa k svojmu \u00fa\u010dtu Flume" } } diff --git a/homeassistant/components/flume/translations/uk.json b/homeassistant/components/flume/translations/uk.json index 53fb4f3d6d7..60e1680031d 100644 --- a/homeassistant/components/flume/translations/uk.json +++ b/homeassistant/components/flume/translations/uk.json @@ -9,6 +9,12 @@ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username} \u0431\u0456\u043b\u044c\u0448\u0435 \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439." + }, "user": { "data": { "client_id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0456\u0454\u043d\u0442\u0430", diff --git a/homeassistant/components/flux_led/__init__.py b/homeassistant/components/flux_led/__init__.py index 04d60fec25e..7d7ef2d42bf 100644 --- a/homeassistant/components/flux_led/__init__.py +++ b/homeassistant/components/flux_led/__init__.py @@ -111,7 +111,7 @@ async def _async_migrate_unique_ids(hass: HomeAssistant, entry: ConfigEntry) -> new_unique_id = None if entity_unique_id.startswith(entry_id): # Old format {entry_id}....., New format {unique_id}.... - new_unique_id = f"{unique_id}{entity_unique_id[len(entry_id):]}" + new_unique_id = f"{unique_id}{entity_unique_id.removeprefix(entry_id)}" elif ( ":" in entity_mac and entity_mac != unique_id diff --git a/homeassistant/components/flux_led/config_flow.py b/homeassistant/components/flux_led/config_flow.py index b245c0c2bc2..11e045bec70 100644 --- a/homeassistant/components/flux_led/config_flow.py +++ b/homeassistant/components/flux_led/config_flow.py @@ -121,8 +121,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async_update_entry_from_discovery( self.hass, entry, device, None, allow_update_mac ) - or entry.state == config_entries.ConfigEntryState.SETUP_RETRY - ): + and entry.state + not in ( + config_entries.ConfigEntryState.SETUP_IN_PROGRESS, + config_entries.ConfigEntryState.NOT_LOADED, + ) + ) or entry.state == config_entries.ConfigEntryState.SETUP_RETRY: self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) ) @@ -151,16 +155,16 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): device = await self._async_try_connect(host, device) except FLUX_LED_EXCEPTIONS: return self.async_abort(reason="cannot_connect") - else: - discovered_mac = device[ATTR_ID] - if device[ATTR_MODEL_DESCRIPTION] or ( - discovered_mac is not None - and (formatted_discovered_mac := dr.format_mac(discovered_mac)) - and formatted_discovered_mac != mac - and mac_matches_by_one(discovered_mac, mac) - ): - self._discovered_device = device - await self._async_set_discovered_mac(device, True) + + discovered_mac = device[ATTR_ID] + if device[ATTR_MODEL_DESCRIPTION] or ( + discovered_mac is not None + and (formatted_discovered_mac := dr.format_mac(discovered_mac)) + and formatted_discovered_mac != mac + and mac_matches_by_one(discovered_mac, mac) + ): + self._discovered_device = device + await self._async_set_discovered_mac(device, True) return await self.async_step_discovery_confirm() async def async_step_discovery_confirm( diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 66aa9fe0b92..585e56fd941 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "dependencies": ["network"], "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": ["flux_led==0.28.34"], + "requirements": ["flux_led==0.28.35"], "quality_scale": "platinum", "codeowners": ["@icemanch", "@bdraco"], "iot_class": "local_push", diff --git a/homeassistant/components/flux_led/translations/lv.json b/homeassistant/components/flux_led/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/flux_led/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux_led/translations/tr.json b/homeassistant/components/flux_led/translations/tr.json index 01bdbe36849..f9633bdb330 100644 --- a/homeassistant/components/flux_led/translations/tr.json +++ b/homeassistant/components/flux_led/translations/tr.json @@ -11,7 +11,7 @@ "flow_title": "{model} {id} ({ipaddr})", "step": { "discovery_confirm": { - "description": "{model} {id} ( {ipaddr} ) kurulumu yapmak istiyor musunuz?" + "description": "{model} {id} ( {ipaddr} ) kurmak istiyor musunuz?" }, "user": { "data": { diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index 58c8c9c04fa..f1d1d6de7d4 100644 --- a/homeassistant/components/folder_watcher/manifest.json +++ b/homeassistant/components/folder_watcher/manifest.json @@ -2,7 +2,7 @@ "domain": "folder_watcher", "name": "Folder Watcher", "documentation": "https://www.home-assistant.io/integrations/folder_watcher", - "requirements": ["watchdog==2.2.0"], + "requirements": ["watchdog==2.2.1"], "codeowners": [], "quality_scale": "internal", "iot_class": "local_polling", diff --git a/homeassistant/components/forecast_solar/translations/el.json b/homeassistant/components/forecast_solar/translations/el.json index 31e2708207f..9acb1ece57c 100644 --- a/homeassistant/components/forecast_solar/translations/el.json +++ b/homeassistant/components/forecast_solar/translations/el.json @@ -28,7 +28,7 @@ "inverter_size": "\u039c\u03ad\u03b3\u03b5\u03b8\u03bf\u03c2 \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03bf\u03c0\u03ad\u03b1 (Watt)", "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." + "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\u03bd \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03c4\u03bf\u03c5 \u03b1\u03c0\u03bf\u03c4\u03b5\u03bb\u03ad\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2 Forecast.Solar. \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." } } } diff --git a/homeassistant/components/forecast_solar/translations/hu.json b/homeassistant/components/forecast_solar/translations/hu.json index 98407c0cb92..b8182d8a74d 100644 --- a/homeassistant/components/forecast_solar/translations/hu.json +++ b/homeassistant/components/forecast_solar/translations/hu.json @@ -28,7 +28,7 @@ "inverter_size": "Inverter m\u00e9rete (Watt)", "modules power": "A napelemmodulok teljes cs\u00facsteljes\u00edtm\u00e9nye (Watt)" }, - "description": "Ezek az \u00e9rt\u00e9kek lehet\u0151v\u00e9 teszik a Forecast.Solar eredm\u00e9ny\u00e9nek finomhangol\u00e1s\u00e1t. Ha egy mez\u0151 nem egy\u00e9rtelm\u0171, k\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t." + "description": "Ezek az \u00e9rt\u00e9kek lehet\u0151v\u00e9 teszik a Forecast.Solar eredm\u00e9ny\u00e9nek finomhangol\u00e1s\u00e1t. Ha egy mez\u0151 nem egy\u00e9rtelm\u0171, k\u00e9rem, olvassa el a dokument\u00e1ci\u00f3t." } } } diff --git a/homeassistant/components/forecast_solar/translations/tr.json b/homeassistant/components/forecast_solar/translations/tr.json index 1fa6def5714..4510d69f216 100644 --- a/homeassistant/components/forecast_solar/translations/tr.json +++ b/homeassistant/components/forecast_solar/translations/tr.json @@ -15,6 +15,9 @@ } }, "options": { + "error": { + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" + }, "step": { "init": { "data": { @@ -25,7 +28,7 @@ "inverter_size": "\u0130nverter boyutu (Watt)", "modules power": "Solar mod\u00fcllerinizin toplam en y\u00fcksek Watt g\u00fcc\u00fc" }, - "description": "Bu de\u011ferler Solar.Forecast sonucunun ayarlanmas\u0131na izin verir. Bir alan net de\u011filse l\u00fctfen belgelere bak\u0131n." + "description": "Bu de\u011ferler, Forecast.Solar sonucunun de\u011fi\u015ftirilmesine izin verir. Bir alan net de\u011filse l\u00fctfen belgelere bak\u0131n." } } } diff --git a/homeassistant/components/forked_daapd/browse_media.py b/homeassistant/components/forked_daapd/browse_media.py index a4c97d3a035..dee1cd444c3 100644 --- a/homeassistant/components/forked_daapd/browse_media.py +++ b/homeassistant/components/forked_daapd/browse_media.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Sequence from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Union, cast +from typing import TYPE_CHECKING, Any, cast from urllib.parse import quote, unquote from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType @@ -160,7 +160,7 @@ async def get_owntone_content( return create_browse_media_response( master, media_content, - cast(list[dict[str, Union[int, str]]], result), + cast(list[dict[str, int | str]], result), children, ) if media_content.id_or_path == "": # top level search @@ -188,7 +188,7 @@ async def get_owntone_content( return create_browse_media_response( master, media_content, - cast(list[dict[str, Union[int, str]]], result), + cast(list[dict[str, int | str]], result), ) # Not a directory or top level of library # We should have content type and id @@ -214,7 +214,7 @@ async def get_owntone_content( ) return create_browse_media_response( - master, media_content, cast(list[dict[str, Union[int, str]]], result) + master, media_content, cast(list[dict[str, int | str]], result) ) diff --git a/homeassistant/components/forked_daapd/translations/el.json b/homeassistant/components/forked_daapd/translations/el.json index ec29c3de039..6ceef681a22 100644 --- a/homeassistant/components/forked_daapd/translations/el.json +++ b/homeassistant/components/forked_daapd/translations/el.json @@ -2,15 +2,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", - "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." + "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 Owntone." }, "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.", + "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 Owntone.", "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.", + "websocket_not_enabled": "\u03a4\u03bf websocket \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Owntone \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf.", "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." + "wrong_server_type": "\u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 Owntone \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Owntone \u03bc\u03b5 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 > = 27.0." }, "flow_title": "{name} ({host})", "step": { @@ -21,7 +21,7 @@ "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" + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Owntone" } } }, @@ -34,8 +34,8 @@ "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.", - "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd 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 Owntone.", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ce\u03bd Owntone" } } } diff --git a/homeassistant/components/forked_daapd/translations/hu.json b/homeassistant/components/forked_daapd/translations/hu.json index e10e4f19bd2..77b1ed72cdb 100644 --- a/homeassistant/components/forked_daapd/translations/hu.json +++ b/homeassistant/components/forked_daapd/translations/hu.json @@ -5,7 +5,7 @@ "not_forked_daapd": "Az eszk\u00f6z nem Owntone-kiszolg\u00e1l\u00f3." }, "error": { - "forbidden": "Nem siker\u00fclt csatlakozni. K\u00e9rj\u00fck, ellen\u0151rizze az Owntone h\u00e1l\u00f3zati enged\u00e9lyeit.", + "forbidden": "Nem siker\u00fclt csatlakozni. K\u00e9rem, ellen\u0151rizze az Owntone h\u00e1l\u00f3zati enged\u00e9lyeit.", "unknown_error": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", "websocket_not_enabled": "Az Owntone server websocket nincs enged\u00e9lyezve.", "wrong_host_or_port": "A csatlakoz\u00e1s sikertelen. K\u00e9rem, ellen\u0151rizze a c\u00edmet \u00e9s a portot.", diff --git a/homeassistant/components/forked_daapd/translations/lv.json b/homeassistant/components/forked_daapd/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/id.json b/homeassistant/components/foscam/translations/id.json index 89d57eb8e4a..c18a463f62b 100644 --- a/homeassistant/components/foscam/translations/id.json +++ b/homeassistant/components/foscam/translations/id.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", - "invalid_response": "Response tidak valid dari perangkat", + "invalid_response": "Respons tidak valid dari perangkat", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/foscam/translations/lv.json b/homeassistant/components/foscam/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/foscam/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/uk.json b/homeassistant/components/foscam/translations/uk.json new file mode 100644 index 00000000000..337e9e7fa20 --- /dev/null +++ b/homeassistant/components/foscam/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/lv.json b/homeassistant/components/freebox/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/freebox/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/sk.json b/homeassistant/components/freebox/translations/sk.json index f2d872d321b..1bd21738abe 100644 --- a/homeassistant/components/freebox/translations/sk.json +++ b/homeassistant/components/freebox/translations/sk.json @@ -10,7 +10,7 @@ }, "step": { "link": { - "description": "Kliknite na \u201eOdosla\u0165\u201c a potom sa dotknite \u0161\u00edpky doprava na smerova\u010di a zaregistrujte Freebox s Home Assistant. \n\n ![Umiestnenie tla\u010didla na smerova\u010di](/static/images/config_freebox.png)", + "description": "Kliknite na \"Odosla\u0165\" a potom sa dotknite \u0161\u00edpky doprava na smerova\u010di a zaregistrujte Freebox s Home Assistant. \n\n ![Umiestnenie tla\u010didla na smerova\u010di](/static/images/config_freebox.png)", "title": "Prepoji\u0165 router Freebox" }, "user": { diff --git a/homeassistant/components/freedompro/__init__.py b/homeassistant/components/freedompro/__init__.py index 6bd4d03bde0..5e1f8e0b577 100644 --- a/homeassistant/components/freedompro/__init__.py +++ b/homeassistant/components/freedompro/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Final +from typing import Any, Final from pyfreedompro import get_list, get_states @@ -60,14 +60,14 @@ async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> Non await hass.config_entries.async_reload(config_entry.entry_id) -class FreedomproDataUpdateCoordinator(DataUpdateCoordinator): +class FreedomproDataUpdateCoordinator(DataUpdateCoordinator[list[dict[str, Any]]]): """Class to manage fetching Freedompro data API.""" def __init__(self, hass, api_key): """Initialize.""" self._hass = hass self._api_key = api_key - self._devices = None + self._devices: list[dict[str, Any]] | None = None update_interval = timedelta(minutes=1) super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) diff --git a/homeassistant/components/freedompro/binary_sensor.py b/homeassistant/components/freedompro/binary_sensor.py index 3a33c5a2a2c..c56d3cb2ad8 100644 --- a/homeassistant/components/freedompro/binary_sensor.py +++ b/homeassistant/components/freedompro/binary_sensor.py @@ -43,7 +43,7 @@ async def async_setup_entry( ) -class Device(CoordinatorEntity, BinarySensorEntity): +class Device(CoordinatorEntity[FreedomproDataUpdateCoordinator], BinarySensorEntity): """Representation of an Freedompro binary_sensor.""" def __init__( diff --git a/homeassistant/components/freedompro/cover.py b/homeassistant/components/freedompro/cover.py index 265e06802b5..3839415d31b 100644 --- a/homeassistant/components/freedompro/cover.py +++ b/homeassistant/components/freedompro/cover.py @@ -45,7 +45,7 @@ async def async_setup_entry( ) -class Device(CoordinatorEntity, CoverEntity): +class Device(CoordinatorEntity[FreedomproDataUpdateCoordinator], CoverEntity): """Representation of an Freedompro cover.""" def __init__( diff --git a/homeassistant/components/freedompro/light.py b/homeassistant/components/freedompro/light.py index d3f99cbd4e0..7dc573f9225 100644 --- a/homeassistant/components/freedompro/light.py +++ b/homeassistant/components/freedompro/light.py @@ -37,7 +37,7 @@ async def async_setup_entry( ) -class Device(CoordinatorEntity, LightEntity): +class Device(CoordinatorEntity[FreedomproDataUpdateCoordinator], LightEntity): """Representation of an Freedompro light.""" def __init__( diff --git a/homeassistant/components/freedompro/translations/lv.json b/homeassistant/components/freedompro/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/freedompro/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 94053f47284..89c85c77972 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -34,7 +34,7 @@ from homeassistant.helpers import ( entity_registry as er, update_coordinator, ) -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import DeviceInfo from homeassistant.util import dt as dt_util @@ -302,10 +302,12 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): """Event specific per FRITZ!Box entry to signal updates in devices.""" return f"{DOMAIN}-device-update-{self._unique_id}" - def _update_hosts_info(self) -> list[HostInfo]: + async def _async_update_hosts_info(self) -> list[HostInfo]: """Retrieve latest hosts information from the FRITZ!Box.""" try: - return self.fritz_hosts.get_hosts_info() # type: ignore [no-any-return] + return await self.hass.async_add_executor_job( + self.fritz_hosts.get_hosts_info + ) except Exception as ex: # pylint: disable=[broad-except] if not self.hass.is_stopping: raise HomeAssistantError("Error refreshing hosts info") from ex @@ -318,14 +320,22 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): release_url = info.get("NewX_AVM-DE_InfoURL") return bool(version), version, release_url - def _get_wan_access(self, ip_address: str) -> bool | None: + async def _async_update_device_info(self) -> tuple[bool, str | None, str | None]: + """Retrieve latest device information from the FRITZ!Box.""" + return await self.hass.async_add_executor_job(self._update_device_info) + + async def _async_get_wan_access(self, ip_address: str) -> bool | None: """Get WAN access rule for given IP address.""" try: - return not self.connection.call_action( - "X_AVM-DE_HostFilter:1", - "GetWANAccessByIP", - NewIPv4Address=ip_address, - ).get("NewDisallow") + wan_access = await self.hass.async_add_executor_job( + partial( + self.connection.call_action, + "X_AVM-DE_HostFilter:1", + "GetWANAccessByIP", + NewIPv4Address=ip_address, + ) + ) + return not wan_access.get("NewDisallow") except FRITZ_EXCEPTIONS as ex: _LOGGER.debug( ( @@ -337,10 +347,6 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): ) return None - 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: @@ -356,13 +362,13 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): self._devices[dev_mac] = device return True - def send_signal_device_update(self, new_device: bool) -> None: + async def async_send_signal_device_update(self, new_device: bool) -> None: """Signal device data updated.""" - dispatcher_send(self.hass, self.signal_device_update) + async_dispatcher_send(self.hass, self.signal_device_update) if new_device: - dispatcher_send(self.hass, self.signal_device_new) + async_dispatcher_send(self.hass, self.signal_device_new) - def scan_devices(self, now: datetime | None = None) -> None: + async def async_scan_devices(self, now: datetime | None = None) -> None: """Scan for new devices and return a list of found device ids.""" if self.hass.is_stopping: @@ -374,7 +380,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): self._update_available, self._latest_firmware, self._release_url, - ) = self._update_device_info() + ) = await self._async_update_device_info() _LOGGER.debug("Checking devices for FRITZ!Box device %s", self.host) _default_consider_home = DEFAULT_CONSIDER_HOME.total_seconds() @@ -387,7 +393,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): new_device = False hosts = {} - for host in self._update_hosts_info(): + for host in await self._async_update_hosts_info(): if not host.get("mac"): continue @@ -411,14 +417,18 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): self.mesh_role = MeshRoles.NONE for mac, info in hosts.items(): if info.ip_address: - info.wan_access = self._get_wan_access(info.ip_address) + info.wan_access = await self._async_get_wan_access(info.ip_address) if self.manage_device_info(info, mac, consider_home): new_device = True - self.send_signal_device_update(new_device) + await self.async_send_signal_device_update(new_device) return try: - if not (topology := self.fritz_hosts.get_mesh_topology()): + if not ( + topology := await self.hass.async_add_executor_job( + self.fritz_hosts.get_mesh_topology + ) + ): raise Exception("Mesh supported but empty topology reported") except FritzActionError: self.mesh_role = MeshRoles.SLAVE @@ -457,7 +467,9 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): dev_info: Device = hosts[dev_mac] if dev_info.ip_address: - dev_info.wan_access = self._get_wan_access(dev_info.ip_address) + dev_info.wan_access = await self._async_get_wan_access( + dev_info.ip_address + ) for link in interf["node_links"]: intf = mesh_intf.get(link["node_interface_1_uid"]) @@ -472,7 +484,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): if self.manage_device_info(dev_info, dev_mac, consider_home): new_device = True - self.send_signal_device_update(new_device) + await self.async_send_signal_device_update(new_device) async def async_trigger_firmware_update(self) -> bool: """Trigger firmware update.""" @@ -615,7 +627,7 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator[None]): class AvmWrapper(FritzBoxTools): """Setup AVM wrapper for API calls.""" - def _service_call_action( + async def _async_service_call( self, service_name: str, service_suffix: str, @@ -632,10 +644,13 @@ class AvmWrapper(FritzBoxTools): return {} try: - result: dict = self.connection.call_action( - f"{service_name}:{service_suffix}", - action_name, - **kwargs, + result: dict = await self.hass.async_add_executor_job( + partial( + self.connection.call_action, + f"{service_name}:{service_suffix}", + action_name, + **kwargs, + ) ) return result except FritzSecurityError: @@ -666,13 +681,15 @@ class AvmWrapper(FritzBoxTools): async def async_get_upnp_configuration(self) -> dict[str, Any]: """Call X_AVM-DE_UPnP service.""" - return await self.hass.async_add_executor_job(self.get_upnp_configuration) + return await self._async_service_call("X_AVM-DE_UPnP", "1", "GetInfo") async def async_get_wan_link_properties(self) -> dict[str, Any]: """Call WANCommonInterfaceConfig service.""" - return await self.hass.async_add_executor_job( - partial(self.get_wan_link_properties) + return await self._async_service_call( + "WANCommonInterfaceConfig", + "1", + "GetCommonLinkProperties", ) async def async_ipv6_active(self) -> bool: @@ -703,34 +720,49 @@ class AvmWrapper(FritzBoxTools): ) return connection_info + async def async_get_num_port_mapping(self, con_type: str) -> dict[str, Any]: + """Call GetPortMappingNumberOfEntries action.""" + + return await self._async_service_call( + con_type, "1", "GetPortMappingNumberOfEntries" + ) + async def async_get_port_mapping(self, con_type: str, index: int) -> dict[str, Any]: """Call GetGenericPortMappingEntry action.""" - return await self.hass.async_add_executor_job( - partial(self.get_port_mapping, con_type, index) + return await self._async_service_call( + con_type, "1", "GetGenericPortMappingEntry", NewPortMappingIndex=index ) async def async_get_wlan_configuration(self, index: int) -> dict[str, Any]: """Call WLANConfiguration service.""" - return await self.hass.async_add_executor_job( - partial(self.get_wlan_configuration, index) + return await self._async_service_call( + "WLANConfiguration", str(index), "GetInfo" + ) + + async def async_get_ontel_num_deflections(self) -> dict[str, Any]: + """Call GetNumberOfDeflections action from X_AVM-DE_OnTel service.""" + + return await self._async_service_call( + "X_AVM-DE_OnTel", "1", "GetNumberOfDeflections" ) async def async_get_ontel_deflections(self) -> dict[str, Any]: """Call GetDeflections action from X_AVM-DE_OnTel service.""" - return await self.hass.async_add_executor_job( - partial(self.get_ontel_deflections) - ) + return await self._async_service_call("X_AVM-DE_OnTel", "1", "GetDeflections") async def async_set_wlan_configuration( self, index: int, turn_on: bool ) -> dict[str, Any]: """Call SetEnable action from WLANConfiguration service.""" - return await self.hass.async_add_executor_job( - partial(self.set_wlan_configuration, index, turn_on) + return await self._async_service_call( + "WLANConfiguration", + str(index), + "SetEnable", + NewEnable="1" if turn_on else "0", ) async def async_set_deflection_enable( @@ -738,94 +770,7 @@ class AvmWrapper(FritzBoxTools): ) -> dict[str, Any]: """Call SetDeflectionEnable service.""" - return await self.hass.async_add_executor_job( - partial(self.set_deflection_enable, index, turn_on) - ) - - async def async_add_port_mapping( - self, con_type: str, port_mapping: Any - ) -> dict[str, Any]: - """Call AddPortMapping service.""" - - return await self.hass.async_add_executor_job( - partial( - self.add_port_mapping, - con_type, - port_mapping, - ) - ) - - async def async_set_allow_wan_access( - self, ip_address: str, turn_on: bool - ) -> dict[str, Any]: - """Call X_AVM-DE_HostFilter service.""" - - return await self.hass.async_add_executor_job( - partial(self.set_allow_wan_access, ip_address, turn_on) - ) - - def get_upnp_configuration(self) -> dict[str, Any]: - """Call X_AVM-DE_UPnP service.""" - - return self._service_call_action("X_AVM-DE_UPnP", "1", "GetInfo") - - def get_ontel_num_deflections(self) -> dict[str, Any]: - """Call GetNumberOfDeflections action from X_AVM-DE_OnTel service.""" - - return self._service_call_action( - "X_AVM-DE_OnTel", "1", "GetNumberOfDeflections" - ) - - def get_ontel_deflections(self) -> dict[str, Any]: - """Call GetDeflections action from X_AVM-DE_OnTel service.""" - - return self._service_call_action("X_AVM-DE_OnTel", "1", "GetDeflections") - - def get_default_connection(self) -> dict[str, Any]: - """Call Layer3Forwarding service.""" - - return self._service_call_action( - "Layer3Forwarding", "1", "GetDefaultConnectionService" - ) - - def get_num_port_mapping(self, con_type: str) -> dict[str, Any]: - """Call GetPortMappingNumberOfEntries action.""" - - return self._service_call_action(con_type, "1", "GetPortMappingNumberOfEntries") - - def get_port_mapping(self, con_type: str, index: int) -> dict[str, Any]: - """Call GetGenericPortMappingEntry action.""" - - return self._service_call_action( - con_type, "1", "GetGenericPortMappingEntry", NewPortMappingIndex=index - ) - - def get_wlan_configuration(self, index: int) -> dict[str, Any]: - """Call WLANConfiguration service.""" - - return self._service_call_action("WLANConfiguration", str(index), "GetInfo") - - def get_wan_link_properties(self) -> dict[str, Any]: - """Call WANCommonInterfaceConfig service.""" - - return self._service_call_action( - "WANCommonInterfaceConfig", "1", "GetCommonLinkProperties" - ) - - def set_wlan_configuration(self, index: int, turn_on: bool) -> dict[str, Any]: - """Call SetEnable action from WLANConfiguration service.""" - - return self._service_call_action( - "WLANConfiguration", - str(index), - "SetEnable", - NewEnable="1" if turn_on else "0", - ) - - def set_deflection_enable(self, index: int, turn_on: bool) -> dict[str, Any]: - """Call SetDeflectionEnable service.""" - - return self._service_call_action( + return await self._async_service_call( "X_AVM-DE_OnTel", "1", "SetDeflectionEnable", @@ -833,17 +778,24 @@ class AvmWrapper(FritzBoxTools): NewEnable="1" if turn_on else "0", ) - def add_port_mapping(self, con_type: str, port_mapping: Any) -> dict[str, Any]: + async def async_add_port_mapping( + self, con_type: str, port_mapping: Any + ) -> dict[str, Any]: """Call AddPortMapping service.""" - return self._service_call_action( - con_type, "1", "AddPortMapping", **port_mapping + return await self._async_service_call( + con_type, + "1", + "AddPortMapping", + **port_mapping, ) - def set_allow_wan_access(self, ip_address: str, turn_on: bool) -> dict[str, Any]: + async def async_set_allow_wan_access( + self, ip_address: str, turn_on: bool + ) -> dict[str, Any]: """Call X_AVM-DE_HostFilter service.""" - return self._service_call_action( + return await self._async_service_call( "X_AVM-DE_HostFilter", "1", "DisallowWANAccessByIP", diff --git a/homeassistant/components/fritz/diagnostics.py b/homeassistant/components/fritz/diagnostics.py index ed45295892b..0322e55a9e0 100644 --- a/homeassistant/components/fritz/diagnostics.py +++ b/homeassistant/components/fritz/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for AVM FRITZ!Box.""" 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 @@ -14,7 +16,7 @@ TO_REDACT = {CONF_USERNAME, CONF_PASSWORD} async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" avm_wrapper: AvmWrapper = hass.data[DOMAIN][entry.entry_id] diff --git a/homeassistant/components/fritz/switch.py b/homeassistant/components/fritz/switch.py index a45cd347463..a83b39ebbb1 100644 --- a/homeassistant/components/fritz/switch.py +++ b/homeassistant/components/fritz/switch.py @@ -39,14 +39,14 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def deflection_entities_list( +async def _async_deflection_entities_list( avm_wrapper: AvmWrapper, device_friendly_name: str ) -> list[FritzBoxDeflectionSwitch]: """Get list of deflection entities.""" _LOGGER.debug("Setting up %s switches", SWITCH_TYPE_DEFLECTION) - deflections_response = avm_wrapper.get_ontel_num_deflections() + deflections_response = await avm_wrapper.async_get_ontel_num_deflections() if not deflections_response: _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) return [] @@ -61,7 +61,7 @@ def deflection_entities_list( _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) return [] - if not (deflection_list := avm_wrapper.get_ontel_deflections()): + if not (deflection_list := await avm_wrapper.async_get_ontel_deflections()): return [] items = xmltodict.parse(deflection_list["NewDeflectionList"])["List"]["Item"] @@ -74,7 +74,7 @@ def deflection_entities_list( ] -def port_entities_list( +async def _async_port_entities_list( avm_wrapper: AvmWrapper, device_friendly_name: str, local_ip: str ) -> list[FritzBoxPortSwitch]: """Get list of port forwarding entities.""" @@ -86,7 +86,7 @@ def port_entities_list( return [] # Query port forwardings and setup a switch for each forward for the current device - resp = avm_wrapper.get_num_port_mapping(avm_wrapper.device_conn_type) + resp = await avm_wrapper.async_get_num_port_mapping(avm_wrapper.device_conn_type) if not resp: _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) return [] @@ -103,7 +103,9 @@ def port_entities_list( for i in range(port_forwards_count): - portmap = avm_wrapper.get_port_mapping(avm_wrapper.device_conn_type, i) + portmap = await avm_wrapper.async_get_port_mapping( + avm_wrapper.device_conn_type, i + ) if not portmap: _LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION) continue @@ -136,7 +138,7 @@ def port_entities_list( return entities_list -def wifi_entities_list( +async def _async_wifi_entities_list( avm_wrapper: AvmWrapper, device_friendly_name: str ) -> list[FritzBoxWifiSwitch]: """Get list of wifi entities.""" @@ -155,9 +157,7 @@ def wifi_entities_list( _LOGGER.debug("WiFi networks count: %s", wifi_count) networks: dict = {} for i in range(1, wifi_count + 1): - network_info = avm_wrapper.connection.call_action( - f"WLANConfiguration{i}", "GetInfo" - ) + network_info = await avm_wrapper.async_get_wlan_configuration(i) # Devices with 4 WLAN services, use the 2nd for internal communications if not (wifi_count == 4 and i == 2): networks[i] = { @@ -190,7 +190,7 @@ def wifi_entities_list( ] -def profile_entities_list( +async def _async_profile_entities_list( avm_wrapper: AvmWrapper, data_fritz: FritzData, ) -> list[FritzBoxProfileSwitch]: @@ -221,7 +221,7 @@ def profile_entities_list( return new_profiles -def all_entities_list( +async def async_all_entities_list( avm_wrapper: AvmWrapper, device_friendly_name: str, data_fritz: FritzData, @@ -233,10 +233,10 @@ def all_entities_list( return [] return [ - *deflection_entities_list(avm_wrapper, device_friendly_name), - *port_entities_list(avm_wrapper, device_friendly_name, local_ip), - *wifi_entities_list(avm_wrapper, device_friendly_name), - *profile_entities_list(avm_wrapper, data_fritz), + *await _async_deflection_entities_list(avm_wrapper, device_friendly_name), + *await _async_port_entities_list(avm_wrapper, device_friendly_name, local_ip), + *await _async_wifi_entities_list(avm_wrapper, device_friendly_name), + *await _async_profile_entities_list(avm_wrapper, data_fritz), ] @@ -252,8 +252,7 @@ async def async_setup_entry( local_ip = await async_get_source_ip(avm_wrapper.hass, target_ip=avm_wrapper.host) - entities_list = await hass.async_add_executor_job( - all_entities_list, + entities_list = await async_all_entities_list( avm_wrapper, entry.title, data_fritz, @@ -263,12 +262,14 @@ async def async_setup_entry( async_add_entities(entities_list) @callback - def update_avm_device() -> None: + async def async_update_avm_device() -> None: """Update the values of the AVM device.""" - async_add_entities(profile_entities_list(avm_wrapper, data_fritz)) + async_add_entities(await _async_profile_entities_list(avm_wrapper, data_fritz)) entry.async_on_unload( - async_dispatcher_connect(hass, avm_wrapper.signal_device_new, update_avm_device) + async_dispatcher_connect( + hass, avm_wrapper.signal_device_new, async_update_avm_device + ) ) diff --git a/homeassistant/components/fritz/translations/el.json b/homeassistant/components/fritz/translations/el.json index 0cbaa10ef20..e61f9d21bb7 100644 --- a/homeassistant/components/fritz/translations/el.json +++ b/homeassistant/components/fritz/translations/el.json @@ -20,8 +20,8 @@ "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" + "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} \u03c3\u03b1\u03c2", + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf FRITZ!Box Tools" }, "reauth_confirm": { "data": { @@ -38,7 +38,7 @@ "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.", + "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03b5\u03c1\u03b3\u03b1\u03bb\u03b5\u03af\u03b1 FRITZ!Box \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/hu.json b/homeassistant/components/fritz/translations/hu.json index 71e6c0233ae..6e01a1a6da7 100644 --- a/homeassistant/components/fritz/translations/hu.json +++ b/homeassistant/components/fritz/translations/hu.json @@ -20,8 +20,8 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "Felfedezte a FRITZ! Boxot: {name} \n\n A FRITZ! Box Tools be\u00e1ll\u00edt\u00e1sa a {name}", - "title": "A FRITZ! Box Tools be\u00e1ll\u00edt\u00e1sa" + "description": "FRITZ!Box felfedezve: {name} \n\nA FRITZ! Eszk\u00f6z\u00f6k be\u00e1ll\u00edt\u00e1sa a {name} kezel\u00e9s\u00e9hez", + "title": "A FRITZ!Box Eszk\u00f6z\u00f6k be\u00e1ll\u00edt\u00e1sa" }, "reauth_confirm": { "data": { @@ -38,8 +38,8 @@ "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "description": "A FRITZ! Box eszk\u00f6z\u00f6k be\u00e1ll\u00edt\u00e1sa a FRITZ! Box vez\u00e9rl\u00e9s\u00e9hez.\n Minimum sz\u00fcks\u00e9ges: felhaszn\u00e1l\u00f3n\u00e9v, jelsz\u00f3.", - "title": "A FRITZ! Box Tools be\u00e1ll\u00edt\u00e1sa" + "description": "A FRITZ!Box Eszk\u00f6z\u00f6k be\u00e1ll\u00edt\u00e1sa a FRITZ!Box vez\u00e9rl\u00e9s\u00e9hez.\nMinimum sz\u00fcks\u00e9ges: felhaszn\u00e1l\u00f3n\u00e9v, jelsz\u00f3.", + "title": "A FRITZ!Box Eszk\u00f6z\u00f6k be\u00e1ll\u00edt\u00e1sa" } } }, diff --git a/homeassistant/components/fritz/translations/lv.json b/homeassistant/components/fritz/translations/lv.json new file mode 100644 index 00000000000..862ef1ca431 --- /dev/null +++ b/homeassistant/components/fritz/translations/lv.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritz/translations/sk.json b/homeassistant/components/fritz/translations/sk.json index d362deef2d5..39ff5f14daf 100644 --- a/homeassistant/components/fritz/translations/sk.json +++ b/homeassistant/components/fritz/translations/sk.json @@ -47,7 +47,7 @@ "step": { "init": { "data": { - "consider_home": "Sekundy na \u010dakanie, zariadenia \u201edoma\u201c", + "consider_home": "Sekundy na \u010dakanie, zariadenia 'doma'", "old_discovery": "Povoli\u0165 star\u00fa met\u00f3du zis\u0165ovania" } } diff --git a/homeassistant/components/fritz/translations/tr.json b/homeassistant/components/fritz/translations/tr.json index af443cac976..ddeac95ebc2 100644 --- a/homeassistant/components/fritz/translations/tr.json +++ b/homeassistant/components/fritz/translations/tr.json @@ -20,8 +20,8 @@ "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "Bulunan FRITZ!Box: {name} \n\n {name} kontrol etmek i\u00e7in FRITZ!Box Tools'u kurun", - "title": "FRITZ!Box Tools Kurulumu" + "description": "FRITZ!Box'\u0131 ke\u015ffetti: {name} \n\n {name} kontrol etmek i\u00e7in FRITZ!Box Ara\u00e7lar\u0131n\u0131 kurun", + "title": "FRITZ!Box Ara\u00e7lar\u0131'n\u0131 kurun" }, "reauth_confirm": { "data": { @@ -38,8 +38,8 @@ "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "FRITZ!Box'\u0131n\u0131z\u0131 kontrol etmek i\u00e7in FRITZ!Box Tools'u kurun.\n Minimum gerekli: kullan\u0131c\u0131 ad\u0131, \u015fifre.", - "title": "FRITZ!Box Tools Kurulumu" + "description": "FRITZ!Box'\u0131n\u0131z\u0131 kontrol etmek i\u00e7in FRITZ!Box Ara\u00e7lar\u0131n\u0131 kurun.\n Gereken minimum: kullan\u0131c\u0131 ad\u0131, \u015fifre.", + "title": "FRITZ!Box Ara\u00e7lar\u0131'n\u0131 kurun" } } }, diff --git a/homeassistant/components/fritz/translations/uk.json b/homeassistant/components/fritz/translations/uk.json new file mode 100644 index 00000000000..89d75176045 --- /dev/null +++ b/homeassistant/components/fritz/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 FRITZ!Box Tools, \u0449\u043e\u0431 \u043a\u0435\u0440\u0443\u0432\u0430\u0442\u0438 \u0441\u0432\u043e\u0457\u043c FRITZ!Box.\n\u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0438\u0439 \u043c\u0456\u043d\u0456\u043c\u0443\u043c: \u043b\u043e\u0433\u0456\u043d, \u043f\u0430\u0440\u043e\u043b\u044c." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/diagnostics.py b/homeassistant/components/fritzbox/diagnostics.py index 403082c4f90..6c50e1311df 100644 --- a/homeassistant/components/fritzbox/diagnostics.py +++ b/homeassistant/components/fritzbox/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for AVM Fritz!Smarthome.""" 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 @@ -14,7 +16,7 @@ TO_REDACT = {CONF_USERNAME, CONF_PASSWORD} async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" data: dict = hass.data[DOMAIN][entry.entry_id] coordinator: FritzboxDataUpdateCoordinator = data[CONF_COORDINATOR] diff --git a/homeassistant/components/fritzbox/translations/lv.json b/homeassistant/components/fritzbox/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/fritzbox/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/uk.json b/homeassistant/components/fritzbox/translations/uk.json index 5a2d8a1c35e..7d92a9e81aa 100644 --- a/homeassistant/components/fritzbox/translations/uk.json +++ b/homeassistant/components/fritzbox/translations/uk.json @@ -18,6 +18,12 @@ }, "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name}?" }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/fritzbox_callmonitor/translations/lv.json b/homeassistant/components/fritzbox_callmonitor/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/uk.json b/homeassistant/components/fritzbox_callmonitor/translations/uk.json new file mode 100644 index 00000000000..337e9e7fa20 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index 53342864da7..670551ca7c5 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -333,24 +333,28 @@ METER_ENTITY_DESCRIPTIONS: list[SensorEntityDescription] = [ SensorEntityDescription( key="power_factor_phase_1", name="Power factor phase 1", + device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_factor_phase_2", name="Power factor phase 2", + device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_factor_phase_3", name="Power factor phase 3", + device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), SensorEntityDescription( key="power_factor", name="Power factor", + device_class=SensorDeviceClass.POWER_FACTOR, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( diff --git a/homeassistant/components/fronius/translations/lv.json b/homeassistant/components/fronius/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/fronius/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index eff8bc77a9a..b152b2d65d8 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -313,7 +313,7 @@ def _frontend_root(dev_repo_path: str | None) -> pathlib.Path: if dev_repo_path is not None: return pathlib.Path(dev_repo_path) / "hass_frontend" # Keep import here so that we can import frontend without installing reqs - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel import hass_frontend return hass_frontend.where() @@ -702,7 +702,7 @@ async def websocket_get_version( for req in integration.requirements: if req.startswith("home-assistant-frontend=="): - frontend = req.split("==", 1)[1] + frontend = req.removeprefix("home-assistant-frontend==") if frontend is None: connection.send_error(msg["id"], "unknown_version", "Version not found") diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b940afead24..b33668cde4c 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20230110.0"], + "requirements": ["home-assistant-frontend==20230201.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/components/garages_amsterdam/translations/lv.json b/homeassistant/components/garages_amsterdam/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/garages_amsterdam/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic/config_flow.py b/homeassistant/components/generic/config_flow.py index 6fa01ba369e..94a885a7c5d 100644 --- a/homeassistant/components/generic/config_flow.py +++ b/homeassistant/components/generic/config_flow.py @@ -221,7 +221,7 @@ async def async_test_stream( return {} # Import from stream.worker as stream cannot reexport from worker # without forcing the av dependency on default_config - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.components.stream.worker import StreamWorkerError if not isinstance(stream_source, template_helper.Template): diff --git a/homeassistant/components/generic/const.py b/homeassistant/components/generic/const.py index eb376909422..4fd600db381 100644 --- a/homeassistant/components/generic/const.py +++ b/homeassistant/components/generic/const.py @@ -9,8 +9,3 @@ CONF_STILL_IMAGE_URL = "still_image_url" CONF_STREAM_SOURCE = "stream_source" CONF_FRAMERATE = "framerate" GET_IMAGE_TIMEOUT = 10 - -DEFAULT_USERNAME = None -DEFAULT_PASSWORD = None -DEFAULT_IMAGE_URL = None -DEFAULT_STREAM_SOURCE = None diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index 938e685dcab..8fdf6bb04f1 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -2,7 +2,7 @@ "domain": "generic", "name": "Generic Camera", "config_flow": true, - "requirements": ["ha-av==10.0.0", "pillow==9.3.0"], + "requirements": ["ha-av==10.0.0", "pillow==9.4.0"], "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/generic", "codeowners": ["@davet2001"], diff --git a/homeassistant/components/generic/translations/id.json b/homeassistant/components/generic/translations/id.json index 7843f58e7a4..9cdbdf3f507 100644 --- a/homeassistant/components/generic/translations/id.json +++ b/homeassistant/components/generic/translations/id.json @@ -13,7 +13,7 @@ "stream_no_route_to_host": "Tidak dapat menemukan host saat mencoba menyambung ke streaming", "stream_not_permitted": "Operasi tidak diizinkan saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", "template_error": "Kesalahan saat merender templat. Tinjau log untuk info lebih lanjut.", - "timeout": "Tenggang waktu habis saat memuat URL", + "timeout": "Tenggang waktu pemuatan URL habis", "unable_still_load": "Tidak dapat memuat gambar yang valid dari URL gambar diam (mis. host yang tidak valid, URL, atau kegagalan autentikasi). Tinjau log untuk info lebih lanjut.", "unknown": "Kesalahan yang tidak diharapkan" }, @@ -52,7 +52,7 @@ "stream_no_route_to_host": "Tidak dapat menemukan host saat mencoba menyambung ke streaming", "stream_not_permitted": "Operasi tidak diizinkan saat mencoba menyambung ke streaming. Apakah protokol transportasi RTSP salah?", "template_error": "Kesalahan saat merender templat. Tinjau log untuk info lebih lanjut.", - "timeout": "Tenggang waktu habis saat memuat URL", + "timeout": "Tenggang waktu pemuatan URL habis", "unable_still_load": "Tidak dapat memuat gambar yang valid dari URL gambar diam (mis. host yang tidak valid, URL, atau kegagalan autentikasi). Tinjau log untuk info lebih lanjut.", "unknown": "Kesalahan yang tidak diharapkan" }, diff --git a/homeassistant/components/generic/translations/nl.json b/homeassistant/components/generic/translations/nl.json index d6d1d380990..f4aa14ac25a 100644 --- a/homeassistant/components/generic/translations/nl.json +++ b/homeassistant/components/generic/translations/nl.json @@ -7,6 +7,7 @@ "already_exists": "Een camera met deze URL instellingen bestaat al.", "invalid_still_image": "URL heeft geen geldig stilstaand beeld geretourneerd", "no_still_image_or_stream_url": "U moet ten minste een stilstaand beeld of stream-URL specificeren", + "relative_url": "Relatieve URL's zijn niet toegestaan", "stream_io_error": "Input/Output fout bij het proberen te verbinden met stream. Verkeerde RTSP transport protocol?", "stream_no_route_to_host": "Kan de host niet vinden terwijl u verbinding probeert te maken met de stream", "stream_not_permitted": "Operatie niet toegestaan bij poging om verbinding te maken met stream. Verkeerd RTSP transport protocol?", @@ -29,6 +30,12 @@ "verify_ssl": "SSL-certificaat verifi\u00ebren" }, "description": "Voer de instellingen in om verbinding te maken met de camera." + }, + "user_confirm_still": { + "data": { + "confirmed_ok": "Dit beeld ziet er goed uit." + }, + "title": "Voorbeeld" } } }, @@ -37,6 +44,7 @@ "already_exists": "Een camera met deze URL instellingen bestaat al.", "invalid_still_image": "URL heeft geen geldig stilstaand beeld geretourneerd", "no_still_image_or_stream_url": "U moet ten minste een stilstaand beeld of stream-URL specificeren", + "relative_url": "Relatieve URL's zijn niet toegestaan", "stream_io_error": "Input/Output fout bij het proberen te verbinden met stream. Verkeerde RTSP transport protocol?", "stream_no_route_to_host": "Kan de host niet vinden terwijl u verbinding probeert te maken met de stream", "stream_not_permitted": "Operatie niet toegestaan bij poging om verbinding te maken met stream. Verkeerd RTSP transport protocol?", @@ -46,6 +54,12 @@ "unknown": "Onverwachte fout" }, "step": { + "confirm_still": { + "data": { + "confirmed_ok": "Dit beeld ziet er goed uit." + }, + "title": "Voorbeeld" + }, "init": { "data": { "authentication": "Authenticatie", diff --git a/homeassistant/components/generic/translations/uk.json b/homeassistant/components/generic/translations/uk.json new file mode 100644 index 00000000000..34a87ba7ae3 --- /dev/null +++ b/homeassistant/components/generic/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic_hygrostat/humidifier.py b/homeassistant/components/generic_hygrostat/humidifier.py index b8db68fbee9..8ed2711d7cd 100644 --- a/homeassistant/components/generic_hygrostat/humidifier.py +++ b/homeassistant/components/generic_hygrostat/humidifier.py @@ -270,7 +270,7 @@ class GenericHygrostat(HumidifierEntity, RestoreEntity): await self._async_device_turn_off() await self.async_update_ha_state() - async def async_set_humidity(self, humidity: int): + async def async_set_humidity(self, humidity: int) -> None: """Set new target humidity.""" if humidity is None: return diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 9d5bc7f9328..0691de19d76 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -2,7 +2,7 @@ "domain": "geniushub", "name": "Genius Hub", "documentation": "https://www.home-assistant.io/integrations/geniushub", - "requirements": ["geniushub-client==0.6.30"], + "requirements": ["geniushub-client==0.7.0"], "codeowners": ["@zxdavb"], "iot_class": "local_polling", "loggers": ["geniushubclient"] diff --git a/homeassistant/components/geofency/translations/el.json b/homeassistant/components/geofency/translations/el.json index cf51a439e06..436329c0e70 100644 --- a/homeassistant/components/geofency/translations/el.json +++ b/homeassistant/components/geofency/translations/el.json @@ -6,7 +6,7 @@ "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." + "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 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \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 \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\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": { diff --git a/homeassistant/components/geofency/translations/hu.json b/homeassistant/components/geofency/translations/hu.json index 9da5f3622b6..8cc19bbbbe5 100644 --- a/homeassistant/components/geofency/translations/hu.json +++ b/homeassistant/components/geofency/translations/hu.json @@ -6,7 +6,7 @@ "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." }, "create_entry": { - "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtano a webhook funkci\u00f3t a Geofencyben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1lja: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3]({docs_url}) linken tal\u00e1lhat\u00f3k." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni Home Assistantba, be kell \u00e1ll\u00edtania a Geofency webhook funkci\u00f3j\u00e1t. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 adatokat: \n\n - URL: `{webhook_url}`\n - Met\u00f3dus: POST\n\nB\u0151vebb inform\u00e1ci\u00f3 [a dokument\u00e1ci\u00f3ban]({docs_url}) olvashat\u00f3." }, "step": { "user": { diff --git a/homeassistant/components/geofency/translations/tr.json b/homeassistant/components/geofency/translations/tr.json index 8cd04ad16c7..ea226aff6d0 100644 --- a/homeassistant/components/geofency/translations/tr.json +++ b/homeassistant/components/geofency/translations/tr.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, "create_entry": { - "default": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in Geofency'de webhook \u00f6zelli\u011fini ayarlaman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url}" + "default": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in Geofency'de webhook \u00f6zelli\u011fini kurman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url} ) bak\u0131n." }, "step": { "user": { diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json index dd62682e304..ac3d1f8114d 100644 --- a/homeassistant/components/geonetnz_quakes/manifest.json +++ b/homeassistant/components/geonetnz_quakes/manifest.json @@ -3,7 +3,7 @@ "name": "GeoNet NZ Quakes", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/geonetnz_quakes", - "requirements": ["aio_geojson_geonetnz_quakes==0.13"], + "requirements": ["aio_geojson_geonetnz_quakes==0.15"], "codeowners": ["@exxamalte"], "quality_scale": "platinum", "iot_class": "cloud_polling", diff --git a/homeassistant/components/geonetnz_volcano/manifest.json b/homeassistant/components/geonetnz_volcano/manifest.json index 7c765ecb939..d4a2c344dba 100644 --- a/homeassistant/components/geonetnz_volcano/manifest.json +++ b/homeassistant/components/geonetnz_volcano/manifest.json @@ -3,7 +3,7 @@ "name": "GeoNet NZ Volcano", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/geonetnz_volcano", - "requirements": ["aio_geojson_geonetnz_volcano==0.6"], + "requirements": ["aio_geojson_geonetnz_volcano==0.8"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", "loggers": ["aio_geojson_geonetnz_volcano"], diff --git a/homeassistant/components/gios/diagnostics.py b/homeassistant/components/gios/diagnostics.py index b056473fbec..954580d5c3f 100644 --- a/homeassistant/components/gios/diagnostics.py +++ b/homeassistant/components/gios/diagnostics.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import asdict +from typing import Any from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -12,7 +13,7 @@ from .const import DOMAIN async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: GiosDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index feb3f90a313..cabbb671aed 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -59,7 +59,6 @@ SENSOR_TYPES: tuple[GiosSensorEntityDescription, ...] = ( GiosSensorEntityDescription( key=ATTR_AQI, name="AQI", - device_class=SensorDeviceClass.AQI, value=None, ), GiosSensorEntityDescription( diff --git a/homeassistant/components/github/coordinator.py b/homeassistant/components/github/coordinator.py index 679c3d89aeb..45ab055aa9a 100644 --- a/homeassistant/components/github/coordinator.py +++ b/homeassistant/components/github/coordinator.py @@ -136,9 +136,9 @@ class GitHubDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): # These are unexpected and we log the trace to help with troubleshooting LOGGER.exception(exception) raise UpdateFailed(exception) from exception - else: - self._last_response = response - return response.data["data"]["repository"] + + self._last_response = response + return response.data["data"]["repository"] async def _handle_event(self, event: GitHubEventModel) -> None: """Handle an event.""" diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 7e9d767f20d..ee959943b82 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -196,6 +196,7 @@ SENSOR_TYPES: tuple[GlancesSensorEntityDescription, ...] = ( type="sensors", name_suffix="Charge", native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.BATTERY, icon="mdi:battery", state_class=SensorStateClass.MEASUREMENT, ), diff --git a/homeassistant/components/glances/translations/lt.json b/homeassistant/components/glances/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/glances/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/translations/lv.json b/homeassistant/components/glances/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/glances/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/coordinator.py b/homeassistant/components/goalzero/coordinator.py index 416b420f29d..61c3a8dba29 100644 --- a/homeassistant/components/goalzero/coordinator.py +++ b/homeassistant/components/goalzero/coordinator.py @@ -11,7 +11,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DOMAIN, LOGGER -class GoalZeroDataUpdateCoordinator(DataUpdateCoordinator): +class GoalZeroDataUpdateCoordinator(DataUpdateCoordinator[None]): """Data update coordinator for the Goal zero integration.""" config_entry: ConfigEntry diff --git a/homeassistant/components/goalzero/switch.py b/homeassistant/components/goalzero/switch.py index 25fdeb52114..ac4872bba32 100644 --- a/homeassistant/components/goalzero/switch.py +++ b/homeassistant/components/goalzero/switch.py @@ -52,10 +52,10 @@ class GoalZeroSwitch(GoalZeroEntity, SwitchEntity): """Turn off the switch.""" payload = {self.entity_description.key: 0} await self._api.post_state(payload=payload) - self.coordinator.async_set_updated_data(data=payload) + self.coordinator.async_set_updated_data(None) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" payload = {self.entity_description.key: 1} await self._api.post_state(payload=payload) - self.coordinator.async_set_updated_data(data=payload) + self.coordinator.async_set_updated_data(None) diff --git a/homeassistant/components/goalzero/translations/el.json b/homeassistant/components/goalzero/translations/el.json index 6e793797046..2aee9c9dfc3 100644 --- a/homeassistant/components/goalzero/translations/el.json +++ b/homeassistant/components/goalzero/translations/el.json @@ -19,7 +19,7 @@ "host": "\u0394\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2", "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, - "description": "\u03a0\u03c1\u03ce\u03c4\u03b1, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03ba\u03b1\u03c4\u03b5\u03b2\u03ac\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae Goal Zero: https://www.goalzero.com/product-features/yeti-app/ \n\n \u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf Yeti \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf Wi-Fi. \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 \u03bf \u0392\u03bf\u03b7\u03b8\u03cc\u03c2 \u039f\u03b9\u03ba\u03af\u03b1\u03c2 \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." + "description": "\u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03b1\u03c0\u03b1\u03b9\u03c4\u03ae\u03c3\u03b5\u03b9\u03c2." } } } diff --git a/homeassistant/components/gogogate2/translations/el.json b/homeassistant/components/gogogate2/translations/el.json index 373d36bb397..ae8725c03b9 100644 --- a/homeassistant/components/gogogate2/translations/el.json +++ b/homeassistant/components/gogogate2/translations/el.json @@ -16,7 +16,7 @@ "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" + "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 Gogogate2 \u03ae \u03c4\u03bf\u03c5 ismartgate" } } } diff --git a/homeassistant/components/gogogate2/translations/lt.json b/homeassistant/components/gogogate2/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goodwe/translations/lv.json b/homeassistant/components/goodwe/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/goodwe/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index bc5e814b24d..934b34c126b 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -191,8 +191,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryAuthFailed from err except ApiException as err: raise ConfigEntryNotReady from err - else: - hass.config_entries.async_update_entry(entry, unique_id=primary_calendar.id) + + hass.config_entries.async_update_entry(entry, unique_id=primary_calendar.id) # Only expose the add event service if we have the correct permissions if get_feature_access(hass, entry) is FeatureAccess.read_write: @@ -327,7 +327,7 @@ def load_config(path: str) -> dict[str, Any]: calendars = {} try: with open(path, encoding="utf8") as file: - data = yaml.safe_load(file) + data = yaml.safe_load(file) or [] for calendar in data: calendars[calendar[CONF_CAL_ID]] = DEVICE_SCHEMA(calendar) except FileNotFoundError as err: diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 702146ee052..2cbf122da01 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -6,7 +6,7 @@ import asyncio from collections.abc import Iterable from datetime import datetime, timedelta import logging -from typing import Any, Union, cast +from typing import Any, cast from gcal_sync.api import ( GoogleCalendarService, @@ -19,9 +19,9 @@ from gcal_sync.model import AccessRole, DateOrDatetime, Event from gcal_sync.store import ScopedCalendarStore from gcal_sync.sync import CalendarEventSyncManager from gcal_sync.timeline import Timeline -import voluptuous as vol from homeassistant.components.calendar import ( + CREATE_EVENT_SCHEMA, ENTITY_ID_FORMAT, EVENT_DESCRIPTION, EVENT_END, @@ -38,11 +38,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITIES, CONF_NAME, CONF_OFFSET from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError, PlatformNotReady -from homeassistant.helpers import ( - config_validation as cv, - entity_platform, - entity_registry as er, -) +from homeassistant.helpers import entity_platform, entity_registry as er from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( @@ -74,7 +70,6 @@ from .const import ( EVENT_IN_WEEKS, EVENT_START_DATE, EVENT_START_DATETIME, - EVENT_TYPES_CONF, FeatureAccess, ) @@ -95,41 +90,7 @@ OPAQUE = "opaque" # we need to strip when working with the frontend recurrence rule values RRULE_PREFIX = "RRULE:" -_EVENT_IN_TYPES = vol.Schema( - { - vol.Exclusive(EVENT_IN_DAYS, EVENT_TYPES_CONF): cv.positive_int, - vol.Exclusive(EVENT_IN_WEEKS, EVENT_TYPES_CONF): cv.positive_int, - } -) - SERVICE_CREATE_EVENT = "create_event" -CREATE_EVENT_SCHEMA = vol.All( - cv.has_at_least_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN), - cv.has_at_most_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN), - cv.make_entity_service_schema( - { - vol.Required(EVENT_SUMMARY): cv.string, - vol.Optional(EVENT_DESCRIPTION, default=""): cv.string, - vol.Inclusive( - EVENT_START_DATE, "dates", "Start and end dates must both be specified" - ): cv.date, - vol.Inclusive( - EVENT_END_DATE, "dates", "Start and end dates must both be specified" - ): cv.date, - vol.Inclusive( - EVENT_START_DATETIME, - "datetimes", - "Start and end datetimes must both be specified", - ): cv.datetime, - vol.Inclusive( - EVENT_END_DATETIME, - "datetimes", - "Start and end datetimes must both be specified", - ): cv.datetime, - vol.Optional(EVENT_IN): _EVENT_IN_TYPES, - } - ), -) async def async_setup_entry( @@ -392,9 +353,7 @@ class CalendarQueryUpdateCoordinator(DataUpdateCoordinator[list[Event]]): class GoogleCalendarEntity( - CoordinatorEntity[ - Union[CalendarSyncUpdateCoordinator, CalendarQueryUpdateCoordinator] - ], + CoordinatorEntity[CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator], CalendarEntity, ): """A calendar event entity.""" @@ -546,9 +505,12 @@ class GoogleCalendarEntity( if rrule := kwargs.get(EVENT_RRULE): event.recurrence = [f"{RRULE_PREFIX}{rrule}"] - await cast( - CalendarSyncUpdateCoordinator, self.coordinator - ).sync.store_service.async_add_event(event) + try: + await cast( + CalendarSyncUpdateCoordinator, self.coordinator + ).sync.store_service.async_add_event(event) + except ApiException as err: + raise HomeAssistantError(f"Error while creating event: {str(err)}") from err await self.coordinator.async_refresh() async def async_delete_event( diff --git a/homeassistant/components/google/translations/el.json b/homeassistant/components/google/translations/el.json index 21e5580da3d..4e13026136f 100644 --- a/homeassistant/components/google/translations/el.json +++ b/homeassistant/components/google/translations/el.json @@ -28,7 +28,7 @@ "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 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", + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u0397\u03bc\u03b5\u03c1\u03bf\u03bb\u03bf\u03b3\u03af\u03bf\u03c5 Google \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/google/translations/id.json b/homeassistant/components/google/translations/id.json index 20ed21a56be..de8673a3758 100644 --- a/homeassistant/components/google/translations/id.json +++ b/homeassistant/components/google/translations/id.json @@ -12,7 +12,7 @@ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", "oauth_error": "Menerima respons token yang tidak valid.", "reauth_successful": "Autentikasi ulang berhasil", - "timeout_connect": "Tenggang waktu membuat koneksi habis" + "timeout_connect": "Tenggang waktu pembuatan koneksi habis" }, "create_entry": { "default": "Berhasil diautentikasi" diff --git a/homeassistant/components/google/translations/ru.json b/homeassistant/components/google/translations/ru.json index 5fc2cb03feb..9c9e8da033d 100644 --- a/homeassistant/components/google/translations/ru.json +++ b/homeassistant/components/google/translations/ru.json @@ -1,6 +1,6 @@ { "application_credentials": { - "description": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c]({more_info_url}) \u043d\u0430 [\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 OAuth]({oauth_consent_url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c Home Assistant \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u041a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u044e Google. \u0422\u0430\u043a\u0436\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441 \u0432\u0430\u0448\u0438\u043c \u043a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u0435\u043c:\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439]({oauth_creds_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f**.\n2. \u0412 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0435\u043c \u0441\u043f\u0438\u0441\u043a\u0435 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth**.\n3. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **TV and Limited Input devices** \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0422\u0438\u043f\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f." + "description": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c]({more_info_url}) \u043d\u0430 [\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 OAuth]({oauth_consent_url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c Home Assistant \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u041a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u044e Google. \u0422\u0430\u043a\u0436\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441 \u0412\u0430\u0448\u0438\u043c \u043a\u0430\u043b\u0435\u043d\u0434\u0430\u0440\u0435\u043c:\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 [Credentials]({oauth_creds_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 **Create Credentials**.\n2. \u0412 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0435\u043c \u0441\u043f\u0438\u0441\u043a\u0435 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **OAuth client ID**.\n3. \u0412 \u043f\u043e\u043b\u0435 **Application Type** \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **TV and Limited Input devices**." }, "config": { "abort": { diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 82868828531..3a0315a5931 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -166,6 +166,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler ) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 3f9c90d40e1..2a011679d09 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from asyncio import gather -from collections.abc import Mapping +from collections.abc import Callable, Mapping from datetime import datetime, timedelta from http import HTTPStatus import logging @@ -85,7 +85,7 @@ def _get_registry_entries( class AbstractConfig(ABC): """Hold the configuration for Google Assistant.""" - _unsub_report_state = None + _unsub_report_state: Callable[[], None] | None = None def __init__(self, hass): """Initialize abstract config.""" @@ -197,7 +197,7 @@ class AbstractConfig(ABC): def async_enable_report_state(self): """Enable proactive mode.""" # Circular dep - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .report_state import async_enable_report_state if self._unsub_report_state is None: @@ -338,7 +338,7 @@ class AbstractConfig(ABC): async def _handle_local_webhook(self, hass, webhook_id, request): """Handle an incoming local SDK message.""" # Circular dep - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from . import smart_home self._local_last_active = utcnow() diff --git a/homeassistant/components/google_assistant/logbook.py b/homeassistant/components/google_assistant/logbook.py index ac12ae2cb8c..9559188cbd2 100644 --- a/homeassistant/components/google_assistant/logbook.py +++ b/homeassistant/components/google_assistant/logbook.py @@ -17,9 +17,7 @@ def async_describe_events(hass, async_describe_event): commands = [] for command_payload in event.data["execution"]: - command = command_payload["command"] - if command.startswith(COMMON_COMMAND_PREFIX): - command = command[len(COMMON_COMMAND_PREFIX) :] + command = command_payload["command"].removeprefix(COMMON_COMMAND_PREFIX) commands.append(command) message = f"sent command {', '.join(commands)}" diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 920a63b70f8..71abdf1758d 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, TypeVar from homeassistant.components import ( alarm_control_panel, @@ -161,13 +161,15 @@ COMMAND_SELECT_CHANNEL = f"{PREFIX_COMMANDS}selectChannel" COMMAND_LOCATE = f"{PREFIX_COMMANDS}Locate" COMMAND_CHARGE = f"{PREFIX_COMMANDS}Charge" -TRAITS = [] +TRAITS: list[type[_Trait]] = [] FAN_SPEED_MAX_SPEED_COUNT = 5 +_TraitT = TypeVar("_TraitT", bound="_Trait") -def register_trait(trait): - """Decorate a function to register a trait.""" + +def register_trait(trait: type[_TraitT]) -> type[_TraitT]: + """Decorate a class to register a trait.""" TRAITS.append(trait) return trait @@ -288,7 +290,7 @@ class CameraStreamTrait(_Trait): name = TRAIT_CAMERA_STREAM commands = [COMMAND_GET_CAMERA_STREAM] - stream_info = None + stream_info: dict[str, str] | None = None @staticmethod def supported(domain, features, device_class, _): diff --git a/homeassistant/components/google_assistant_sdk/__init__.py b/homeassistant/components/google_assistant_sdk/__init__.py index 119ba9e1d27..93699321eda 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -2,29 +2,45 @@ from __future__ import annotations import aiohttp +from gassist_text import TextAssistant +from google.oauth2.credentials import Credentials import voluptuous as vol +from homeassistant.components import conversation from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.const import CONF_NAME, Platform +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, Platform from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation as cv, discovery, intent from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, async_get_config_entry_implementation, ) from homeassistant.helpers.typing import ConfigType -from .const import DOMAIN -from .helpers import async_send_text_commands +from .const import ( + CONF_ENABLE_CONVERSATION_AGENT, + CONF_LANGUAGE_CODE, + DATA_MEM_STORAGE, + DATA_SESSION, + DOMAIN, +) +from .helpers import ( + GoogleAssistantSDKAudioView, + InMemoryStorage, + async_send_text_commands, + default_language_code, +) SERVICE_SEND_TEXT_COMMAND = "send_text_command" SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND = "command" +SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER = "media_player" SERVICE_SEND_TEXT_COMMAND_SCHEMA = vol.All( { vol.Required(SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND): vol.All( - str, vol.Length(min=1) + cv.ensure_list, [vol.All(str, vol.Length(min=1))] ), + vol.Optional(SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER): cv.comp_entity_ids, }, ) @@ -42,6 +58,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Google Assistant SDK from a config entry.""" + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {} + implementation = await async_get_config_entry_implementation(hass, entry) session = OAuth2Session(hass, entry, implementation) try: @@ -54,10 +72,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady from err except aiohttp.ClientError as err: raise ConfigEntryNotReady from err - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = session + hass.data[DOMAIN][entry.entry_id][DATA_SESSION] = session + + mem_storage = InMemoryStorage(hass) + hass.data[DOMAIN][entry.entry_id][DATA_MEM_STORAGE] = mem_storage + hass.http.register_view(GoogleAssistantSDKAudioView(mem_storage)) await async_setup_service(hass) + entry.async_on_unload(entry.add_update_listener(update_listener)) + await update_listener(hass, entry) + return True @@ -73,6 +98,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: for service_name in hass.services.async_services()[DOMAIN]: hass.services.async_remove(DOMAIN, service_name) + if entry.options.get(CONF_ENABLE_CONVERSATION_AGENT, False): + conversation.async_unset_agent(hass, entry) + return True @@ -81,8 +109,11 @@ async def async_setup_service(hass: HomeAssistant) -> None: async def send_text_command(call: ServiceCall) -> None: """Send a text command to Google Assistant SDK.""" - command: str = call.data[SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND] - await async_send_text_commands([command], hass) + commands: list[str] = call.data[SERVICE_SEND_TEXT_COMMAND_FIELD_COMMAND] + media_players: list[str] | None = call.data.get( + SERVICE_SEND_TEXT_COMMAND_FIELD_MEDIA_PLAYER + ) + await async_send_text_commands(hass, commands, media_players) hass.services.async_register( DOMAIN, @@ -90,3 +121,59 @@ async def async_setup_service(hass: HomeAssistant) -> None: send_text_command, schema=SERVICE_SEND_TEXT_COMMAND_SCHEMA, ) + + +async def update_listener(hass, entry): + """Handle options update.""" + if entry.options.get(CONF_ENABLE_CONVERSATION_AGENT, False): + agent = GoogleAssistantConversationAgent(hass, entry) + conversation.async_set_agent(hass, entry, agent) + else: + conversation.async_unset_agent(hass, entry) + + +class GoogleAssistantConversationAgent(conversation.AbstractConversationAgent): + """Google Assistant SDK conversation agent.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the agent.""" + self.hass = hass + self.entry = entry + self.assistant: TextAssistant | None = None + self.session: OAuth2Session | None = None + + @property + def attribution(self): + """Return the attribution.""" + return { + "name": "Powered by Google Assistant SDK", + "url": "https://www.home-assistant.io/integrations/google_assistant_sdk/", + } + + async def async_process( + self, user_input: conversation.ConversationInput + ) -> conversation.ConversationResult: + """Process a sentence.""" + if self.session: + session = self.session + else: + session = self.hass.data[DOMAIN][self.entry.entry_id][DATA_SESSION] + self.session = session + if not session.valid_token: + await session.async_ensure_token_valid() + self.assistant = None + if not self.assistant: + credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) + language_code = self.entry.options.get( + CONF_LANGUAGE_CODE, default_language_code(self.hass) + ) + self.assistant = TextAssistant(credentials, language_code) + + resp = self.assistant.assist(user_input.text) + text_response = resp[0] or "" + + intent_response = intent.IntentResponse(language=user_input.language) + intent_response.async_set_speech(text_response) + return conversation.ConversationResult( + response=intent_response, conversation_id=user_input.conversation_id + ) diff --git a/homeassistant/components/google_assistant_sdk/config_flow.py b/homeassistant/components/google_assistant_sdk/config_flow.py index b4f617ca029..b93a3be93f2 100644 --- a/homeassistant/components/google_assistant_sdk/config_flow.py +++ b/homeassistant/components/google_assistant_sdk/config_flow.py @@ -13,7 +13,13 @@ from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_entry_oauth2_flow -from .const import CONF_LANGUAGE_CODE, DEFAULT_NAME, DOMAIN, SUPPORTED_LANGUAGE_CODES +from .const import ( + CONF_ENABLE_CONVERSATION_AGENT, + CONF_LANGUAGE_CODE, + DEFAULT_NAME, + DOMAIN, + SUPPORTED_LANGUAGE_CODES, +) from .helpers import default_language_code _LOGGER = logging.getLogger(__name__) @@ -108,6 +114,12 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_LANGUAGE_CODE, default=self.config_entry.options.get(CONF_LANGUAGE_CODE), ): vol.In(SUPPORTED_LANGUAGE_CODES), + vol.Required( + CONF_ENABLE_CONVERSATION_AGENT, + default=self.config_entry.options.get( + CONF_ENABLE_CONVERSATION_AGENT + ), + ): bool, } ), ) diff --git a/homeassistant/components/google_assistant_sdk/const.py b/homeassistant/components/google_assistant_sdk/const.py index acd9a405343..c9f86160bb4 100644 --- a/homeassistant/components/google_assistant_sdk/const.py +++ b/homeassistant/components/google_assistant_sdk/const.py @@ -5,8 +5,12 @@ DOMAIN: Final = "google_assistant_sdk" DEFAULT_NAME: Final = "Google Assistant SDK" +CONF_ENABLE_CONVERSATION_AGENT: Final = "enable_conversation_agent" CONF_LANGUAGE_CODE: Final = "language_code" +DATA_MEM_STORAGE: Final = "mem_storage" +DATA_SESSION: Final = "session" + # https://developers.google.com/assistant/sdk/reference/rpc/languages SUPPORTED_LANGUAGE_CODES: Final = [ "de-DE", @@ -20,5 +24,7 @@ SUPPORTED_LANGUAGE_CODES: Final = [ "fr-CA", "fr-FR", "it-IT", + "ja-JP", + "ko-KR", "pt-BR", ] diff --git a/homeassistant/components/google_assistant_sdk/helpers.py b/homeassistant/components/google_assistant_sdk/helpers.py index 15e325f10c1..1c85e5b6a4b 100644 --- a/homeassistant/components/google_assistant_sdk/helpers.py +++ b/homeassistant/components/google_assistant_sdk/helpers.py @@ -1,18 +1,38 @@ """Helper classes for Google Assistant SDK integration.""" from __future__ import annotations +from http import HTTPStatus import logging +from typing import Any +import uuid import aiohttp +from aiohttp import web from gassist_text import TextAssistant from google.oauth2.credentials import Credentials +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.media_player import ( + ATTR_MEDIA_ANNOUNCE, + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + DOMAIN as DOMAIN_MP, + SERVICE_PLAY_MEDIA, + MediaType, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session +from homeassistant.helpers.event import async_call_later -from .const import CONF_LANGUAGE_CODE, DOMAIN, SUPPORTED_LANGUAGE_CODES +from .const import ( + CONF_LANGUAGE_CODE, + DATA_MEM_STORAGE, + DATA_SESSION, + DOMAIN, + SUPPORTED_LANGUAGE_CODES, +) _LOGGER = logging.getLogger(__name__) @@ -22,16 +42,20 @@ DEFAULT_LANGUAGE_CODES = { "es": "es-ES", "fr": "fr-FR", "it": "it-IT", + "ja": "ja-JP", + "ko": "ko-KR", "pt": "pt-BR", } -async def async_send_text_commands(commands: list[str], hass: HomeAssistant) -> None: +async def async_send_text_commands( + hass: HomeAssistant, commands: list[str], media_players: list[str] | None = None +) -> None: """Send text commands to Google Assistant Service.""" # There can only be 1 entry (config_flow has single_instance_allowed) entry: ConfigEntry = hass.config_entries.async_entries(DOMAIN)[0] - session: OAuth2Session = hass.data[DOMAIN].get(entry.entry_id) + session: OAuth2Session = hass.data[DOMAIN][entry.entry_id][DATA_SESSION] try: await session.async_ensure_token_valid() except aiohttp.ClientResponseError as err: @@ -41,10 +65,32 @@ async def async_send_text_commands(commands: list[str], hass: HomeAssistant) -> credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) language_code = entry.options.get(CONF_LANGUAGE_CODE, default_language_code(hass)) - with TextAssistant(credentials, language_code) as assistant: + with TextAssistant( + credentials, language_code, audio_out=bool(media_players) + ) as assistant: for command in commands: - text_response = assistant.assist(command)[0] + resp = assistant.assist(command) + text_response = resp[0] _LOGGER.debug("command: %s\nresponse: %s", command, text_response) + audio_response = resp[2] + if media_players and audio_response: + mem_storage: InMemoryStorage = hass.data[DOMAIN][entry.entry_id][ + DATA_MEM_STORAGE + ] + audio_url = GoogleAssistantSDKAudioView.url.format( + filename=mem_storage.store_and_get_identifier(audio_response) + ) + await hass.services.async_call( + DOMAIN_MP, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_players, + ATTR_MEDIA_CONTENT_ID: audio_url, + ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, + ATTR_MEDIA_ANNOUNCE: True, + }, + blocking=True, + ) def default_language_code(hass: HomeAssistant): @@ -53,3 +99,53 @@ def default_language_code(hass: HomeAssistant): if language_code in SUPPORTED_LANGUAGE_CODES: return language_code return DEFAULT_LANGUAGE_CODES.get(hass.config.language, "en-US") + + +class InMemoryStorage: + """Temporarily store and retrieve data from in memory storage.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize InMemoryStorage.""" + self.hass: HomeAssistant = hass + self.mem: dict[str, bytes] = {} + + def store_and_get_identifier(self, data: bytes) -> str: + """ + Temporarily store data and return identifier to be able to retrieve it. + + Data expires after 5 minutes. + """ + identifier: str = uuid.uuid1().hex + self.mem[identifier] = data + + def async_remove_from_mem(*_: Any) -> None: + """Cleanup memory.""" + self.mem.pop(identifier, None) + + # Remove the entry from memory 5 minutes later + async_call_later(self.hass, 5 * 60, async_remove_from_mem) + + return identifier + + def retrieve(self, identifier: str) -> bytes | None: + """Retrieve previously stored data.""" + return self.mem.get(identifier) + + +class GoogleAssistantSDKAudioView(HomeAssistantView): + """Google Assistant SDK view to serve audio responses.""" + + requires_auth = True + url = "/api/google_assistant_sdk/audio/{filename}" + name = "api:google_assistant_sdk:audio" + + def __init__(self, mem_storage: InMemoryStorage) -> None: + """Initialize GoogleAssistantSDKView.""" + self.mem_storage: InMemoryStorage = mem_storage + + async def get(self, request: web.Request, filename: str) -> web.Response: + """Start a get request.""" + audio = self.mem_storage.retrieve(filename) + if not audio: + return web.Response(status=HTTPStatus.NOT_FOUND) + return web.Response(body=audio, content_type="audio/mpeg") diff --git a/homeassistant/components/google_assistant_sdk/manifest.json b/homeassistant/components/google_assistant_sdk/manifest.json index e1b390f9496..cc1f2b474b9 100644 --- a/homeassistant/components/google_assistant_sdk/manifest.json +++ b/homeassistant/components/google_assistant_sdk/manifest.json @@ -2,9 +2,9 @@ "domain": "google_assistant_sdk", "name": "Google Assistant SDK", "config_flow": true, - "dependencies": ["application_credentials"], + "dependencies": ["application_credentials", "http"], "documentation": "https://www.home-assistant.io/integrations/google_assistant_sdk/", - "requirements": ["gassist-text==0.0.7"], + "requirements": ["gassist-text==0.0.10"], "codeowners": ["@tronikos"], "iot_class": "cloud_polling", "integration_type": "service" diff --git a/homeassistant/components/google_assistant_sdk/notify.py b/homeassistant/components/google_assistant_sdk/notify.py index 3872a1df2a3..80d0e70f44c 100644 --- a/homeassistant/components/google_assistant_sdk/notify.py +++ b/homeassistant/components/google_assistant_sdk/notify.py @@ -13,12 +13,14 @@ from .helpers import async_send_text_commands, default_language_code # https://support.google.com/assistant/answer/9071582?hl=en LANG_TO_BROADCAST_COMMAND = { - "en": ("broadcast", "broadcast to"), - "de": ("Nachricht an alle", "Nachricht an alle an"), - "es": ("Anuncia", "Anuncia en"), - "fr": ("Diffuse", "Diffuse dans"), - "it": ("Trasmetti", "Trasmetti in"), - "pt": ("Transmite", "Transmite para"), + "en": ("broadcast {0}", "broadcast to {1} {0}"), + "de": ("Nachricht an alle {0}", "Nachricht an alle an {1} {0}"), + "es": ("Anuncia {0}", "Anuncia en {1} {0}"), + "fr": ("Diffuse {0}", "Diffuse dans {1} {0}"), + "it": ("Trasmetti {0}", "Trasmetti in {1} {0}"), + "ja": ("{0}とブロードキャストして", "{0}と{1}にブロードキャストして"), + "ko": ("{0} 라고 방송해 줘", "{0} 라고 {1}에 방송해 줘"), + "pt": ("Transmite {0}", "Transmite para {1} {0}"), } @@ -62,10 +64,10 @@ class BroadcastNotificationService(BaseNotificationService): commands = [] targets = kwargs.get(ATTR_TARGET) if not targets: - commands.append(f"{broadcast_commands(language_code)[0]} {message}") + commands.append(broadcast_commands(language_code)[0].format(message)) else: for target in targets: commands.append( - f"{broadcast_commands(language_code)[1]} {target} {message}" + broadcast_commands(language_code)[1].format(message, target) ) - await async_send_text_commands(commands, self.hass) + await async_send_text_commands(self.hass, commands) diff --git a/homeassistant/components/google_assistant_sdk/services.yaml b/homeassistant/components/google_assistant_sdk/services.yaml index b9d4e8635de..fc2a3ad264f 100644 --- a/homeassistant/components/google_assistant_sdk/services.yaml +++ b/homeassistant/components/google_assistant_sdk/services.yaml @@ -4,7 +4,14 @@ send_text_command: fields: command: name: Command - description: Command to send to Google Assistant. + description: Command(s) to send to Google Assistant. example: turn off kitchen TV selector: text: + media_player: + name: Media Player Entity + description: Name(s) of media player entities to play response on + example: media_player.living_room_speaker + selector: + entity: + domain: media_player diff --git a/homeassistant/components/google_assistant_sdk/strings.json b/homeassistant/components/google_assistant_sdk/strings.json index 66a2b975b5e..d4c85be91e5 100644 --- a/homeassistant/components/google_assistant_sdk/strings.json +++ b/homeassistant/components/google_assistant_sdk/strings.json @@ -31,8 +31,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Enable the conversation agent", "language_code": "Language code" - } + }, + "description": "Set language for interactions with Google Assistant and whether you want to enable the conversation agent." } } }, diff --git a/homeassistant/components/google_assistant_sdk/translations/bg.json b/homeassistant/components/google_assistant_sdk/translations/bg.json index dcd006d0e14..d99db48deaf 100644 --- a/homeassistant/components/google_assistant_sdk/translations/bg.json +++ b/homeassistant/components/google_assistant_sdk/translations/bg.json @@ -27,8 +27,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0433\u0435\u043d\u0442\u0430 \u0437\u0430 \u0440\u0430\u0437\u0433\u043e\u0432\u043e\u0440", "language_code": "\u0415\u0437\u0438\u043a\u043e\u0432 \u043a\u043e\u0434" - } + }, + "description": "\u0417\u0430\u0434\u0430\u0439\u0442\u0435 \u0435\u0437\u0438\u043a \u0437\u0430 \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u0441 Google Assistant \u0438 \u0434\u0430\u043b\u0438 \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u0442\u0435 \u0430\u0433\u0435\u043d\u0442\u0430 \u0437\u0430 \u0440\u0430\u0437\u0433\u043e\u0432\u043e\u0440." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/ca.json b/homeassistant/components/google_assistant_sdk/translations/ca.json index 41c8bb575af..e408f8b15dc 100644 --- a/homeassistant/components/google_assistant_sdk/translations/ca.json +++ b/homeassistant/components/google_assistant_sdk/translations/ca.json @@ -1,6 +1,6 @@ { "application_credentials": { - "description": "Segueix les [instruccions]({more_info_url}) de [la pantalla de consentiment OAuth]({oauth_consent_url}) perqu\u00e8 Home Assistant tingui acc\u00e9s als teu Google Assistant SDK. Tamb\u00e9 has de crear les credencials d'aplicaci\u00f3 enlla\u00e7ades al teu compte:\n 1. V\u00e9s a [Credencials]({oauth_creds_url}) i fes clic a **Crear credencials**.\n 2. A la llista desplegable, selecciona **ID de client OAuth**.\n 3. Selecciona **Aplicaci\u00f3 Web** al tipus d'aplicaci\u00f3.\n \n " + "description": "Seguiu les [instruccions]({more_info_url}) de [la pantalla de consentiment OAuth]({oauth_consent_url}) perqu\u00e8 Home Assistant tingui acc\u00e9s al vostre Google Assistant SDK. Tamb\u00e9 heu de crear les credencials d'aplicaci\u00f3 enlla\u00e7ades al vostre compte:\n 1. Aneu a [Credencials]({oauth_creds_url}) i feu clic a **Crear credencials**.\n 2. A la llista desplegable, seleccioneu **ID de client OAuth**.\n 3. Seleccioneu **Aplicaci\u00f3 Web** al tipus d'aplicaci\u00f3.\n \n " }, "config": { "abort": { @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Activa l'agent de conversa", "language_code": "Codi d'idioma" - } + }, + "description": "Defineix l'idioma de les interaccions amb Google Assisant i tria si voleu activar l'agent de conversa." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/cs.json b/homeassistant/components/google_assistant_sdk/translations/cs.json new file mode 100644 index 00000000000..3b814303e69 --- /dev/null +++ b/homeassistant/components/google_assistant_sdk/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/google_assistant_sdk/translations/de.json b/homeassistant/components/google_assistant_sdk/translations/de.json index 73a6b908ea2..84d80f0a36f 100644 --- a/homeassistant/components/google_assistant_sdk/translations/de.json +++ b/homeassistant/components/google_assistant_sdk/translations/de.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Aktiviere den Konversationsagenten", "language_code": "Sprachcode" - } + }, + "description": "Lege die Sprache f\u00fcr Interaktionen mit Google Assistant fest und ob du den Konversationsagenten aktivieren m\u00f6chtest." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/el.json b/homeassistant/components/google_assistant_sdk/translations/el.json index a735860a746..65cf48b1f77 100644 --- a/homeassistant/components/google_assistant_sdk/translations/el.json +++ b/homeassistant/components/google_assistant_sdk/translations/el.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd \u03b5\u03ba\u03c0\u03c1\u03cc\u03c3\u03c9\u03c0\u03bf \u03c3\u03c5\u03bd\u03bf\u03bc\u03b9\u03bb\u03af\u03b1\u03c2", "language_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b3\u03bb\u03ce\u03c3\u03c3\u03b1\u03c2" - } + }, + "description": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b3\u03bb\u03ce\u03c3\u03c3\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03b9\u03c2 \u03b1\u03bb\u03bb\u03b7\u03bb\u03b5\u03c0\u03b9\u03b4\u03c1\u03ac\u03c3\u03b5\u03b9\u03c2 \u03bc\u03b5 \u03c4\u03bf\u03bd \u0392\u03bf\u03b7\u03b8\u03cc Google \u03ba\u03b1\u03b9 \u03b5\u03ac\u03bd \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c0\u03b1\u03c1\u03ac\u03b3\u03bf\u03bd\u03c4\u03b1 \u03c3\u03c5\u03bd\u03bf\u03bc\u03b9\u03bb\u03af\u03b1\u03c2." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/en.json b/homeassistant/components/google_assistant_sdk/translations/en.json index 36d28427ca2..4a8d8bfce60 100644 --- a/homeassistant/components/google_assistant_sdk/translations/en.json +++ b/homeassistant/components/google_assistant_sdk/translations/en.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Enable the conversation agent", "language_code": "Language code" - } + }, + "description": "Set language for interactions with Google Assistant and whether you want to enable the conversation agent." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/es.json b/homeassistant/components/google_assistant_sdk/translations/es.json index 18513dec620..409ecc8493f 100644 --- a/homeassistant/components/google_assistant_sdk/translations/es.json +++ b/homeassistant/components/google_assistant_sdk/translations/es.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Habilitar el agente de conversaci\u00f3n", "language_code": "C\u00f3digo de idioma" - } + }, + "description": "Configura el idioma para las interacciones con el Asistente de Google y si deseas habilitar el agente de conversaci\u00f3n." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/et.json b/homeassistant/components/google_assistant_sdk/translations/et.json index b060d69f795..957626d4929 100644 --- a/homeassistant/components/google_assistant_sdk/translations/et.json +++ b/homeassistant/components/google_assistant_sdk/translations/et.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "V\u00f5ta vestlusagent kasutusele", "language_code": "Keele kood" - } + }, + "description": "M\u00e4\u00e4ra keel Google'i assistendiga suhtlemiseks ja kas soovid vestlusagendi lubada." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/hu.json b/homeassistant/components/google_assistant_sdk/translations/hu.json index e39c135827d..ce0c2e811fa 100644 --- a/homeassistant/components/google_assistant_sdk/translations/hu.json +++ b/homeassistant/components/google_assistant_sdk/translations/hu.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "A besz\u00e9lget\u00e9si \u00fcgyn\u00f6k enged\u00e9lyez\u00e9se", "language_code": "Nyelvi k\u00f3d" - } + }, + "description": "\u00c1ll\u00edtsa be a Google Asszisztenssel folytatott interakci\u00f3k nyelv\u00e9t, \u00e9s azt, hogy szeretn\u00e9-e enged\u00e9lyezni a besz\u00e9lget\u00e9si \u00fcgyn\u00f6k\u00f6t." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/id.json b/homeassistant/components/google_assistant_sdk/translations/id.json index 0ab64cd9529..3f4d08d2204 100644 --- a/homeassistant/components/google_assistant_sdk/translations/id.json +++ b/homeassistant/components/google_assistant_sdk/translations/id.json @@ -11,7 +11,7 @@ "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", "oauth_error": "Menerima respons token yang tidak valid.", "reauth_successful": "Autentikasi ulang berhasil", - "timeout_connect": "Tenggang waktu membuat koneksi habis", + "timeout_connect": "Tenggang waktu pembuatan koneksi habis", "unknown": "Kesalahan yang tidak diharapkan" }, "create_entry": { @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Aktifkan agen percakapan", "language_code": "Kode bahasa" - } + }, + "description": "Tetapkan bahasa untuk interaksi dengan Asisten Google dan apakah Anda ingin mengaktifkan agen percakapan." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/it.json b/homeassistant/components/google_assistant_sdk/translations/it.json index 03e2421fc86..92791c192ba 100644 --- a/homeassistant/components/google_assistant_sdk/translations/it.json +++ b/homeassistant/components/google_assistant_sdk/translations/it.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Abilita l'agente di conversazione", "language_code": "Codice lingua" - } + }, + "description": "Imposta la lingua per le interazioni con Google Assistant e se desideri abilitare l'agente di conversazione." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/nl.json b/homeassistant/components/google_assistant_sdk/translations/nl.json index 66849b98b82..e860a2a5a9a 100644 --- a/homeassistant/components/google_assistant_sdk/translations/nl.json +++ b/homeassistant/components/google_assistant_sdk/translations/nl.json @@ -15,6 +15,9 @@ "default": "Authenticatie geslaagd" }, "step": { + "auth": { + "title": "Google-account koppelen" + }, "pick_implementation": { "title": "Kies een authenticatie methode" }, diff --git a/homeassistant/components/google_assistant_sdk/translations/no.json b/homeassistant/components/google_assistant_sdk/translations/no.json index 0a4a81a8b30..60550a69a5f 100644 --- a/homeassistant/components/google_assistant_sdk/translations/no.json +++ b/homeassistant/components/google_assistant_sdk/translations/no.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Aktiver samtaleagenten", "language_code": "Spr\u00e5kkode" - } + }, + "description": "Angi spr\u00e5k for interaksjoner med Google Assistant og om du vil aktivere samtaleagenten." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/pl.json b/homeassistant/components/google_assistant_sdk/translations/pl.json index 0ad35200fe6..134c1506fb2 100644 --- a/homeassistant/components/google_assistant_sdk/translations/pl.json +++ b/homeassistant/components/google_assistant_sdk/translations/pl.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "W\u0142\u0105cz agenta konwersacji", "language_code": "Kod j\u0119zyka" - } + }, + "description": "Ustaw j\u0119zyk interakcji z Asystentem Google i czy chcesz w\u0142\u0105czy\u0107 agenta konwersacji." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/pt-BR.json b/homeassistant/components/google_assistant_sdk/translations/pt-BR.json index 9864778e52d..c06344180f2 100644 --- a/homeassistant/components/google_assistant_sdk/translations/pt-BR.json +++ b/homeassistant/components/google_assistant_sdk/translations/pt-BR.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Habilitar o agente de conversa", "language_code": "C\u00f3digo do idioma" - } + }, + "description": "Defina o idioma para intera\u00e7\u00f5es com o Google Assistant e se deseja ativar o agente de conversa\u00e7\u00e3o." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/ru.json b/homeassistant/components/google_assistant_sdk/translations/ru.json index 493185dc2d0..9f6ad6cbc2a 100644 --- a/homeassistant/components/google_assistant_sdk/translations/ru.json +++ b/homeassistant/components/google_assistant_sdk/translations/ru.json @@ -1,6 +1,6 @@ { "application_credentials": { - "description": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c]({more_info_url}) \u043d\u0430 [\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 OAuth]({oauth_consent_url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c Home Assistant \u0434\u043e\u0441\u0442\u0443\u043f \u043a Google Assistant SDK. \u0422\u0430\u043a\u0436\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441 \u0412\u0430\u0448\u0438\u043c \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u043e\u043c:\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439]({oauth_creds_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f**.\n2. \u0412 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0435\u043c \u0441\u043f\u0438\u0441\u043a\u0435 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth**.\n3. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0412\u0435\u0431 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435** \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0422\u0438\u043f\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f." + "description": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c]({more_info_url}) \u043d\u0430 [\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 OAuth]({oauth_consent_url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c Home Assistant \u0434\u043e\u0441\u0442\u0443\u043f \u043a Google Assistant SDK. \u0422\u0430\u043a\u0436\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441 \u0412\u0430\u0448\u0438\u043c \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u043e\u043c:\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 [Credentials]({oauth_creds_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 **Create Credentials**.\n2. \u0412 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0435\u043c \u0441\u043f\u0438\u0441\u043a\u0435 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **OAuth client ID**.\n3. \u0412 \u043f\u043e\u043b\u0435 **Application Type** \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **Web application**." }, "config": { "abort": { @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0430\u0433\u0435\u043d\u0442 \u0434\u0438\u0430\u043b\u043e\u0433\u0430", "language_code": "\u041a\u043e\u0434 \u044f\u0437\u044b\u043a\u0430" - } + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u044f\u0437\u044b\u043a \u0434\u043b\u044f \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441 Google Assistant \u0438 \u0443\u043a\u0430\u0436\u0438\u0442\u0435, \u0445\u043e\u0442\u0438\u0442\u0435 \u043b\u0438 \u0412\u044b \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0430\u0433\u0435\u043d\u0442 \u0434\u0438\u0430\u043b\u043e\u0433\u0430." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/sk.json b/homeassistant/components/google_assistant_sdk/translations/sk.json index 2c710d13a70..f02ff3abf04 100644 --- a/homeassistant/components/google_assistant_sdk/translations/sk.json +++ b/homeassistant/components/google_assistant_sdk/translations/sk.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "Povolenie agenta pre konverz\u00e1ciu", "language_code": "K\u00f3d jazyka" - } + }, + "description": "Nastavte jazyk pre interakcie s Asistentom Google a \u010di chcete povoli\u0165 agenta konverz\u00e1cie." } } } diff --git a/homeassistant/components/google_assistant_sdk/translations/tr.json b/homeassistant/components/google_assistant_sdk/translations/tr.json new file mode 100644 index 00000000000..4ffbe3a77cb --- /dev/null +++ b/homeassistant/components/google_assistant_sdk/translations/tr.json @@ -0,0 +1,44 @@ +{ + "application_credentials": { + "description": "Home Assistant'\u0131n Google Asistan SDK'n\u0131za eri\u015fmesine izin vermek i\u00e7in [OAuth izin ekran\u0131]( {oauth_consent_url} ) i\u00e7in [talimatlar\u0131]( {more_info_url} ) uygulay\u0131n. Ayr\u0131ca, hesab\u0131n\u0131za ba\u011fl\u0131 Uygulama Kimlik Bilgileri olu\u015fturman\u0131z gerekir:\n 1. [Kimlik Bilgileri]( {oauth_creds_url} ) sayfas\u0131na gidin ve **Kimlik Bilgileri Olu\u015ftur**'a t\u0131klay\u0131n.\n 1. A\u00e7\u0131l\u0131r listeden **OAuth istemci kimli\u011fi**'ni se\u00e7in.\n 1. Uygulama T\u00fcr\u00fc olarak **Web uygulamas\u0131**'n\u0131 se\u00e7in. \n\n" + }, + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_access_token": "Ge\u00e7ersiz eri\u015fim anahtar\u0131", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "oauth_error": "Ge\u00e7ersiz anahtar verileri al\u0131nd\u0131.", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "timeout_connect": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131", + "unknown": "Beklenmeyen hata" + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, + "step": { + "auth": { + "title": "Google Hesab\u0131n\u0131 Ba\u011fla" + }, + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + }, + "reauth_confirm": { + "description": "Google Asistan SDK entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor", + "title": "Entegrasyonu Yeniden Do\u011frula" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "enable_conversation_agent": "Konu\u015fma arac\u0131s\u0131n\u0131 etkinle\u015ftir", + "language_code": "Dil kodu" + }, + "description": "Google Asistan ile etkile\u015fimler i\u00e7in dili ve konu\u015fma arac\u0131s\u0131n\u0131 etkinle\u015ftirmek isteyip istemedi\u011finizi ayarlay\u0131n." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_assistant_sdk/translations/uk.json b/homeassistant/components/google_assistant_sdk/translations/uk.json new file mode 100644 index 00000000000..cf006b0a05c --- /dev/null +++ b/homeassistant/components/google_assistant_sdk/translations/uk.json @@ -0,0 +1,13 @@ +{ + "options": { + "step": { + "init": { + "data": { + "enable_conversation_agent": "\u0423\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c \u0430\u0433\u0435\u043d\u0442 \u0440\u043e\u0437\u043c\u043e\u0432\u0438", + "language_code": "\u041a\u043e\u0434 \u043c\u043e\u0432\u0438" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043c\u043e\u0432\u0443 \u0434\u043b\u044f \u0432\u0437\u0430\u0454\u043c\u043e\u0434\u0456\u0457 \u0437 Google Assistant \u0456 \u0447\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0438 \u0432\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0430\u0433\u0435\u043d\u0442 \u0440\u043e\u0437\u043c\u043e\u0432\u0438." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_assistant_sdk/translations/zh-Hant.json b/homeassistant/components/google_assistant_sdk/translations/zh-Hant.json index 3212505e74e..fbafdc2ed7c 100644 --- a/homeassistant/components/google_assistant_sdk/translations/zh-Hant.json +++ b/homeassistant/components/google_assistant_sdk/translations/zh-Hant.json @@ -34,8 +34,10 @@ "step": { "init": { "data": { + "enable_conversation_agent": "\u555f\u7528\u5c0d\u8a71\u52a9\u7406", "language_code": "\u8a9e\u8a00\u4ee3\u78bc" - } + }, + "description": "\u8a2d\u5b9a\u8207 Google Assistant \u4e92\u52d5\u8a9e\u8a00\u4ee5\u53ca\u662f\u5426\u8981\u555f\u7528\u5c0d\u8a71\u52a9\u7406\u3002" } } } diff --git a/homeassistant/components/google_mail/__init__.py b/homeassistant/components/google_mail/__init__.py new file mode 100644 index 00000000000..a24d5c17874 --- /dev/null +++ b/homeassistant/components/google_mail/__init__.py @@ -0,0 +1,81 @@ +"""Support for Google Mail.""" +from __future__ import annotations + +from aiohttp.client_exceptions import ClientError, ClientResponseError + +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.const import CONF_NAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import discovery +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.config_entry_oauth2_flow import ( + OAuth2Session, + async_get_config_entry_implementation, +) +from homeassistant.helpers.typing import ConfigType + +from .api import AsyncConfigEntryAuth +from .const import DATA_AUTH, DATA_HASS_CONFIG, DOMAIN +from .services import async_setup_services + +PLATFORMS = [Platform.NOTIFY, Platform.SENSOR] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Google Mail platform.""" + hass.data.setdefault(DOMAIN, {})[DATA_HASS_CONFIG] = config + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Google Mail from a config entry.""" + implementation = await async_get_config_entry_implementation(hass, entry) + session = OAuth2Session(hass, entry, implementation) + auth = AsyncConfigEntryAuth(async_get_clientsession(hass), session) + try: + await auth.check_and_refresh_token() + except ClientResponseError as err: + if 400 <= err.status < 500: + raise ConfigEntryAuthFailed( + "OAuth session is not valid, reauth required" + ) from err + raise ConfigEntryNotReady from err + except ClientError as err: + raise ConfigEntryNotReady from err + hass.data[DOMAIN][entry.entry_id] = auth + + hass.async_create_task( + discovery.async_load_platform( + hass, + Platform.NOTIFY, + DOMAIN, + {DATA_AUTH: auth, CONF_NAME: entry.title}, + hass.data[DOMAIN][DATA_HASS_CONFIG], + ) + ) + + await hass.config_entries.async_forward_entry_setups( + entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY] + ) + + await async_setup_services(hass) + + 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) + loaded_entries = [ + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + if len(loaded_entries) == 1: + for service_name in hass.services.async_services()[DOMAIN]: + hass.services.async_remove(DOMAIN, service_name) + + return unload_ok diff --git a/homeassistant/components/google_mail/api.py b/homeassistant/components/google_mail/api.py new file mode 100644 index 00000000000..202fa5b56b6 --- /dev/null +++ b/homeassistant/components/google_mail/api.py @@ -0,0 +1,41 @@ +"""API for Google Mail bound to Home Assistant OAuth.""" +from aiohttp import ClientSession +from google.auth.exceptions import RefreshError +from google.oauth2.credentials import Credentials +from google.oauth2.utils import OAuthClientAuthHandler +from googleapiclient.discovery import Resource, build + +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.helpers import config_entry_oauth2_flow + + +class AsyncConfigEntryAuth(OAuthClientAuthHandler): + """Provide Google Mail authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + websession: ClientSession, + oauth2Session: config_entry_oauth2_flow.OAuth2Session, + ) -> None: + """Initialize Google Mail Auth.""" + self.oauth_session = oauth2Session + super().__init__(websession) + + @property + def access_token(self) -> str: + """Return the access token.""" + return self.oauth_session.token[CONF_ACCESS_TOKEN] + + async def check_and_refresh_token(self) -> str: + """Check the token.""" + await self.oauth_session.async_ensure_token_valid() + return self.access_token + + async def get_resource(self) -> Resource: + """Get current resource.""" + try: + credentials = Credentials(await self.check_and_refresh_token()) + except RefreshError as ex: + self.oauth_session.config_entry.async_start_reauth(self.oauth_session.hass) + raise ex + return build("gmail", "v1", credentials=credentials) diff --git a/homeassistant/components/google_mail/application_credentials.py b/homeassistant/components/google_mail/application_credentials.py new file mode 100644 index 00000000000..0b3b1dfd056 --- /dev/null +++ b/homeassistant/components/google_mail/application_credentials.py @@ -0,0 +1,20 @@ +"""application_credentials platform for Google Mail.""" +from homeassistant.components.application_credentials import AuthorizationServer +from homeassistant.core import HomeAssistant + + +async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: + """Return authorization server.""" + return AuthorizationServer( + "https://accounts.google.com/o/oauth2/v2/auth", + "https://oauth2.googleapis.com/token", + ) + + +async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]: + """Return description placeholders for the credentials dialog.""" + return { + "oauth_consent_url": "https://console.cloud.google.com/apis/credentials/consent", + "more_info_url": "https://www.home-assistant.io/integrations/google_mail/", + "oauth_creds_url": "https://console.cloud.google.com/apis/credentials", + } diff --git a/homeassistant/components/google_mail/config_flow.py b/homeassistant/components/google_mail/config_flow.py new file mode 100644 index 00000000000..0552f57bf5c --- /dev/null +++ b/homeassistant/components/google_mail/config_flow.py @@ -0,0 +1,85 @@ +"""Config flow for Google Mail integration.""" +from __future__ import annotations + +from collections.abc import Mapping +import logging +from typing import Any, cast + +from google.oauth2.credentials import Credentials +from googleapiclient.discovery import build + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_entry_oauth2_flow + +from .const import DEFAULT_ACCESS, DOMAIN + + +class OAuth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle Google Mail OAuth2 authentication.""" + + DOMAIN = DOMAIN + + reauth_entry: ConfigEntry | None = None + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + @property + def extra_authorize_data(self) -> dict[str, Any]: + """Extra data that needs to be appended to the authorize url.""" + return { + "scope": " ".join(DEFAULT_ACCESS), + # Add params to ensure we get back a refresh token + "access_type": "offline", + "prompt": "consent", + } + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm reauth dialog.""" + if user_input is None: + return self.async_show_form(step_id="reauth_confirm") + return await self.async_step_user() + + async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: + """Create an entry for the flow, or update existing entry.""" + + def _get_profile() -> str: + """Get profile from inside the executor.""" + users = build( # pylint: disable=no-member + "gmail", "v1", credentials=credentials + ).users() + return users.getProfile(userId="me").execute()["emailAddress"] + + credentials = Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN]) + email = await self.hass.async_add_executor_job(_get_profile) + + if not self.reauth_entry: + await self.async_set_unique_id(email) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=email, data=data) + + if self.reauth_entry.unique_id == email: + self.hass.config_entries.async_update_entry(self.reauth_entry, data=data) + await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_abort( + reason="wrong_account", + description_placeholders={"email": cast(str, self.reauth_entry.unique_id)}, + ) diff --git a/homeassistant/components/google_mail/const.py b/homeassistant/components/google_mail/const.py new file mode 100644 index 00000000000..6e70ea9838c --- /dev/null +++ b/homeassistant/components/google_mail/const.py @@ -0,0 +1,25 @@ +"""Constants for Google Mail integration.""" +from __future__ import annotations + +ATTR_BCC = "bcc" +ATTR_CC = "cc" +ATTR_ENABLED = "enabled" +ATTR_END = "end" +ATTR_FROM = "from" +ATTR_ME = "me" +ATTR_MESSAGE = "message" +ATTR_PLAIN_TEXT = "plain_text" +ATTR_RESTRICT_CONTACTS = "restrict_contacts" +ATTR_RESTRICT_DOMAIN = "restrict_domain" +ATTR_SEND = "send" +ATTR_START = "start" +ATTR_TITLE = "title" + +DATA_AUTH = "auth" +DATA_HASS_CONFIG = "hass_config" +DEFAULT_ACCESS = [ + "https://www.googleapis.com/auth/gmail.compose", + "https://www.googleapis.com/auth/gmail.settings.basic", +] +DOMAIN = "google_mail" +MANUFACTURER = "Google, Inc." diff --git a/homeassistant/components/google_mail/entity.py b/homeassistant/components/google_mail/entity.py new file mode 100644 index 00000000000..5e447125e82 --- /dev/null +++ b/homeassistant/components/google_mail/entity.py @@ -0,0 +1,32 @@ +"""Entity representing a Google Mail account.""" +from __future__ import annotations + +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription + +from .api import AsyncConfigEntryAuth +from .const import DOMAIN, MANUFACTURER + + +class GoogleMailEntity(Entity): + """An HA implementation for Google Mail entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + auth: AsyncConfigEntryAuth, + description: EntityDescription, + ) -> None: + """Initialize a Google Mail entity.""" + self.auth = auth + self.entity_description = description + self._attr_unique_id = ( + f"{auth.oauth_session.config_entry.entry_id}_{description.key}" + ) + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, auth.oauth_session.config_entry.entry_id)}, + manufacturer=MANUFACTURER, + name=auth.oauth_session.config_entry.unique_id, + ) diff --git a/homeassistant/components/google_mail/manifest.json b/homeassistant/components/google_mail/manifest.json new file mode 100644 index 00000000000..6e4757aa619 --- /dev/null +++ b/homeassistant/components/google_mail/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "google_mail", + "name": "Google Mail", + "config_flow": true, + "dependencies": ["application_credentials"], + "documentation": "https://www.home-assistant.io/integrations/google_mail/", + "requirements": ["google-api-python-client==2.71.0"], + "codeowners": ["@tkdrob"], + "iot_class": "cloud_polling", + "integration_type": "service" +} diff --git a/homeassistant/components/google_mail/notify.py b/homeassistant/components/google_mail/notify.py new file mode 100644 index 00000000000..eba38c32491 --- /dev/null +++ b/homeassistant/components/google_mail/notify.py @@ -0,0 +1,65 @@ +"""Notification service for Google Mail integration.""" +from __future__ import annotations + +import base64 +from email.message import EmailMessage +from typing import Any + +from googleapiclient.http import HttpRequest + +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_MESSAGE, + ATTR_TARGET, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + BaseNotificationService, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .api import AsyncConfigEntryAuth +from .const import ATTR_BCC, ATTR_CC, ATTR_FROM, ATTR_ME, ATTR_SEND, DATA_AUTH + + +async def async_get_service( + hass: HomeAssistant, + config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, +) -> GMailNotificationService | None: + """Get the notification service.""" + return GMailNotificationService(discovery_info) if discovery_info else None + + +class GMailNotificationService(BaseNotificationService): + """Define the Google Mail notification logic.""" + + def __init__(self, config: dict[str, Any]) -> None: + """Initialize the service.""" + self.auth: AsyncConfigEntryAuth = config[DATA_AUTH] + + async def async_send_message(self, message: str, **kwargs: Any) -> None: + """Send a message.""" + data: dict[str, Any] = kwargs.get(ATTR_DATA) or {} + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + + email = EmailMessage() + email.set_content(message) + if to_addrs := kwargs.get(ATTR_TARGET): + email["To"] = ", ".join(to_addrs) + email["From"] = data.get(ATTR_FROM, ATTR_ME) + email["Subject"] = title + email[ATTR_CC] = ", ".join(data.get(ATTR_CC, [])) + email[ATTR_BCC] = ", ".join(data.get(ATTR_BCC, [])) + + encoded_message = base64.urlsafe_b64encode(email.as_bytes()).decode() + body = {"raw": encoded_message} + msg: HttpRequest + users = (await self.auth.get_resource()).users() + if data.get(ATTR_SEND) is False: + msg = users.drafts().create(userId=email["From"], body={ATTR_MESSAGE: body}) + else: + if not to_addrs: + raise ValueError("recipient address required") + msg = users.messages().send(userId=email["From"], body=body) + await self.hass.async_add_executor_job(msg.execute) diff --git a/homeassistant/components/google_mail/sensor.py b/homeassistant/components/google_mail/sensor.py new file mode 100644 index 00000000000..c30ea1c0a65 --- /dev/null +++ b/homeassistant/components/google_mail/sensor.py @@ -0,0 +1,52 @@ +"""Support for Google Mail Sensors.""" +from __future__ import annotations + +from datetime import datetime, timedelta, timezone + +from googleapiclient.http import HttpRequest + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import GoogleMailEntity + +SCAN_INTERVAL = timedelta(minutes=15) + +SENSOR_TYPE = SensorEntityDescription( + key="vacation_end_date", + name="Vacation end date", + icon="mdi:clock", + device_class=SensorDeviceClass.TIMESTAMP, +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Google Mail sensor.""" + async_add_entities( + [GoogleMailSensor(hass.data[DOMAIN][entry.entry_id], SENSOR_TYPE)], True + ) + + +class GoogleMailSensor(GoogleMailEntity, SensorEntity): + """Representation of a Google Mail sensor.""" + + async def async_update(self) -> None: + """Get the vacation data.""" + service = await self.auth.get_resource() + settings: HttpRequest = service.users().settings().getVacation(userId="me") + data = await self.hass.async_add_executor_job(settings.execute) + + if data["enableAutoReply"]: + value = datetime.fromtimestamp(int(data["endTime"]) / 1000, tz=timezone.utc) + else: + value = None + self._attr_native_value = value diff --git a/homeassistant/components/google_mail/services.py b/homeassistant/components/google_mail/services.py new file mode 100644 index 00000000000..1450a5d31b8 --- /dev/null +++ b/homeassistant/components/google_mail/services.py @@ -0,0 +1,95 @@ +"""Services for Google Mail integration.""" +from __future__ import annotations + +from datetime import datetime, timedelta + +from googleapiclient.http import HttpRequest +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.service import async_extract_config_entry_ids + +from .api import AsyncConfigEntryAuth +from .const import ( + ATTR_ENABLED, + ATTR_END, + ATTR_ME, + ATTR_MESSAGE, + ATTR_PLAIN_TEXT, + ATTR_RESTRICT_CONTACTS, + ATTR_RESTRICT_DOMAIN, + ATTR_START, + ATTR_TITLE, + DOMAIN, +) + +SERVICE_SET_VACATION = "set_vacation" + +SERVICE_VACATION_SCHEMA = vol.All( + cv.make_entity_service_schema( + { + vol.Required(ATTR_ENABLED, default=True): cv.boolean, + vol.Optional(ATTR_TITLE): cv.string, + vol.Required(ATTR_MESSAGE): cv.string, + vol.Optional(ATTR_PLAIN_TEXT, default=True): cv.boolean, + vol.Optional(ATTR_RESTRICT_CONTACTS): cv.boolean, + vol.Optional(ATTR_RESTRICT_DOMAIN): cv.boolean, + vol.Optional(ATTR_START): cv.date, + vol.Optional(ATTR_END): cv.date, + }, + ) +) + + +async def async_setup_services(hass: HomeAssistant) -> None: + """Set up services for Google Mail integration.""" + + async def extract_gmail_config_entries(call: ServiceCall) -> list[ConfigEntry]: + return [ + entry + for entry_id in await async_extract_config_entry_ids(hass, call) + if (entry := hass.config_entries.async_get_entry(entry_id)) + and entry.domain == DOMAIN + ] + + async def gmail_service(call: ServiceCall) -> None: + """Call Google Mail service.""" + auth: AsyncConfigEntryAuth + for entry in await extract_gmail_config_entries(call): + if not (auth := hass.data[DOMAIN].get(entry.entry_id)): + raise ValueError(f"Config entry not loaded: {entry.entry_id}") + service = await auth.get_resource() + + _settings = { + "enableAutoReply": call.data[ATTR_ENABLED], + "responseSubject": call.data.get(ATTR_TITLE), + } + if contacts := call.data.get(ATTR_RESTRICT_CONTACTS): + _settings["restrictToContacts"] = contacts + if domain := call.data.get(ATTR_RESTRICT_DOMAIN): + _settings["restrictToDomain"] = domain + if _date := call.data.get(ATTR_START): + _dt = datetime.combine(_date, datetime.min.time()) + _settings["startTime"] = _dt.timestamp() * 1000 + if _date := call.data.get(ATTR_END): + _dt = datetime.combine(_date, datetime.min.time()) + _settings["endTime"] = (_dt + timedelta(days=1)).timestamp() * 1000 + if call.data[ATTR_PLAIN_TEXT]: + _settings["responseBodyPlainText"] = call.data[ATTR_MESSAGE] + else: + _settings["responseBodyHtml"] = call.data[ATTR_MESSAGE] + settings: HttpRequest = ( + service.users() + .settings() + .updateVacation(userId=ATTR_ME, body=_settings) + ) + await hass.async_add_executor_job(settings.execute) + + hass.services.async_register( + domain=DOMAIN, + service=SERVICE_SET_VACATION, + schema=SERVICE_VACATION_SCHEMA, + service_func=gmail_service, + ) diff --git a/homeassistant/components/google_mail/services.yaml b/homeassistant/components/google_mail/services.yaml new file mode 100644 index 00000000000..76ef40fa3aa --- /dev/null +++ b/homeassistant/components/google_mail/services.yaml @@ -0,0 +1,53 @@ +set_vacation: + name: Set Vacation + description: Set vacation responder settings for Google Mail. + target: + device: + integration: google_mail + entity: + integration: google_mail + fields: + enabled: + name: Enabled + required: true + default: true + description: Turn this off to end vacation responses. + selector: + boolean: + title: + name: Title + description: The subject for the email + selector: + text: + message: + name: Message + description: Body of the email + required: true + selector: + text: + plain_text: + name: Plain text + default: true + description: Choose to send message in plain text or HTML. + selector: + boolean: + restrict_contacts: + name: Restrict to Contacts + description: Restrict automatic reply to contacts. + selector: + boolean: + restrict_domain: + name: Restrict to Domain + description: Restrict automatic reply to domain. This only affects GSuite accounts. + selector: + boolean: + start: + name: start + description: First day of the vacation + selector: + date: + end: + name: end + description: Last day of the vacation + selector: + date: diff --git a/homeassistant/components/google_mail/strings.json b/homeassistant/components/google_mail/strings.json new file mode 100644 index 00000000000..eb44bffb134 --- /dev/null +++ b/homeassistant/components/google_mail/strings.json @@ -0,0 +1,34 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Google Mail integration needs to re-authenticate your account" + }, + "auth": { + "title": "Link Google Account" + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]", + "oauth_error": "[%key:common::config_flow::abort::oauth2_error%]", + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "wrong_account": "Wrong account: Please authenticate with {email}." + }, + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" + } + }, + "application_credentials": { + "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Mail. You also need to create Application Credentials linked to your account:\n1. Go to [Credentials]({oauth_creds_url}) and click **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **Web application** for the Application Type.\n\n" + } +} diff --git a/homeassistant/components/google_mail/translations/bg.json b/homeassistant/components/google_mail/translations/bg.json new file mode 100644 index 00000000000..2bd3ad6b331 --- /dev/null +++ b/homeassistant/components/google_mail/translations/bg.json @@ -0,0 +1,23 @@ +{ + "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", + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044a\u0442 \u043d\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d. \u041c\u043e\u043b\u044f, \u0441\u043b\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u044f\u0442\u0430.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0435\u0448\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "step": { + "pick_implementation": { + "title": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "reauth_confirm": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Google Mail \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u043a\u0430\u0443\u043d\u0442\u0430 \u0432\u0438", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/ca.json b/homeassistant/components/google_mail/translations/ca.json new file mode 100644 index 00000000000..1b0dcd90d9d --- /dev/null +++ b/homeassistant/components/google_mail/translations/ca.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "Segueix les [instruccions]({more_info_url}) de [la pantalla de consentiment OAuth]({oauth_consent_url}) perqu\u00e8 Home Assistant tingui acc\u00e9s al teu correu de Google. Tamb\u00e9 has de crear les credencials d'aplicaci\u00f3 enlla\u00e7ades al teu compte:\n 1. V\u00e9s a [Credencials]({oauth_creds_url}) i fes clic a **Crear credencials**.\n 2. A la llista desplegable, selecciona **ID de client OAuth**.\n 3. Selecciona **Aplicaci\u00f3 Web** al tipus d'aplicaci\u00f3.\n \n " + }, + "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_access_token": "Token d'acc\u00e9s inv\u00e0lid", + "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", + "oauth_error": "S'han rebut dades token inv\u00e0lides.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "timeout_connect": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3", + "unknown": "Error inesperat" + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa" + }, + "step": { + "auth": { + "title": "Vinculaci\u00f3 amb compte de Google" + }, + "pick_implementation": { + "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + }, + "reauth_confirm": { + "description": "La integraci\u00f3 Google Mail ha de tornar a autenticar-se amb el teu compte", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/de.json b/homeassistant/components/google_mail/translations/de.json new file mode 100644 index 00000000000..bb7745e24a7 --- /dev/null +++ b/homeassistant/components/google_mail/translations/de.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "Befolge die [Anweisungen]({more_info_url}) f\u00fcr den [OAuth-Zustimmungsbildschirm]({oauth_consent_url}), um Home Assistant Zugriff auf dein Google Mail zu gew\u00e4hren. Du musst auch mit deinem Konto verkn\u00fcpfte Anwendungsanmeldeinformationen erstellen:\n1. Gehe zu [Credentials]({oauth_creds_url}) und klicke auf **Create Credentials**.\n2. W\u00e4hle aus der Dropdown-Liste **OAuth-Client-ID** aus.\n3. W\u00e4hle **Webanwendung** als Anwendungstyp aus." + }, + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "oauth_error": "Ung\u00fcltige Token-Daten empfangen.", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "timeout_connect": "Zeit\u00fcberschreitung beim Verbindungsaufbau", + "unknown": "Unerwarteter Fehler" + }, + "create_entry": { + "default": "Erfolgreich authentifiziert" + }, + "step": { + "auth": { + "title": "Google-Konto verkn\u00fcpfen" + }, + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + }, + "reauth_confirm": { + "description": "Die Google Mail-Integration muss dein Konto erneut authentifizieren", + "title": "Integration erneut authentifizieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/el.json b/homeassistant/components/google_mail/translations/el.json new file mode 100644 index 00000000000..75f37324e5b --- /dev/null +++ b/homeassistant/components/google_mail/translations/el.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "\u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 [\u03bf\u03b4\u03b7\u03b3\u03af\u03b5\u03c2]({more_info_url}) \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd [\u03bf\u03b8\u03cc\u03bd\u03b7 \u03c3\u03c5\u03b3\u03ba\u03b1\u03c4\u03ac\u03b8\u03b5\u03c3\u03b7\u03c2 OAuth]({oauth_consent_url}) \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c3\u03c4\u03bf\u03bd Home Assistant \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7 \u03c3\u03c4\u03bf Google Mail \u03c3\u03b1\u03c2. \u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03b5\u03c0\u03af\u03c3\u03b7\u03c2 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03c0\u03bf\u03c5 \u03c3\u03c5\u03bd\u03b4\u03ad\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bc\u03b5 \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2:\n1. \u039c\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 [Credentials]({oauth_creds_url}) \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf **Create Credentials**.\n1. \u0391\u03c0\u03cc \u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03c0\u03c4\u03c5\u03c3\u03c3\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1 \u03b5\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 **\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 OAuth**.\n1. \u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 **Web application** \u03b3\u03b9\u03b1 \u03c4\u03bf Application Type (\u03a4\u03cd\u03c0\u03bf\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2).\n\n" + }, + "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", + "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", + "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.", + "oauth_error": "\u039b\u03ae\u03c6\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd.", + "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", + "timeout_connect": "\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" + }, + "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": { + "auth": { + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Google" + }, + "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 Google Mail \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" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/en.json b/homeassistant/components/google_mail/translations/en.json new file mode 100644 index 00000000000..9f9495d0ec0 --- /dev/null +++ b/homeassistant/components/google_mail/translations/en.json @@ -0,0 +1,34 @@ +{ + "application_credentials": { + "description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Mail. You also need to create Application Credentials linked to your account:\n1. Go to [Credentials]({oauth_creds_url}) and click **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **Web application** for the Application Type.\n\n" + }, + "config": { + "abort": { + "already_configured": "Account is already configured", + "already_in_progress": "Configuration flow is already in progress", + "cannot_connect": "Failed to connect", + "invalid_access_token": "Invalid access token", + "missing_configuration": "The component is not configured. Please follow the documentation.", + "oauth_error": "Received invalid token data.", + "reauth_successful": "Re-authentication was successful", + "timeout_connect": "Timeout establishing connection", + "unknown": "Unexpected error", + "wrong_account": "Wrong account: Please authenticate with {email}." + }, + "create_entry": { + "default": "Successfully authenticated" + }, + "step": { + "auth": { + "title": "Link Google Account" + }, + "pick_implementation": { + "title": "Pick Authentication Method" + }, + "reauth_confirm": { + "description": "The Google Mail integration needs to re-authenticate your account", + "title": "Reauthenticate Integration" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/es.json b/homeassistant/components/google_mail/translations/es.json new file mode 100644 index 00000000000..7a4ed15e1ac --- /dev/null +++ b/homeassistant/components/google_mail/translations/es.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "Sigue las [instrucciones]({more_info_url}) para la [pantalla de consentimiento de OAuth]( {oauth_consent_url} ) para dar acceso a Home Assistant a tu correo de Google. Tambi\u00e9n debes crear Credenciales de aplicaci\u00f3n vinculadas a tu cuenta:\n1. Ve a [Credenciales]( {oauth_creds_url} ) y haz clic en **Crear credenciales**.\n1. En la lista desplegable, selecciona **ID de cliente de OAuth**.\n1. Selecciona **Aplicaci\u00f3n web** para el Tipo de aplicaci\u00f3n. \n\n" + }, + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "cannot_connect": "No se pudo conectar", + "invalid_access_token": "Token de acceso no v\u00e1lido", + "missing_configuration": "El componente no est\u00e1 configurado. Por favor, sigue la documentaci\u00f3n.", + "oauth_error": "Se han recibido datos de token no v\u00e1lidos.", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", + "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n", + "unknown": "Error inesperado" + }, + "create_entry": { + "default": "Autenticado correctamente" + }, + "step": { + "auth": { + "title": "Vincular cuenta de Google" + }, + "pick_implementation": { + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" + }, + "reauth_confirm": { + "description": "La integraci\u00f3n de Google Mail necesita volver a autenticar tu cuenta", + "title": "Volver a autenticar la integraci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/et.json b/homeassistant/components/google_mail/translations/et.json new file mode 100644 index 00000000000..d9832b863df --- /dev/null +++ b/homeassistant/components/google_mail/translations/et.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "J\u00e4rgi [juhiseid]({more_info_url}) [OAuthi n\u00f5usoleku kuva]({oauth_consent_url}), et anda Home Assistantile juurdep\u00e4\u00e4s Google Mailile. Samuti pead looma oma kontoga lingitud rakenduse identimisteabe:\n1. Mine aadressile [Credentials]({oauth_creds_url}) ja kl\u00f5psa **Create Credentials**.\n1. Vali rippmen\u00fc\u00fcst **OAuth kliendi ID**.\n1. Vali rakenduse t\u00fc\u00fcbiks **Veebirakendus**.\n\n" + }, + "config": { + "abort": { + "already_configured": "Konto on juba seadistatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "cannot_connect": "\u00dchendamine nurjus", + "invalid_access_token": "Vigane juurdep\u00e4\u00e4sut\u00f5end", + "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.", + "oauth_error": "Saadi sobimatud loaandmed.", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "timeout_connect": "\u00dchenduse ajal\u00f5pp", + "unknown": "Ootamatu t\u00f5rge" + }, + "create_entry": { + "default": "Tuvastamine \u00f5nnestus" + }, + "step": { + "auth": { + "title": "Google'i konto linkimine" + }, + "pick_implementation": { + "title": "Vali tuvastusmeetod" + }, + "reauth_confirm": { + "description": "Google Maili sidumine peab konto uuesti autentima", + "title": "Taastuvasta sidumine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/hu.json b/homeassistant/components/google_mail/translations/hu.json new file mode 100644 index 00000000000..45933bcca25 --- /dev/null +++ b/homeassistant/components/google_mail/translations/hu.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "K\u00f6vesse az [utas\u00edt\u00e1sokat]({more_info_url}) az [OAuth hozz\u00e1j\u00e1rul\u00e1si k\u00e9perny\u0151n]({oauth_consent_url}), hogy a rendszer hozz\u00e1f\u00e9rhessen a Google Mail-hez. A fi\u00f3kj\u00e1hoz kapcsol\u00f3d\u00f3 Alkalmaz\u00e1si hiteles\u00edt\u0151 adatokat is l\u00e9tre kell hoznia:\n1. Menjen a [Hiteles\u00edt\u00e9si adatok]({oauth_creds_url}) men\u00fcpontra, \u00e9s kattintson a **Hiteles\u00edt\u00e9si adatok l\u00e9trehoz\u00e1sa** gombra.\n1. A leg\u00f6rd\u00fcl\u0151 list\u00e1b\u00f3l v\u00e1lassza ki az **OAuth \u00fcgyf\u00e9l azonos\u00edt\u00f3t**.\n1. Az Alkalmaz\u00e1s t\u00edpus\u00e1hoz v\u00e1lassza a **Webalkalmaz\u00e1s** lehet\u0151s\u00e9get.\n\n" + }, + "config": { + "abort": { + "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_access_token": "\u00c9rv\u00e9nytelen hozz\u00e1f\u00e9r\u00e9si token", + "missing_configuration": "A komponens nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", + "oauth_error": "\u00c9rv\u00e9nytelen token adatok \u00e9rkeztek.", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "timeout_connect": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "create_entry": { + "default": "Sikeres hiteles\u00edt\u00e9s" + }, + "step": { + "auth": { + "title": "Google-fi\u00f3k \u00f6sszekapcsol\u00e1sa" + }, + "pick_implementation": { + "title": "V\u00e1lasszon egy hiteles\u00edt\u00e9si m\u00f3dszert" + }, + "reauth_confirm": { + "description": "A Google Mail integr\u00e1ci\u00f3nak \u00fajra hiteles\u00edtenie kell a fi\u00f3kj\u00e1t", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/id.json b/homeassistant/components/google_mail/translations/id.json new file mode 100644 index 00000000000..3f2dbc2b4e6 --- /dev/null +++ b/homeassistant/components/google_mail/translations/id.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "Ikuti [instruksi]({more_info_url}) untuk [layar persetujuan OAuth]({oauth_consent_url}) untuk memberikan akses Home Assistant ke Google Mail Anda. Anda juga perlu membuat Kredensial Aplikasi yang ditautkan ke akun Anda:\n1. Buka [Kredensial]({oauth_creds_url}) dan klik **Buat Kredensial**.\n1. Dari daftar drop-down pilih **OAuth client ID**.\n1. Pilih **Aplikasi web** untuk Jenis Aplikasi.\n\n" + }, + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "cannot_connect": "Gagal terhubung", + "invalid_access_token": "Token akses tidak valid", + "missing_configuration": "Komponen tidak dikonfigurasi. Ikuti petunjuk dalam dokumentasi.", + "oauth_error": "Menerima respons token yang tidak valid.", + "reauth_successful": "Autentikasi ulang berhasil", + "timeout_connect": "Tenggang waktu pembuatan koneksi habis", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "create_entry": { + "default": "Berhasil diautentikasi" + }, + "step": { + "auth": { + "title": "Tautkan Akun Google" + }, + "pick_implementation": { + "title": "Pilih Metode Autentikasi" + }, + "reauth_confirm": { + "description": "Integrasi Google Mail perlu mengautentikasi ulang akun Anda", + "title": "Autentikasi Ulang Integrasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/it.json b/homeassistant/components/google_mail/translations/it.json new file mode 100644 index 00000000000..0328e89d866 --- /dev/null +++ b/homeassistant/components/google_mail/translations/it.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "Segui le [istruzioni]({more_info_url}) per la [schermata di consenso OAuth]({oauth_consent_url}) per concedere a Home Assistant l'accesso alla tua posta Google. Devi anche creare le Credenziali dell'Applicazione collegate al tuo account:\n 1. Vai a [Credenziali]({oauth_creds_url}) e fai clic su **Crea credenziali**.\n 1. Dall'elenco a discesa selezionare **ID client OAuth**.\n 1. Selezionare **Applicazione web** per Tipo di applicazione. \n\n" + }, + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "cannot_connect": "Impossibile connettersi", + "invalid_access_token": "Token di accesso non valido", + "missing_configuration": "Il componente non \u00e8 configurato. Segui la documentazione.", + "oauth_error": "Ricevuti dati token non validi.", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "timeout_connect": "Tempo scaduto per stabile la connessione.", + "unknown": "Errore imprevisto" + }, + "create_entry": { + "default": "Autenticazione riuscita" + }, + "step": { + "auth": { + "title": "Collegare l'account Google" + }, + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + }, + "reauth_confirm": { + "description": "L'integrazione di Google Mail deve autenticare nuovamente il tuo account", + "title": "Autentica nuovamente l'integrazione" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/nl.json b/homeassistant/components/google_mail/translations/nl.json new file mode 100644 index 00000000000..e860a2a5a9a --- /dev/null +++ b/homeassistant/components/google_mail/translations/nl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "cannot_connect": "Kan geen verbinding maken", + "invalid_access_token": "Ongeldig toegangstoken", + "missing_configuration": "Integratie niet geconfigureerd. Raadpleeg de documentatie.", + "oauth_error": "Ongeldige tokengegevens ontvangen.", + "reauth_successful": "Herauthenticatie geslaagd", + "timeout_connect": "Time-out bij het maken van verbinding", + "unknown": "Onverwachte fout" + }, + "create_entry": { + "default": "Authenticatie geslaagd" + }, + "step": { + "auth": { + "title": "Google-account koppelen" + }, + "pick_implementation": { + "title": "Kies een authenticatie methode" + }, + "reauth_confirm": { + "title": "Integratie herauthenticeren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/no.json b/homeassistant/components/google_mail/translations/no.json new file mode 100644 index 00000000000..51bcf419a94 --- /dev/null +++ b/homeassistant/components/google_mail/translations/no.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "F\u00f8lg [instruksjonene]( {more_info_url} ) for [OAuth-samtykkeskjermen]( {oauth_consent_url} ) for \u00e5 gi Home Assistant tilgang til Google Mail. Du m\u00e5 ogs\u00e5 opprette applikasjonslegitimasjon knyttet til kontoen din:\n 1. G\u00e5 til [Credentials]( {oauth_creds_url} ) og klikk p\u00e5 **Create Credentials**.\n 1. Velg **OAuth-klient-ID** fra rullegardinlisten.\n 1. Velg **Webapplikasjon** for applikasjonstype. \n\n" + }, + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "cannot_connect": "Tilkobling mislyktes", + "invalid_access_token": "Ugyldig tilgangstoken", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", + "oauth_error": "Mottatt ugyldige token data.", + "reauth_successful": "Re-autentisering var vellykket", + "timeout_connect": "Tidsavbrudd oppretter forbindelse", + "unknown": "Uventet feil" + }, + "create_entry": { + "default": "Vellykket godkjenning" + }, + "step": { + "auth": { + "title": "Tilknytt Google-kontoen" + }, + "pick_implementation": { + "title": "Velg godkjenningsmetode" + }, + "reauth_confirm": { + "description": "Google Mail-integrasjonen m\u00e5 autentisere kontoen din p\u00e5 nytt", + "title": "Godkjenne integrering p\u00e5 nytt" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/pl.json b/homeassistant/components/google_mail/translations/pl.json new file mode 100644 index 00000000000..66d3bb02042 --- /dev/null +++ b/homeassistant/components/google_mail/translations/pl.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "Post\u0119puj zgodnie z [instrukcjami]({more_info_url}) na [ekran akceptacji OAuth]({oauth_consent_url}), aby przyzna\u0107 Home Assistantowi dost\u0119p do Google Mail. Musisz r\u00f3wnie\u017c utworzy\u0107 po\u015bwiadczenia aplikacji powi\u0105zane z Twoim kontem:\n1. Przejd\u017a do [Po\u015bwiadczenia]({oauth_creds_url}) i kliknij **Utw\u00f3rz po\u015bwiadczenia**.\n2. Z listy rozwijanej wybierz **Identyfikator klienta OAuth**.\n3. Wybierz **Aplikacja internetowa** jako Typ aplikacji. \n\n" + }, + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_access_token": "Niepoprawny token dost\u0119pu", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "oauth_error": "Otrzymano nieprawid\u0142owe dane tokena.", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "timeout_connect": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono" + }, + "step": { + "auth": { + "title": "Po\u0142\u0105czenie z kontem Google" + }, + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + }, + "reauth_confirm": { + "description": "Integracja Google Mail wymaga ponownego uwierzytelnienia Twojego konta", + "title": "Ponownie uwierzytelnij integracj\u0119" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/pt-BR.json b/homeassistant/components/google_mail/translations/pt-BR.json new file mode 100644 index 00000000000..4a013296d9c --- /dev/null +++ b/homeassistant/components/google_mail/translations/pt-BR.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "Siga as [instru\u00e7\u00f5es]({more_info_url}) para a [tela de consentimento OAuth]({oauth_consent_url}) para dar ao Home Assistant acesso ao seu Google Mail. Voc\u00ea tamb\u00e9m precisa criar credenciais de aplicativo vinculadas \u00e0 sua conta:\n 1. Acesse [Credenciais]({oauth_creds_url}) e clique em **Criar credenciais**.\n 1. Na lista suspensa, selecione **OAuth client ID**.\n 1. Selecione **Aplicativo da Web** para o Tipo de aplicativo." + }, + "config": { + "abort": { + "already_configured": "A conta j\u00e1 est\u00e1 configurada", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "cannot_connect": "Falhou ao conectar", + "invalid_access_token": "Chave eletr\u00f4nica de acesso inv\u00e1lida", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Siga a documenta\u00e7\u00e3o.", + "oauth_error": "Dados de token inv\u00e1lidos recebidos.", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "timeout_connect": "Tempo limite estabelecendo conex\u00e3o", + "unknown": "Erro inesperado" + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "auth": { + "title": "Vincular conta do Google" + }, + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "description": "A integra\u00e7\u00e3o do Gmail precisa reautenticar sua conta", + "title": "Reautenticar Integra\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/ru.json b/homeassistant/components/google_mail/translations/ru.json new file mode 100644 index 00000000000..1a84654e4c9 --- /dev/null +++ b/homeassistant/components/google_mail/translations/ru.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c]({more_info_url}) \u043d\u0430 [\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 OAuth]({oauth_consent_url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c Home Assistant \u0434\u043e\u0441\u0442\u0443\u043f \u043a Google Mail. \u0422\u0430\u043a\u0436\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441 \u0412\u0430\u0448\u0438\u043c \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u043e\u043c:\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 [Credentials]({oauth_creds_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 **Create Credentials**.\n2. \u0412 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0435\u043c \u0441\u043f\u0438\u0441\u043a\u0435 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **OAuth client ID**.\n3. \u0412 \u043f\u043e\u043b\u0435 **Application Type** \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **Web application**." + }, + "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_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_access_token": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", + "oauth_error": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u044b \u043d\u0435\u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0442\u043e\u043a\u0435\u043d\u0430.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", + "timeout_connect": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "create_entry": { + "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "step": { + "auth": { + "title": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Google" + }, + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "reauth_confirm": { + "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 Google.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/sk.json b/homeassistant/components/google_mail/translations/sk.json new file mode 100644 index 00000000000..b9c5e6b8ac1 --- /dev/null +++ b/homeassistant/components/google_mail/translations/sk.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "Pod\u013ea [pokynov]({more_info_url}) pre [obrazovka s\u00fahlasu s protokolom OAuth]({oauth_consent_url}) povo\u013ete Asistentovi dom\u00e1cnosti pr\u00edstup k va\u0161ej po\u0161te Google Mail. Mus\u00edte tie\u017e vytvori\u0165 poverenia aplik\u00e1cie prepojen\u00e9 s va\u0161\u00edm \u00fa\u010dtom:\n1. Prejdite na [Credentials]({oauth_creds_url}) a kliknite na **Create Credentials**.\n1. Z rozba\u013eovacieho zoznamu vyberte **ID klienta OAuth**.\n1. Ako Typ aplik\u00e1cie vyberte **Webov\u00e1 aplik\u00e1cia**. \n\n" + }, + "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_access_token": "Neplatn\u00fd pr\u00edstupov\u00fd token", + "missing_configuration": "Komponent nie je nakonfigurovan\u00fd. Postupujte pod\u013ea dokument\u00e1cie.", + "oauth_error": "Prijat\u00e9 neplatn\u00e9 \u00fadaje tokenu.", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "timeout_connect": "\u010casov\u00fd limit na nadviazanie spojenia", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "create_entry": { + "default": "\u00daspe\u0161ne overen\u00e9" + }, + "step": { + "auth": { + "title": "Prepoji\u0165 \u00fa\u010det Google" + }, + "pick_implementation": { + "title": "Vyberte met\u00f3du overenia" + }, + "reauth_confirm": { + "description": "Integr\u00e1cia slu\u017eby Google Mail vy\u017eaduje op\u00e4tovn\u00e9 overenie v\u00e1\u0161ho \u00fa\u010dtu", + "title": "Znova overi\u0165 integr\u00e1ciu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/tr.json b/homeassistant/components/google_mail/translations/tr.json new file mode 100644 index 00000000000..6860e697c49 --- /dev/null +++ b/homeassistant/components/google_mail/translations/tr.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "Home Assistant'\u0131n Google Mail'inize eri\u015fmesine izin vermek i\u00e7in [OAuth izin ekran\u0131]( {oauth_consent_url} ) i\u00e7in [talimatlar\u0131]( {more_info_url} ) uygulay\u0131n. Ayr\u0131ca, hesab\u0131n\u0131za ba\u011fl\u0131 Uygulama Kimlik Bilgileri olu\u015fturman\u0131z gerekir:\n 1. [Kimlik Bilgileri]( {oauth_creds_url} ) sayfas\u0131na gidin ve **Kimlik Bilgileri Olu\u015ftur**'a t\u0131klay\u0131n.\n 1. A\u00e7\u0131l\u0131r listeden **OAuth istemci kimli\u011fi**'ni se\u00e7in.\n 1. Uygulama T\u00fcr\u00fc olarak **Web uygulamas\u0131**'n\u0131 se\u00e7in. \n\n" + }, + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_access_token": "Ge\u00e7ersiz eri\u015fim anahtar\u0131", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin.", + "oauth_error": "Ge\u00e7ersiz anahtar verileri al\u0131nd\u0131.", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "timeout_connect": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131", + "unknown": "Beklenmeyen hata" + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, + "step": { + "auth": { + "title": "Google Hesab\u0131n\u0131 Ba\u011fla" + }, + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" + }, + "reauth_confirm": { + "description": "Google Mail entegrasyonunun hesab\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor", + "title": "Entegrasyonu Yeniden Do\u011frula" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/uk.json b/homeassistant/components/google_mail/translations/uk.json new file mode 100644 index 00000000000..dd8f263d5a7 --- /dev/null +++ b/homeassistant/components/google_mail/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u041e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e", + "already_in_progress": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0434\u043e\u0442\u0440\u0438\u043c\u0443\u0439\u0442\u0435\u0441\u044c \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0457.", + "timeout_connect": "\u0427\u0430\u0441 \u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0437\u2019\u0454\u0434\u043d\u0430\u043d\u043d\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0456\u0448\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u043e\u0432\u0430\u043d\u043e" + }, + "step": { + "auth": { + "title": "\u041f\u043e\u0432\u2019\u044f\u0437\u0430\u0442\u0438 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 Google" + }, + "reauth_confirm": { + "description": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f Google Mail \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0454 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 \u0432\u0430\u0448\u043e\u0433\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_mail/translations/zh-Hant.json b/homeassistant/components/google_mail/translations/zh-Hant.json new file mode 100644 index 00000000000..c3fa36a828f --- /dev/null +++ b/homeassistant/components/google_mail/translations/zh-Hant.json @@ -0,0 +1,33 @@ +{ + "application_credentials": { + "description": "\u8ddf\u96a8[\u8aaa\u660e]({more_info_url})\u4ee5\u8a2d\u5b9a\u81f3 [OAuth \u540c\u610f\u756b\u9762]({oauth_consent_url})\u3001\u4f9b Home Assistant \u5b58\u53d6\u60a8\u7684 Google \u90f5\u4ef6\u3002\u540c\u6642\u9700\u8981\u65b0\u589e\u9023\u7d50\u81f3\u5e33\u865f\u7684\u61c9\u7528\u7a0b\u5f0f\u6191\u8b49\uff1a\n1. \u700f\u89bd\u81f3 [\u6191\u8b49]({oauth_creds_url}) \u9801\u9762\u4e26\u9ede\u9078 **\u5efa\u7acb\u6191\u8b49**\u3002\n1. \u7531\u4e0b\u62c9\u9078\u55ae\u4e2d\u9078\u64c7 **OAuth \u7528\u6236\u7aef ID**\u3002\n1. \u61c9\u7528\u7a0b\u5f0f\u985e\u578b\u5247\u9078\u64c7 **Web \u61c9\u7528\u7a0b\u5f0f**\u3002\n\n" + }, + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_access_token": "\u5b58\u53d6\u6b0a\u6756\u7121\u6548", + "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "oauth_error": "\u6536\u5230\u7121\u6548\u7684\u6b0a\u6756\u8cc7\u6599\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "timeout_connect": "\u5efa\u7acb\u9023\u7dda\u903e\u6642", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" + }, + "step": { + "auth": { + "title": "\u9023\u7d50 Google \u5e33\u865f" + }, + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, + "reauth_confirm": { + "description": "Google \u90f5\u4ef6\u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google_sheets/translations/id.json b/homeassistant/components/google_sheets/translations/id.json index 391a68a272f..916216a9fc5 100644 --- a/homeassistant/components/google_sheets/translations/id.json +++ b/homeassistant/components/google_sheets/translations/id.json @@ -13,7 +13,7 @@ "oauth_error": "Menerima respons token yang tidak valid.", "open_spreadsheet_failure": "Kesalahan saat membuka spreadsheet, lihat log kesalahan untuk detailnya", "reauth_successful": "Autentikasi ulang berhasil", - "timeout_connect": "Tenggang waktu membuat koneksi habis", + "timeout_connect": "Tenggang waktu pembuatan koneksi habis", "unknown": "Kesalahan yang tidak diharapkan" }, "create_entry": { diff --git a/homeassistant/components/google_sheets/translations/ru.json b/homeassistant/components/google_sheets/translations/ru.json index 71f9d0449d8..1952b16af57 100644 --- a/homeassistant/components/google_sheets/translations/ru.json +++ b/homeassistant/components/google_sheets/translations/ru.json @@ -1,6 +1,6 @@ { "application_credentials": { - "description": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c]({more_info_url}) \u043d\u0430 [\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 OAuth]({oauth_consent_url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c Home Assistant \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0438\u043c Google \u0422\u0430\u0431\u043b\u0438\u0446\u0430\u043c. \u0422\u0430\u043a\u0436\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441 \u0412\u0430\u0448\u0438\u043c \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u043e\u043c:\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439]({oauth_creds_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f**.\n2. \u0412 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0435\u043c \u0441\u043f\u0438\u0441\u043a\u0435 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth**.\n3. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **\u0412\u0435\u0431 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435** \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0422\u0438\u043f\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f." + "description": "\u0421\u043b\u0435\u0434\u0443\u0439\u0442\u0435 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c]({more_info_url}) \u043d\u0430 [\u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 OAuth]({oauth_consent_url}), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0438\u0442\u044c Home Assistant \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0412\u0430\u0448\u0438\u043c Google \u0422\u0430\u0431\u043b\u0438\u0446\u0430\u043c. \u0422\u0430\u043a\u0436\u0435 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441 \u0412\u0430\u0448\u0438\u043c \u0430\u043a\u043a\u0430\u0443\u043d\u0442\u043e\u043c:\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 [Credentials]({oauth_creds_url}) \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 **Create Credentials**.\n2. \u0412 \u0432\u044b\u043f\u0430\u0434\u0430\u044e\u0449\u0435\u043c \u0441\u043f\u0438\u0441\u043a\u0435 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **OAuth client ID**.\n3. \u0412 \u043f\u043e\u043b\u0435 **Application Type** \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 **Web application**." }, "config": { "abort": { diff --git a/homeassistant/components/google_travel_time/translations/nl.json b/homeassistant/components/google_travel_time/translations/nl.json index 464042702dc..adc273be0f9 100644 --- a/homeassistant/components/google_travel_time/translations/nl.json +++ b/homeassistant/components/google_travel_time/translations/nl.json @@ -28,6 +28,7 @@ "mode": "Reiswijze", "time": "Tijd", "time_type": "Tijd Type", + "traffic_mode": "Verkeersmodus", "transit_mode": "Vervoersmodus", "transit_routing_preference": "Voorkeur vervoer route", "units": "Eenheden" diff --git a/homeassistant/components/google_travel_time/translations/sk.json b/homeassistant/components/google_travel_time/translations/sk.json index d5b4a578e52..09bd39de726 100644 --- a/homeassistant/components/google_travel_time/translations/sk.json +++ b/homeassistant/components/google_travel_time/translations/sk.json @@ -33,7 +33,7 @@ "transit_routing_preference": "Predvo\u013eba smerovania verejnej dopravy", "units": "Jednotky" }, - "description": "Volite\u013ene m\u00f4\u017eete zada\u0165 \u010das odchodu alebo \u010das pr\u00edchodu. Ak zad\u00e1vate \u010das odchodu, m\u00f4\u017eete zada\u0165 \u201eteraz\u201c, \u010dasov\u00fa pe\u010diatku syst\u00e9mu Unix alebo 24-hodinov\u00fd \u010dasov\u00fd re\u0165azec, napr\u00edklad \u201e08:00:00\u201c. Ak zad\u00e1vate \u010das pr\u00edchodu, m\u00f4\u017eete pou\u017ei\u0165 \u010dasov\u00fa pe\u010diatku Unixu alebo 24-hodinov\u00fd \u010dasov\u00fd re\u0165azec, napr\u00edklad `08:00:00`" + "description": "Volite\u013ene m\u00f4\u017eete zada\u0165 \u010das odchodu alebo \u010das pr\u00edchodu. Ak zad\u00e1vate \u010das odchodu, m\u00f4\u017eete zada\u0165 'teraz', \u010dasov\u00fa pe\u010diatku syst\u00e9mu Unix alebo 24-hodinov\u00fd \u010dasov\u00fd re\u0165azec, napr\u00edklad '08:00:00'. Ak zad\u00e1vate \u010das pr\u00edchodu, m\u00f4\u017eete pou\u017ei\u0165 \u010dasov\u00fa pe\u010diatku Unixu alebo 24-hodinov\u00fd \u010dasov\u00fd re\u0165azec, napr\u00edklad `08:00:00`" } } }, diff --git a/homeassistant/components/google_travel_time/translations/tr.json b/homeassistant/components/google_travel_time/translations/tr.json index 117ca7797fb..09bf8e6d9c7 100644 --- a/homeassistant/components/google_travel_time/translations/tr.json +++ b/homeassistant/components/google_travel_time/translations/tr.json @@ -28,6 +28,7 @@ "mode": "Seyahat Modu", "time": "Zaman", "time_type": "Zaman T\u00fcr\u00fc", + "traffic_mode": "Trafik Modu", "transit_mode": "Transit Modu", "transit_routing_preference": "Toplu Ta\u015f\u0131ma Tercihi", "units": "Birimler" diff --git a/homeassistant/components/google_wifi/sensor.py b/homeassistant/components/google_wifi/sensor.py index d7be81a8ea5..9fe264219ec 100644 --- a/homeassistant/components/google_wifi/sensor.py +++ b/homeassistant/components/google_wifi/sensor.py @@ -17,7 +17,6 @@ from homeassistant.const import ( CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_NAME, - STATE_UNKNOWN, UnitOfTime, ) from homeassistant.core import HomeAssistant @@ -172,12 +171,12 @@ class GoogleWifiAPI: self.raw_data = None self.conditions = conditions self.data = { - ATTR_CURRENT_VERSION: STATE_UNKNOWN, - ATTR_NEW_VERSION: STATE_UNKNOWN, - ATTR_UPTIME: STATE_UNKNOWN, - ATTR_LAST_RESTART: STATE_UNKNOWN, - ATTR_LOCAL_IP: STATE_UNKNOWN, - ATTR_STATUS: STATE_UNKNOWN, + ATTR_CURRENT_VERSION: None, + ATTR_NEW_VERSION: None, + ATTR_UPTIME: None, + ATTR_LAST_RESTART: None, + ATTR_LOCAL_IP: None, + ATTR_STATUS: None, } self.available = True self.update() @@ -223,7 +222,7 @@ class GoogleWifiAPI: elif ( attr_key == ATTR_LOCAL_IP and not self.raw_data["wan"]["online"] ): - sensor_value = STATE_UNKNOWN + sensor_value = None self.data[attr_key] = sensor_value except KeyError: @@ -235,4 +234,4 @@ class GoogleWifiAPI: description.sensor_key, attr_key, ) - self.data[attr_key] = STATE_UNKNOWN + self.data[attr_key] = None diff --git a/homeassistant/components/govee_ble/manifest.json b/homeassistant/components/govee_ble/manifest.json index aaa659d1667..cfeb33b323b 100644 --- a/homeassistant/components/govee_ble/manifest.json +++ b/homeassistant/components/govee_ble/manifest.json @@ -74,7 +74,7 @@ } ], "requirements": ["govee-ble==0.21.1"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco", "@PierreAronnax"], "iot_class": "local_push" } diff --git a/homeassistant/components/govee_ble/sensor.py b/homeassistant/components/govee_ble/sensor.py index ce603b5b5a6..b2da37bdf7e 100644 --- a/homeassistant/components/govee_ble/sensor.py +++ b/homeassistant/components/govee_ble/sensor.py @@ -1,8 +1,6 @@ """Support for govee ble sensors.""" from __future__ import annotations -from typing import Optional, Union - from govee_ble import DeviceClass, DeviceKey, SensorUpdate, Units from govee_ble.parser import ERROR @@ -117,7 +115,7 @@ async def async_setup_entry( class GoveeBluetoothSensorEntity( PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int, str]]] + PassiveBluetoothDataProcessor[float | int | str | None] ], SensorEntity, ): diff --git a/homeassistant/components/govee_ble/translations/lv.json b/homeassistant/components/govee_ble/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/govee_ble/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/govee_ble/translations/tr.json b/homeassistant/components/govee_ble/translations/tr.json index f63cee3493c..66d94aa9414 100644 --- a/homeassistant/components/govee_ble/translations/tr.json +++ b/homeassistant/components/govee_ble/translations/tr.json @@ -8,13 +8,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/gpslogger/translations/el.json b/homeassistant/components/gpslogger/translations/el.json index 74e1d5075ae..181cf4bd0b5 100644 --- a/homeassistant/components/gpslogger/translations/el.json +++ b/homeassistant/components/gpslogger/translations/el.json @@ -6,7 +6,7 @@ "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." + "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 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \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 \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\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": { diff --git a/homeassistant/components/gpslogger/translations/hu.json b/homeassistant/components/gpslogger/translations/hu.json index 96677b262eb..886dcdcd6b5 100644 --- a/homeassistant/components/gpslogger/translations/hu.json +++ b/homeassistant/components/gpslogger/translations/hu.json @@ -6,7 +6,7 @@ "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." }, "create_entry": { - "default": "Az esem\u00e9ny Home Assistantnek val\u00f3 k\u00fcld\u00e9s\u00e9hez be kell \u00e1ll\u00edtani a webhook funkci\u00f3t a GPSLoggerben. \n\n Az al\u00e1bbi inform\u00e1ci\u00f3kat haszn\u00e1lja: \n\n - URL: `{webhook_url}` \n - Method: POST \n\n Tov\u00e1bbi r\u00e9szletek a [dokument\u00e1ci\u00f3]({docs_url}) linken tal\u00e1lhat\u00f3k." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni Home Assistantba, be kell \u00e1ll\u00edtania a GPSLogger webhook funkci\u00f3j\u00e1t. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 adatokat: \n\n - URL: `{webhook_url}`\n - Met\u00f3dus: POST\n\nB\u0151vebb inform\u00e1ci\u00f3 [a dokument\u00e1ci\u00f3ban]({docs_url}) olvashat\u00f3." }, "step": { "user": { diff --git a/homeassistant/components/gpslogger/translations/tr.json b/homeassistant/components/gpslogger/translations/tr.json index dc14b0d4011..9a85983baf8 100644 --- a/homeassistant/components/gpslogger/translations/tr.json +++ b/homeassistant/components/gpslogger/translations/tr.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, "create_entry": { - "default": "Olaylar\u0131 Home Assistant'a g\u00f6ndermek i\u00e7in GPSLogger'da webhook \u00f6zelli\u011fini ayarlaman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url}" + "default": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in GPSLogger'da webhook \u00f6zelli\u011fini kurman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url} ) bak\u0131n." }, "step": { "user": { diff --git a/homeassistant/components/gree/translations/tr.json b/homeassistant/components/gree/translations/tr.json index 3df15466f03..d8dbccfea8a 100644 --- a/homeassistant/components/gree/translations/tr.json +++ b/homeassistant/components/gree/translations/tr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } } diff --git a/homeassistant/components/greeneye_monitor/sensor.py b/homeassistant/components/greeneye_monitor/sensor.py index 55318ad6018..b4c62cd2bc1 100644 --- a/homeassistant/components/greeneye_monitor/sensor.py +++ b/homeassistant/components/greeneye_monitor/sensor.py @@ -1,7 +1,7 @@ """Support for the sensors in a GreenEye Monitor.""" from __future__ import annotations -from typing import Any, Union +from typing import Any import greeneye @@ -116,12 +116,12 @@ async def async_setup_platform( on_new_monitor(monitor) -UnderlyingSensorType = Union[ - greeneye.monitor.Channel, - greeneye.monitor.PulseCounter, - greeneye.monitor.TemperatureSensor, - greeneye.monitor.VoltageSensor, -] +UnderlyingSensorType = ( + greeneye.monitor.Channel + | greeneye.monitor.PulseCounter + | greeneye.monitor.TemperatureSensor + | greeneye.monitor.VoltageSensor +) class GEMSensor(SensorEntity): diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 0ed293ea777..4543bf79d52 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -58,6 +58,7 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" CONF_ALL = "all" ATTR_ADD_ENTITIES = "add_entities" +ATTR_REMOVE_ENTITIES = "remove_entities" ATTR_AUTO = "auto" ATTR_ENTITIES = "entities" ATTR_OBJECT_ID = "object_id" @@ -75,6 +76,7 @@ PLATFORMS = [ Platform.LOCK, Platform.MEDIA_PLAYER, Platform.NOTIFY, + Platform.SENSOR, Platform.SWITCH, ] @@ -367,6 +369,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: entity_ids = set(group.tracking) | set(delta) await group.async_update_tracked_entity_ids(entity_ids) + if ATTR_REMOVE_ENTITIES in service.data: + delta = service.data[ATTR_REMOVE_ENTITIES] + entity_ids = set(group.tracking) - set(delta) + await group.async_update_tracked_entity_ids(entity_ids) + if ATTR_ENTITIES in service.data: entity_ids = service.data[ATTR_ENTITIES] await group.async_update_tracked_entity_ids(entity_ids) @@ -405,6 +412,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: vol.Optional(ATTR_ALL): cv.boolean, vol.Exclusive(ATTR_ENTITIES, "entities"): cv.entity_ids, vol.Exclusive(ATTR_ADD_ENTITIES, "entities"): cv.entity_ids, + vol.Exclusive(ATTR_REMOVE_ENTITIES, "entities"): cv.entity_ids, } ) ), diff --git a/homeassistant/components/group/config_flow.py b/homeassistant/components/group/config_flow.py index 9a084cde685..069f74bf707 100644 --- a/homeassistant/components/group/config_flow.py +++ b/homeassistant/components/group/config_flow.py @@ -7,7 +7,7 @@ from typing import Any, cast import voluptuous as vol -from homeassistant.const import CONF_ENTITIES +from homeassistant.const import CONF_ENTITIES, CONF_TYPE from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er, selector from homeassistant.helpers.schema_config_entry_flow import ( @@ -21,11 +21,21 @@ from homeassistant.helpers.schema_config_entry_flow import ( from . import DOMAIN from .binary_sensor import CONF_ALL -from .const import CONF_HIDE_MEMBERS +from .const import CONF_HIDE_MEMBERS, CONF_IGNORE_NON_NUMERIC + +_STATISTIC_MEASURES = [ + selector.SelectOptionDict(value="min", label="Minimum"), + selector.SelectOptionDict(value="max", label="Maximum"), + selector.SelectOptionDict(value="mean", label="Arithmetic mean"), + selector.SelectOptionDict(value="median", label="Median"), + selector.SelectOptionDict(value="last", label="Most recently updated"), + selector.SelectOptionDict(value="range", label="Statistical range"), + selector.SelectOptionDict(value="sum", label="Sum"), +] async def basic_group_options_schema( - domain: str, handler: SchemaCommonFlowHandler + domain: str | list[str], handler: SchemaCommonFlowHandler ) -> vol.Schema: """Generate options schema.""" return vol.Schema( @@ -39,7 +49,7 @@ async def basic_group_options_schema( ) -def basic_group_config_schema(domain: str) -> vol.Schema: +def basic_group_config_schema(domain: str | list[str]) -> vol.Schema: """Generate config schema.""" return vol.Schema( { @@ -67,6 +77,32 @@ BINARY_SENSOR_CONFIG_SCHEMA = basic_group_config_schema("binary_sensor").extend( } ) +SENSOR_CONFIG_EXTENDS = { + vol.Required(CONF_TYPE): selector.SelectSelector( + selector.SelectSelectorConfig(options=_STATISTIC_MEASURES), + ), +} +SENSOR_OPTIONS = { + vol.Optional(CONF_IGNORE_NON_NUMERIC, default=False): selector.BooleanSelector(), + vol.Required(CONF_TYPE): selector.SelectSelector( + selector.SelectSelectorConfig(options=_STATISTIC_MEASURES), + ), +} + + +async def sensor_options_schema( + domain: str, handler: SchemaCommonFlowHandler +) -> vol.Schema: + """Generate options schema.""" + return ( + await basic_group_options_schema(["sensor", "number", "input_number"], handler) + ).extend(SENSOR_OPTIONS) + + +SENSOR_CONFIG_SCHEMA = basic_group_config_schema( + ["sensor", "number", "input_number"] +).extend(SENSOR_CONFIG_EXTENDS) + async def light_switch_options_schema( domain: str, handler: SchemaCommonFlowHandler @@ -88,6 +124,7 @@ GROUP_TYPES = [ "light", "lock", "media_player", + "sensor", "switch", ] @@ -139,6 +176,10 @@ CONFIG_FLOW = { basic_group_config_schema("media_player"), validate_user_input=set_group_type("media_player"), ), + "sensor": SchemaFlowFormStep( + SENSOR_CONFIG_SCHEMA, + validate_user_input=set_group_type("sensor"), + ), "switch": SchemaFlowFormStep( basic_group_config_schema("switch"), validate_user_input=set_group_type("switch"), @@ -156,6 +197,7 @@ OPTIONS_FLOW = { "media_player": SchemaFlowFormStep( partial(basic_group_options_schema, "media_player") ), + "sensor": SchemaFlowFormStep(partial(sensor_options_schema, "sensor")), "switch": SchemaFlowFormStep(partial(light_switch_options_schema, "switch")), } diff --git a/homeassistant/components/group/const.py b/homeassistant/components/group/const.py index 82817e71add..3ef280b2770 100644 --- a/homeassistant/components/group/const.py +++ b/homeassistant/components/group/const.py @@ -1,3 +1,4 @@ """Constants for the Group integration.""" CONF_HIDE_MEMBERS = "hide_members" +CONF_IGNORE_NON_NUMERIC = "ignore_non_numeric" diff --git a/homeassistant/components/group/lock.py b/homeassistant/components/group/lock.py index 610e15f3ecc..9c39e145528 100644 --- a/homeassistant/components/group/lock.py +++ b/homeassistant/components/group/lock.py @@ -6,7 +6,12 @@ from typing import Any import voluptuous as vol -from homeassistant.components.lock import DOMAIN, PLATFORM_SCHEMA, LockEntity +from homeassistant.components.lock import ( + DOMAIN, + PLATFORM_SCHEMA, + LockEntity, + LockEntityFeature, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, @@ -100,6 +105,7 @@ class LockGroup(GroupEntity, LockEntity): ) -> None: """Initialize a lock group.""" self._entity_ids = entity_ids + self._attr_supported_features = LockEntityFeature.OPEN self._attr_name = name self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} diff --git a/homeassistant/components/group/sensor.py b/homeassistant/components/group/sensor.py new file mode 100644 index 00000000000..52ce90b2535 --- /dev/null +++ b/homeassistant/components/group/sensor.py @@ -0,0 +1,410 @@ +"""This platform allows several sensors to be grouped into one sensor to provide numeric combinations.""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime +import logging +import statistics +from typing import TYPE_CHECKING, Any + +import voluptuous as vol + +from homeassistant.components.input_number import DOMAIN as INPUT_NUMBER_DOMAIN +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN +from homeassistant.components.sensor import ( + CONF_STATE_CLASS, + DEVICE_CLASSES_SCHEMA, + DOMAIN, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + STATE_CLASSES_SCHEMA, + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DEVICE_CLASS, + CONF_ENTITIES, + CONF_NAME, + CONF_TYPE, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.helpers import config_validation as cv, entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType + +from . import GroupEntity +from .const import CONF_IGNORE_NON_NUMERIC + +DEFAULT_NAME = "Sensor Group" + +ATTR_MIN_VALUE = "min_value" +ATTR_MIN_ENTITY_ID = "min_entity_id" +ATTR_MAX_VALUE = "max_value" +ATTR_MAX_ENTITY_ID = "max_entity_id" +ATTR_MEAN = "mean" +ATTR_MEDIAN = "median" +ATTR_LAST = "last" +ATTR_LAST_ENTITY_ID = "last_entity_id" +ATTR_RANGE = "range" +ATTR_SUM = "sum" +SENSOR_TYPES = { + ATTR_MIN_VALUE: "min", + ATTR_MAX_VALUE: "max", + ATTR_MEAN: "mean", + ATTR_MEDIAN: "median", + ATTR_LAST: "last", + ATTR_RANGE: "range", + ATTR_SUM: "sum", +} +SENSOR_TYPE_TO_ATTR = {v: k for k, v in SENSOR_TYPES.items()} + +# No limit on parallel updates to enable a group calling another group +PARALLEL_UPDATES = 0 + +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_ENTITIES): cv.entities_domain( + [DOMAIN, NUMBER_DOMAIN, INPUT_NUMBER_DOMAIN] + ), + vol.Required(CONF_TYPE): vol.All(cv.string, vol.In(SENSOR_TYPES.values())), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_IGNORE_NON_NUMERIC, default=False): cv.boolean, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): str, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA, + } +) + +_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 the Switch Group platform.""" + async_add_entities( + [ + SensorGroup( + config.get(CONF_UNIQUE_ID), + config[CONF_NAME], + config[CONF_ENTITIES], + config[CONF_IGNORE_NON_NUMERIC], + config[CONF_TYPE], + config.get(CONF_UNIT_OF_MEASUREMENT), + config.get(CONF_STATE_CLASS), + config.get(CONF_DEVICE_CLASS), + ) + ] + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Switch Group config entry.""" + registry = er.async_get(hass) + entities = er.async_validate_entity_ids( + registry, config_entry.options[CONF_ENTITIES] + ) + async_add_entities( + [ + SensorGroup( + config_entry.entry_id, + config_entry.title, + entities, + config_entry.options.get(CONF_IGNORE_NON_NUMERIC, True), + config_entry.options[CONF_TYPE], + None, + None, + None, + ) + ] + ) + + +def calc_min( + sensor_values: list[tuple[str, float, State]] +) -> tuple[dict[str, str | None], float]: + """Calculate min value.""" + val: float | None = None + entity_id: str | None = None + for sensor_id, sensor_value, _ in sensor_values: + if val is None or val > sensor_value: + entity_id, val = sensor_id, sensor_value + + attributes = {ATTR_MIN_ENTITY_ID: entity_id} + if TYPE_CHECKING: + assert val is not None + return attributes, val + + +def calc_max( + sensor_values: list[tuple[str, float, State]] +) -> tuple[dict[str, str | None], float]: + """Calculate max value.""" + val: float | None = None + entity_id: str | None = None + for sensor_id, sensor_value, _ in sensor_values: + if val is None or val < sensor_value: + entity_id, val = sensor_id, sensor_value + + attributes = {ATTR_MAX_ENTITY_ID: entity_id} + if TYPE_CHECKING: + assert val is not None + return attributes, val + + +def calc_mean( + sensor_values: list[tuple[str, float, State]] +) -> tuple[dict[str, str | None], float]: + """Calculate mean value.""" + result = (sensor_value for _, sensor_value, _ in sensor_values) + + value: float = statistics.mean(result) + return {}, value + + +def calc_median( + sensor_values: list[tuple[str, float, State]] +) -> tuple[dict[str, str | None], float]: + """Calculate median value.""" + result = (sensor_value for _, sensor_value, _ in sensor_values) + + value: float = statistics.median(result) + return {}, value + + +def calc_last( + sensor_values: list[tuple[str, float, State]] +) -> tuple[dict[str, str | None], float]: + """Calculate last value.""" + last_updated: datetime | None = None + last_entity_id: str | None = None + for entity_id, state_f, state in sensor_values: + if last_updated is None or state.last_updated > last_updated: + last_updated = state.last_updated + last = state_f + last_entity_id = entity_id + + attributes = {ATTR_LAST_ENTITY_ID: last_entity_id} + return attributes, last + + +def calc_range( + sensor_values: list[tuple[str, float, State]] +) -> tuple[dict[str, str | None], float]: + """Calculate range value.""" + max_result = max((sensor_value for _, sensor_value, _ in sensor_values)) + min_result = min((sensor_value for _, sensor_value, _ in sensor_values)) + + value: float = max_result - min_result + return {}, value + + +def calc_sum( + sensor_values: list[tuple[str, float, State]] +) -> tuple[dict[str, str | None], float]: + """Calculate a sum of values.""" + result = 0.0 + for _, sensor_value, _ in sensor_values: + result += sensor_value + + return {}, result + + +CALC_TYPES: dict[ + str, + Callable[[list[tuple[str, float, State]]], tuple[dict[str, str | None], float]], +] = { + "min": calc_min, + "max": calc_max, + "mean": calc_mean, + "median": calc_median, + "last": calc_last, + "range": calc_range, + "sum": calc_sum, +} + + +class SensorGroup(GroupEntity, SensorEntity): + """Representation of a sensor group.""" + + _attr_available = False + _attr_should_poll = False + _attr_icon = "mdi:calculator" + + def __init__( + self, + unique_id: str | None, + name: str, + entity_ids: list[str], + mode: bool, + sensor_type: str, + unit_of_measurement: str | None, + state_class: SensorStateClass | None, + device_class: SensorDeviceClass | None, + ) -> None: + """Initialize a sensor group.""" + self._entity_ids = entity_ids + self._sensor_type = sensor_type + self._attr_state_class = state_class + self.calc_state_class: SensorStateClass | None = None + self._attr_device_class = device_class + self.calc_device_class: SensorDeviceClass | None = None + self._attr_native_unit_of_measurement = unit_of_measurement + self.calc_unit_of_measurement: str | None = None + self._attr_name = name + if name == DEFAULT_NAME: + self._attr_name = f"{DEFAULT_NAME} {sensor_type}".capitalize() + self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids} + self._attr_unique_id = unique_id + self.mode = all if mode is False else any + self._state_calc: Callable[ + [list[tuple[str, float, State]]], + tuple[dict[str, str | None], float | None], + ] = CALC_TYPES[self._sensor_type] + self._state_incorrect: set[str] = set() + self._extra_state_attribute: dict[str, Any] = {} + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + + @callback + def async_state_changed_listener(event: Event) -> None: + """Handle child updates.""" + self.async_set_context(event.context) + self.async_defer_or_update_ha_state() + + self.async_on_remove( + async_track_state_change_event( + self.hass, self._entity_ids, async_state_changed_listener + ) + ) + + await super().async_added_to_hass() + + @callback + def async_update_group_state(self) -> None: + """Query all members and determine the sensor group state.""" + states: list[StateType] = [] + valid_states: list[bool] = [] + sensor_values: list[tuple[str, float, State]] = [] + for entity_id in self._entity_ids: + if (state := self.hass.states.get(entity_id)) is not None: + states.append(state.state) + try: + sensor_values.append((entity_id, float(state.state), state)) + if entity_id in self._state_incorrect: + self._state_incorrect.remove(entity_id) + except ValueError: + valid_states.append(False) + if entity_id not in self._state_incorrect: + self._state_incorrect.add(entity_id) + _LOGGER.warning( + "Unable to use state. Only numerical states are supported," + " entity %s with value %s excluded from calculation", + entity_id, + state.state, + ) + continue + valid_states.append(True) + + # Set group as unavailable if all members do not have numeric values + self._attr_available = any(numeric_state for numeric_state in valid_states) + + valid_state = self.mode( + state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states + ) + valid_state_numeric = self.mode(numeric_state for numeric_state in valid_states) + + if not valid_state or not valid_state_numeric: + self._attr_native_value = None + return + + # Calculate values + self._calculate_entity_properties() + self._extra_state_attribute, self._attr_native_value = self._state_calc( + sensor_values + ) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Return the state attributes of the sensor.""" + return {ATTR_ENTITY_ID: self._entity_ids, **self._extra_state_attribute} + + @property + def device_class(self) -> SensorDeviceClass | None: + """Return device class.""" + if self._attr_device_class is not None: + return self._attr_device_class + return self.calc_device_class + + @property + def state_class(self) -> SensorStateClass | str | None: + """Return state class.""" + if self._attr_state_class is not None: + return self._attr_state_class + return self.calc_state_class + + @property + def native_unit_of_measurement(self) -> str | None: + """Return native unit of measurement.""" + if self._attr_native_unit_of_measurement is not None: + return self._attr_native_unit_of_measurement + return self.calc_unit_of_measurement + + def _calculate_entity_properties(self) -> None: + """Calculate device_class, state_class and unit of measurement.""" + device_classes = [] + state_classes = [] + unit_of_measurements = [] + + if ( + self._attr_device_class + and self._attr_state_class + and self._attr_native_unit_of_measurement + ): + return + + for entity_id in self._entity_ids: + if (state := self.hass.states.get(entity_id)) is not None: + device_classes.append(state.attributes.get("device_class")) + state_classes.append(state.attributes.get("state_class")) + unit_of_measurements.append(state.attributes.get("unit_of_measurement")) + + self.calc_device_class = None + self.calc_state_class = None + self.calc_unit_of_measurement = None + + # Calculate properties and save if all same + if ( + not self._attr_device_class + and device_classes + and all(x == device_classes[0] for x in device_classes) + ): + self.calc_device_class = device_classes[0] + if ( + not self._attr_state_class + and state_classes + and all(x == state_classes[0] for x in state_classes) + ): + self.calc_state_class = state_classes[0] + if ( + not self._attr_unit_of_measurement + and unit_of_measurements + and all(x == unit_of_measurements[0] for x in unit_of_measurements) + ): + self.calc_unit_of_measurement = unit_of_measurements[0] diff --git a/homeassistant/components/group/services.yaml b/homeassistant/components/group/services.yaml index cba11b1723d..fdb1a1af014 100644 --- a/homeassistant/components/group/services.yaml +++ b/homeassistant/components/group/services.yaml @@ -38,6 +38,12 @@ set: example: domain.entity_id1, domain.entity_id2 selector: object: + remove_entities: + name: Remove Entities + description: List of members that will be removed from group listening. + example: domain.entity_id1, domain.entity_id2 + selector: + object: all: name: All description: Enable this option if the group should only turn on when all entities are on. diff --git a/homeassistant/components/group/strings.json b/homeassistant/components/group/strings.json index 26494255996..dcc97803adc 100644 --- a/homeassistant/components/group/strings.json +++ b/homeassistant/components/group/strings.json @@ -12,6 +12,7 @@ "light": "Light group", "lock": "Lock group", "media_player": "Media player group", + "sensor": "Sensor group", "switch": "Switch group" } }, @@ -65,6 +66,21 @@ "name": "[%key:component::group::config::step::binary_sensor::data::name%]" } }, + "sensor": { + "title": "[%key:component::group::config::step::user::title%]", + "description": "If \"ignore non-numeric\" is enabled, the group's state is calculated if at least one member has a numerical value. If \"ignore non-numeric\" is disabled, the group's state is calculated only if all group members have numerical values.", + "data": { + "ignore_non_numeric": "Ignore non-numeric", + "entities": "Members", + "hide_members": "Hide members", + "name": "Name", + "type": "Type", + "round_digits": "Round value to number of decimals", + "device_class": "Device class", + "state_class": "State class", + "unit_of_measurement": "Unit of Measurement" + } + }, "switch": { "title": "[%key:component::group::config::step::user::title%]", "data": { @@ -117,6 +133,19 @@ "hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]" } }, + "sensor": { + "description": "[%key:component::group::config::step::sensor::description%]", + "data": { + "ignore_non_numeric": "[%key:component::group::config::step::sensor::data::ignore_non_numeric%]", + "entities": "[%key:component::group::config::step::sensor::data::entities%]", + "hide_members": "[%key:component::group::config::step::sensor::data::hide_members%]", + "type": "[%key:component::group::config::step::sensor::data::type%]", + "round_digits": "[%key:component::group::config::step::sensor::data::round_digits%]", + "device_class": "[%key:component::group::config::step::sensor::data::device_class%]", + "state_class": "[%key:component::group::config::step::sensor::data::state_class%]", + "unit_of_measurement": "[%key:component::group::config::step::sensor::data::unit_of_measurement%]" + } + }, "switch": { "description": "[%key:component::group::config::step::binary_sensor::description%]", "data": { diff --git a/homeassistant/components/group/translations/el.json b/homeassistant/components/group/translations/el.json index df1a5982f2c..4a8604a1821 100644 --- a/homeassistant/components/group/translations/el.json +++ b/homeassistant/components/group/translations/el.json @@ -15,7 +15,7 @@ "data": { "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, @@ -23,7 +23,7 @@ "data": { "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, @@ -31,7 +31,7 @@ "data": { "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, @@ -47,7 +47,7 @@ "data": { "entities": "\u039c\u03ad\u03bb\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", "hide_members": "\u0391\u03c0\u03cc\u03ba\u03c1\u03c5\u03c8\u03b7 \u03bc\u03b5\u03bb\u03ce\u03bd", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, @@ -60,7 +60,7 @@ "title": "\u039d\u03ad\u03b1 \u03bf\u03bc\u03ac\u03b4\u03b1" }, "user": { - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03cd\u03c0\u03bf \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", + "description": "\u039f\u03b9 \u03bf\u03bc\u03ac\u03b4\u03b5\u03c2 \u03c3\u03ac\u03c2 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03bf\u03c5\u03bd \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03bd\u03ad\u03b1 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c0\u03bf\u03c5 \u03b1\u03bd\u03c4\u03b9\u03c0\u03c1\u03bf\u03c3\u03c9\u03c0\u03b5\u03cd\u03b5\u03b9 \u03c0\u03bf\u03bb\u03bb\u03ad\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c4\u03bf\u03c5 \u03af\u03b4\u03b9\u03bf\u03c5 \u03c4\u03cd\u03c0\u03bf\u03c5.", "menu_options": { "binary_sensor": "\u039f\u03bc\u03ac\u03b4\u03b1 \u03b4\u03c5\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03c9\u03bd", "cover": "\u039f\u03bc\u03ac\u03b4\u03b1 \u03ba\u03b1\u03bb\u03c5\u03bc\u03bc\u03ac\u03c4\u03c9\u03bd", @@ -68,6 +68,7 @@ "light": "\u039f\u03bc\u03ac\u03b4\u03b1 \u03c6\u03ce\u03c4\u03c9\u03bd", "lock": "\u039f\u03bc\u03ac\u03b4\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2", "media_player": "\u039f\u03bc\u03ac\u03b4\u03b1 \u03b1\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae\u03c2 \u03c0\u03bf\u03bb\u03c5\u03bc\u03ad\u03c3\u03c9\u03bd", + "sensor": "\u039f\u03bc\u03ac\u03b4\u03b1 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03c9\u03bd", "switch": "\u0395\u03bd\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" }, "title": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2" diff --git a/homeassistant/components/group/translations/en.json b/homeassistant/components/group/translations/en.json index a67d23b812d..97e7e23238b 100644 --- a/homeassistant/components/group/translations/en.json +++ b/homeassistant/components/group/translations/en.json @@ -51,6 +51,21 @@ }, "title": "Add Group" }, + "sensor": { + "data": { + "device_class": "Device class", + "entities": "Members", + "hide_members": "Hide members", + "ignore_non_numeric": "Ignore non-numeric", + "name": "Name", + "round_digits": "Round value to number of decimals", + "state_class": "State class", + "type": "Type", + "unit_of_measurement": "Unit of Measurement" + }, + "description": "If \"ignore non-numeric\" is enabled, the group's state is calculated if at least one member has a numerical value. If \"ignore non-numeric\" is disabled, the group's state is calculated only if all group members have numerical values.", + "title": "Add Group" + }, "switch": { "data": { "entities": "Members", @@ -68,6 +83,7 @@ "light": "Light group", "lock": "Lock group", "media_player": "Media player group", + "sensor": "Sensor group", "switch": "Switch group" }, "title": "Add Group" @@ -116,6 +132,19 @@ "hide_members": "Hide members" } }, + "sensor": { + "data": { + "device_class": "Device class", + "entities": "Members", + "hide_members": "Hide members", + "ignore_non_numeric": "Ignore non-numeric", + "round_digits": "Round value to number of decimals", + "state_class": "State class", + "type": "Type", + "unit_of_measurement": "Unit of Measurement" + }, + "description": "If \"ignore non-numeric\" is enabled, the group's state is calculated if at least one member has a numerical value. If \"ignore non-numeric\" is disabled, the group's state is calculated only if all group members have numerical values." + }, "switch": { "data": { "all": "All entities", diff --git a/homeassistant/components/group/translations/et.json b/homeassistant/components/group/translations/et.json index 709d7b2c929..bb9d6e5b27b 100644 --- a/homeassistant/components/group/translations/et.json +++ b/homeassistant/components/group/translations/et.json @@ -51,6 +51,21 @@ }, "title": "Uus grupp" }, + "sensor": { + "data": { + "device_class": "Seadme klass", + "entities": "Liikmed", + "hide_members": "Peida grupi liikmed", + "ignore_non_numeric": "Eira mittenumbrilisi", + "name": "Nimi", + "round_digits": "\u00dcmarda komakohani", + "state_class": "Oleku klass", + "type": "T\u00fc\u00fcp", + "unit_of_measurement": "M\u00f5\u00f5t\u00fchik" + }, + "description": "Kui on lubatud \"eira mittenumbrilisi\" siis arvutatakse grupi olek kui v\u00e4hemalt \u00fcks liige omab arvv\u00e4\u00e4rtust. Kui \"eira mittenumbrilisi\" on keelatud siis arvutatakse grupi olek ainult siis kui k\u00f5ik liikmed omavad arvv\u00e4\u00e4rtust.", + "title": "Lisa grupp" + }, "switch": { "data": { "entities": "Liikmed", @@ -68,6 +83,7 @@ "light": "Valgustite r\u00fchm", "lock": "Lukusta grupp", "media_player": "Meediumipleieri r\u00fchm", + "sensor": "Andurite grupp", "switch": "Grupi vahetamine" }, "title": "Lisa grupp" @@ -116,6 +132,19 @@ "hide_members": "Peida grupi liikmed" } }, + "sensor": { + "data": { + "device_class": "Seadme klass", + "entities": "Liikmed", + "hide_members": "Peida liikmed", + "ignore_non_numeric": "Eira miteenumbrilisi", + "round_digits": "\u00dcmarda komakohani", + "state_class": "Oleku klass", + "type": "T\u00fc\u00fcp", + "unit_of_measurement": "M\u00f5\u00f5t\u00fchik" + }, + "description": "Kui on lubatud \"eira mittenumbrilisi\" siis arvutatakse grupi olek kui v\u00e4hemalt \u00fcks liige omab arvv\u00e4\u00e4rtust. Kui \"eira mittenumbrilisi\" on keelatud siis arvutatakse grupi olek ainult siis kui k\u00f5ik liikmed omavad arvv\u00e4\u00e4rtust." + }, "switch": { "data": { "all": "K\u00f5ik olemid", diff --git a/homeassistant/components/group/translations/it.json b/homeassistant/components/group/translations/it.json index dac9fd264f2..61911a4055e 100644 --- a/homeassistant/components/group/translations/it.json +++ b/homeassistant/components/group/translations/it.json @@ -130,14 +130,14 @@ "_": { "closed": "Chiuso", "home": "In casa", - "locked": "Bloccato", + "locked": "Chiusa", "not_home": "Fuori casa", "off": "Spento", "ok": "OK", "on": "Acceso", "open": "Aperto", "problem": "Problema", - "unlocked": "Sbloccato" + "unlocked": "Aperta" } }, "title": "Gruppo" diff --git a/homeassistant/components/group/translations/sk.json b/homeassistant/components/group/translations/sk.json index 7b689aeab28..18a994446b4 100644 --- a/homeassistant/components/group/translations/sk.json +++ b/homeassistant/components/group/translations/sk.json @@ -8,7 +8,7 @@ "hide_members": "Skry\u0165 \u010dlenov", "name": "N\u00e1zov" }, - "description": "Ak je povolen\u00e9 \u201ev\u0161etky entity\u201c, stav skupiny je zapnut\u00fd, iba ak s\u00fa zapnut\u00e9 v\u0161etci \u010dlenovia. Ak je mo\u017enos\u0165 \u201ev\u0161etky entity\u201c vypnut\u00e1, stav skupiny je zapnut\u00fd, ak je zapnut\u00fd niektor\u00fd \u010dlen.", + "description": "Ak je povolen\u00e9 \"v\u0161etky entity\", stav skupiny je zapnut\u00fd, iba ak s\u00fa zapnut\u00e9 v\u0161etci \u010dlenovia. Ak je mo\u017enos\u0165 \"v\u0161etky entity\" vypnut\u00e1, stav skupiny je zapnut\u00fd, ak je zapnut\u00fd niektor\u00fd \u010dlen.", "title": "Prida\u0165 skupinu" }, "cover": { @@ -82,7 +82,7 @@ "entities": "\u010clenovia", "hide_members": "Skry\u0165 \u010dlenov" }, - "description": "Ak je povolen\u00e9 \u201ev\u0161etky entity\u201c, stav skupiny je zapnut\u00fd, iba ak s\u00fa zapnut\u00e9 v\u0161etci \u010dlenovia. Ak je mo\u017enos\u0165 \u201ev\u0161etky entity\u201c vypnut\u00e1, stav skupiny je zapnut\u00fd, ak je zapnut\u00fd niektor\u00fd \u010dlen." + "description": "Ak je povolen\u00e9 \"v\u0161etky entity\", stav skupiny je zapnut\u00fd, iba ak s\u00fa zapnut\u00e9 v\u0161etci \u010dlenovia. Ak je mo\u017enos\u0165 \"v\u0161etky entity\" vypnut\u00e1, stav skupiny je zapnut\u00fd, ak je zapnut\u00fd niektor\u00fd \u010dlen." }, "cover": { "data": { @@ -102,7 +102,7 @@ "entities": "\u010clenovia", "hide_members": "Skry\u0165 \u010dlenov" }, - "description": "Ak je povolen\u00e9 \u201ev\u0161etky entity\u201c, stav skupiny je zapnut\u00fd, iba ak s\u00fa zapnut\u00e9 v\u0161etci \u010dlenovia. Ak je mo\u017enos\u0165 \u201ev\u0161etky entity\u201c vypnut\u00e1, stav skupiny je zapnut\u00fd, ak je zapnut\u00fd niektor\u00fd \u010dlen." + "description": "Ak je povolen\u00e9 \"v\u0161etky entity\", stav skupiny je zapnut\u00fd, iba ak s\u00fa zapnut\u00e9 v\u0161etci \u010dlenovia. Ak je mo\u017enos\u0165 \"v\u0161etky entity\" vypnut\u00e1, stav skupiny je zapnut\u00fd, ak je zapnut\u00fd niektor\u00fd \u010dlen." }, "lock": { "data": { @@ -122,7 +122,7 @@ "entities": "\u010clenovia", "hide_members": "Skry\u0165 \u010dlenov" }, - "description": "Ak je povolen\u00e9 \u201ev\u0161etky entity\u201c, stav skupiny je zapnut\u00fd, iba ak s\u00fa zapnut\u00e9 v\u0161etci \u010dlenovia. Ak je mo\u017enos\u0165 \u201ev\u0161etky entity\u201c vypnut\u00e1, stav skupiny je zapnut\u00fd, ak je zapnut\u00fd niektor\u00fd \u010dlen." + "description": "Ak je povolen\u00e9 \"v\u0161etky entity\", stav skupiny je zapnut\u00fd, iba ak s\u00fa zapnut\u00e9 v\u0161etci \u010dlenovia. Ak je mo\u017enos\u0165 \"v\u0161etky entity\" vypnut\u00e1, stav skupiny je zapnut\u00fd, ak je zapnut\u00fd niektor\u00fd \u010dlen." } } }, diff --git a/homeassistant/components/group/translations/uk.json b/homeassistant/components/group/translations/uk.json index 08cee558f27..47c5f156103 100644 --- a/homeassistant/components/group/translations/uk.json +++ b/homeassistant/components/group/translations/uk.json @@ -1,4 +1,13 @@ { + "config": { + "step": { + "binary_sensor": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430" + } + } + } + }, "state": { "_": { "closed": "\u0417\u0430\u0447\u0438\u043d\u0435\u043d\u043e", diff --git a/homeassistant/components/growatt_server/strings.json b/homeassistant/components/growatt_server/strings.json index 9717aa217f3..695b8a08c1c 100644 --- a/homeassistant/components/growatt_server/strings.json +++ b/homeassistant/components/growatt_server/strings.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_plants": "No plants have been found on this account" }, "error": { diff --git a/homeassistant/components/growatt_server/translations/bg.json b/homeassistant/components/growatt_server/translations/bg.json index 46573dc14b4..e4df79156c6 100644 --- a/homeassistant/components/growatt_server/translations/bg.json +++ b/homeassistant/components/growatt_server/translations/bg.json @@ -1,5 +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" + }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, diff --git a/homeassistant/components/growatt_server/translations/ca.json b/homeassistant/components/growatt_server/translations/ca.json index 39dc1153434..45887844ef2 100644 --- a/homeassistant/components/growatt_server/translations/ca.json +++ b/homeassistant/components/growatt_server/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", "no_plants": "No s'ha trobat cap planta en aquest compte" }, "error": { diff --git a/homeassistant/components/growatt_server/translations/de.json b/homeassistant/components/growatt_server/translations/de.json index adb769baa2d..620a76349a3 100644 --- a/homeassistant/components/growatt_server/translations/de.json +++ b/homeassistant/components/growatt_server/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "no_plants": "Es wurden keine Pflanzen auf diesem Konto gefunden" }, "error": { diff --git a/homeassistant/components/growatt_server/translations/en.json b/homeassistant/components/growatt_server/translations/en.json index 86196783133..d3cadb2f53c 100644 --- a/homeassistant/components/growatt_server/translations/en.json +++ b/homeassistant/components/growatt_server/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Device is already configured", "no_plants": "No plants have been found on this account" }, "error": { diff --git a/homeassistant/components/growatt_server/translations/et.json b/homeassistant/components/growatt_server/translations/et.json index c3327e3d676..99905273450 100644 --- a/homeassistant/components/growatt_server/translations/et.json +++ b/homeassistant/components/growatt_server/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "no_plants": "Kontolt ei leitud \u00fchtegi taime" }, "error": { diff --git a/homeassistant/components/growatt_server/translations/no.json b/homeassistant/components/growatt_server/translations/no.json index 8d8b985d025..f2105a38e17 100644 --- a/homeassistant/components/growatt_server/translations/no.json +++ b/homeassistant/components/growatt_server/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", "no_plants": "Ingen anlegg funnet p\u00e5 denne kontoen" }, "error": { diff --git a/homeassistant/components/growatt_server/translations/ru.json b/homeassistant/components/growatt_server/translations/ru.json index c5eedf66ad3..45f6a50360d 100644 --- a/homeassistant/components/growatt_server/translations/ru.json +++ b/homeassistant/components/growatt_server/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "no_plants": "\u0412 \u044d\u0442\u043e\u0439 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0440\u0430\u0441\u0442\u0435\u043d\u0438\u044f." }, "error": { diff --git a/homeassistant/components/growatt_server/translations/uk.json b/homeassistant/components/growatt_server/translations/uk.json new file mode 100644 index 00000000000..e9180b28e78 --- /dev/null +++ b/homeassistant/components/growatt_server/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/growatt_server/translations/zh-Hant.json b/homeassistant/components/growatt_server/translations/zh-Hant.json index 62991306cb8..c446f1b12df 100644 --- a/homeassistant/components/growatt_server/translations/zh-Hant.json +++ b/homeassistant/components/growatt_server/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_plants": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u690d\u7269" }, "error": { diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 24999f98e16..f587ef2e54c 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -322,10 +322,11 @@ class PairedSensorManager: """Define a callback for when new paired sensor data is received.""" self._hass.async_create_task(self.async_process_latest_paired_sensor_uids()) - cancel_process_task = self._sensor_pair_dump_coordinator.async_add_listener( - async_create_process_task + self._entry.async_on_unload( + self._sensor_pair_dump_coordinator.async_add_listener( + async_create_process_task + ) ) - self._entry.async_on_unload(cancel_process_task) async def async_pair_sensor(self, uid: str) -> None: """Add a new paired sensor coordinator.""" diff --git a/homeassistant/components/guardian/translations/el.json b/homeassistant/components/guardian/translations/el.json index 2a4963c8649..cc39a332c5b 100644 --- a/homeassistant/components/guardian/translations/el.json +++ b/homeassistant/components/guardian/translations/el.json @@ -23,12 +23,12 @@ "fix_flow": { "step": { "confirm": { - "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03c5\u03c7\u03cc\u03bd \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 `{alternate_service}` \u03bc\u03b5 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2-\u03c3\u03c4\u03cc\u03c7\u03bf\u03c5 `{alternate_target}`. \u03a3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03a5\u03a0\u039f\u0392\u039f\u039b\u0397 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03c0\u03b9\u03c3\u03b7\u03bc\u03ac\u03bd\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03b6\u03ae\u03c4\u03b7\u03bc\u03b1 \u03c9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03c5\u03bc\u03ad\u03bd\u03bf.", - "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03c5\u03c7\u03cc\u03bd \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u00ab {alternate_service} \u00bb \u03bc\u03b5 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c3\u03c4\u03cc\u03c7\u03bf\u03c5 \u00ab {alternate_target} \u00bb.", + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" } } }, - "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" } } } \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/lv.json b/homeassistant/components/guardian/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/guardian/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py index 250fee58db5..010f65cd114 100644 --- a/homeassistant/components/guardian/util.py +++ b/homeassistant/components/guardian/util.py @@ -87,7 +87,6 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): self._api_coro = api_coro self._api_lock = api_lock self._client = client - self._signal_handler_unsubs: list[Callable[..., None]] = [] self.config_entry = entry self.signal_reboot_requested = SIGNAL_REBOOT_REQUESTED.format( @@ -112,16 +111,8 @@ class GuardianDataUpdateCoordinator(DataUpdateCoordinator[dict]): self.last_update_success = False self.async_update_listeners() - self._signal_handler_unsubs.append( + self.config_entry.async_on_unload( async_dispatcher_connect( self.hass, self.signal_reboot_requested, async_reboot_requested ) ) - - @callback - def async_teardown() -> None: - """Tear the coordinator down appropriately.""" - for unsub in self._signal_handler_unsubs: - unsub() - - self.config_entry.async_on_unload(async_teardown) diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index d25b840d761..3fe73d84667 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, "error": { "invalid_credentials": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" diff --git a/homeassistant/components/habitica/translations/bg.json b/homeassistant/components/habitica/translations/bg.json index ce37c7da82c..1de47a1ba00 100644 --- a/homeassistant/components/habitica/translations/bg.json +++ b/homeassistant/components/habitica/translations/bg.json @@ -1,5 +1,8 @@ { "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" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/habitica/translations/ca.json b/homeassistant/components/habitica/translations/ca.json index 729d03834b5..02f4b331823 100644 --- a/homeassistant/components/habitica/translations/ca.json +++ b/homeassistant/components/habitica/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat" + }, "error": { "invalid_credentials": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" diff --git a/homeassistant/components/habitica/translations/de.json b/homeassistant/components/habitica/translations/de.json index cab6b3250ec..ae381a40fff 100644 --- a/homeassistant/components/habitica/translations/de.json +++ b/homeassistant/components/habitica/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, "error": { "invalid_credentials": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" diff --git a/homeassistant/components/habitica/translations/en.json b/homeassistant/components/habitica/translations/en.json index 377e3129ee8..2429582367e 100644 --- a/homeassistant/components/habitica/translations/en.json +++ b/homeassistant/components/habitica/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Account is already configured" + }, "error": { "invalid_credentials": "Invalid authentication", "unknown": "Unexpected error" diff --git a/homeassistant/components/habitica/translations/et.json b/homeassistant/components/habitica/translations/et.json index f2ff048c1c4..a262826fc44 100644 --- a/homeassistant/components/habitica/translations/et.json +++ b/homeassistant/components/habitica/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud" + }, "error": { "invalid_credentials": "Vigane autentimine", "unknown": "Ootamatu t\u00f5rge" diff --git a/homeassistant/components/habitica/translations/no.json b/homeassistant/components/habitica/translations/no.json index 218b861e513..020ec52a3dc 100644 --- a/homeassistant/components/habitica/translations/no.json +++ b/homeassistant/components/habitica/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, "error": { "invalid_credentials": "Ugyldig godkjenning", "unknown": "Uventet feil" diff --git a/homeassistant/components/habitica/translations/ru.json b/homeassistant/components/habitica/translations/ru.json index 23576d2a6d6..30b099c7c27 100644 --- a/homeassistant/components/habitica/translations/ru.json +++ b/homeassistant/components/habitica/translations/ru.json @@ -1,5 +1,8 @@ { "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": { "invalid_credentials": "\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/habitica/translations/uk.json b/homeassistant/components/habitica/translations/uk.json new file mode 100644 index 00000000000..8577b630f74 --- /dev/null +++ b/homeassistant/components/habitica/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u0417\u0430\u043c\u0456\u043d\u0430 \u0456\u043c\u0435\u043d\u0456 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 Habitica. \u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u043c\u0435\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0432\u0438\u043a\u043b\u0438\u043a\u0456\u0432 \u0441\u043b\u0443\u0436\u0431\u0438" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/habitica/translations/zh-Hant.json b/homeassistant/components/habitica/translations/zh-Hant.json index d5aeda2c359..7012f43ceeb 100644 --- a/homeassistant/components/habitica/translations/zh-Hant.json +++ b/homeassistant/components/habitica/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "invalid_credentials": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py index 33c0e3c5b5f..3cb87323c0b 100644 --- a/homeassistant/components/harmony/data.py +++ b/homeassistant/components/harmony/data.py @@ -275,5 +275,5 @@ class HarmonyData(HarmonySubscriberMixin): except aioexc.TimeOut: _LOGGER.error("%s: Syncing hub with Harmony cloud timed-out", self.name) return False - else: - return True + + return True diff --git a/homeassistant/components/harmony/subscriber.py b/homeassistant/components/harmony/subscriber.py index 092eb0d6859..0c22d4b38b9 100644 --- a/homeassistant/components/harmony/subscriber.py +++ b/homeassistant/components/harmony/subscriber.py @@ -2,11 +2,9 @@ from __future__ import annotations import asyncio +from collections.abc import Callable import logging - -# Issue with Python 3.9.0 and 3.9.1 with collections.abc.Callable -# https://bugs.python.org/issue42965 -from typing import Any, Callable, NamedTuple, Optional +from typing import Any, NamedTuple, Optional from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback diff --git a/homeassistant/components/harmony/translations/hu.json b/homeassistant/components/harmony/translations/hu.json index 45bf73f2bd3..afd12bbc40a 100644 --- a/homeassistant/components/harmony/translations/hu.json +++ b/homeassistant/components/harmony/translations/hu.json @@ -11,14 +11,14 @@ "step": { "link": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name} ({host})?", - "title": "A Logitech Harmony Hub be\u00e1ll\u00edt\u00e1sa" + "title": "Logitech Harmony Hub be\u00e1ll\u00edt\u00e1sa" }, "user": { "data": { "host": "C\u00edm", "name": "Hub neve" }, - "title": "A Logitech Harmony Hub be\u00e1ll\u00edt\u00e1sa" + "title": "Logitech Harmony Hub be\u00e1ll\u00edt\u00e1sa" } } }, diff --git a/homeassistant/components/harmony/translations/lv.json b/homeassistant/components/harmony/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/harmony/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/nl.json b/homeassistant/components/harmony/translations/nl.json index aaf16ed2dc7..d4dbf4aca0c 100644 --- a/homeassistant/components/harmony/translations/nl.json +++ b/homeassistant/components/harmony/translations/nl.json @@ -22,6 +22,15 @@ } } }, + "entity": { + "select": { + "activities": { + "state": { + "power_off": "Uitschakelen" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/harmony/translations/select.tr.json b/homeassistant/components/harmony/translations/select.tr.json new file mode 100644 index 00000000000..febb89c19ab --- /dev/null +++ b/homeassistant/components/harmony/translations/select.tr.json @@ -0,0 +1,7 @@ +{ + "state": { + "harmony__activities": { + "power_off": "G\u00fc\u00e7 Kapal\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/tr.json b/homeassistant/components/harmony/translations/tr.json index b4400dcb06a..f7191ff3774 100644 --- a/homeassistant/components/harmony/translations/tr.json +++ b/homeassistant/components/harmony/translations/tr.json @@ -10,15 +10,24 @@ "flow_title": "{name}", "step": { "link": { - "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?", - "title": "Logitech Harmony Hub'\u0131 Kur" + "description": "{name} ( {host} ) kurmak istiyor musunuz?", + "title": "Logitech Harmony Hub'\u0131 kurun" }, "user": { "data": { "host": "Sunucu", "name": "Hub Ad\u0131" }, - "title": "Logitech Harmony Hub'\u0131 Kur" + "title": "Logitech Harmony Hub'\u0131 kurun" + } + } + }, + "entity": { + "select": { + "activities": { + "state": { + "power_off": "G\u00fc\u00e7 Kapal\u0131" + } } } }, diff --git a/homeassistant/components/hassio/addon_manager.py b/homeassistant/components/hassio/addon_manager.py index 46eca080b1b..88e755e3c7b 100644 --- a/homeassistant/components/hassio/addon_manager.py +++ b/homeassistant/components/hassio/addon_manager.py @@ -7,9 +7,7 @@ from dataclasses import dataclass from enum import Enum from functools import partial, wraps import logging -from typing import Any, TypeVar - -from typing_extensions import Concatenate, ParamSpec +from typing import Any, Concatenate, ParamSpec, TypeVar from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 2806c08ee54..0d923075bf7 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -267,7 +267,7 @@ class HassIO: def is_connected(self): """Return true if it connected to Hass.io supervisor. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/supervisor/ping", method="get", timeout=15) @@ -275,7 +275,7 @@ class HassIO: def get_info(self): """Return generic Supervisor information. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/info", method="get") @@ -283,7 +283,7 @@ class HassIO: def get_host_info(self): """Return data for Host. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/host/info", method="get") @@ -291,7 +291,7 @@ class HassIO: def get_os_info(self): """Return data for the OS. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/os/info", method="get") @@ -315,7 +315,7 @@ class HassIO: def get_addon_info(self, addon): """Return data for a Add-on. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command(f"/addons/{addon}/info", method="get") @@ -340,7 +340,7 @@ class HassIO: def get_store(self): """Return data from the store. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/store", method="get") @@ -348,7 +348,7 @@ class HassIO: def get_ingress_panels(self): """Return data for Add-on ingress panels. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/ingress/panels", method="get") @@ -356,7 +356,7 @@ class HassIO: def restart_homeassistant(self): """Restart Home-Assistant container. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/homeassistant/restart") @@ -364,7 +364,7 @@ class HassIO: def stop_homeassistant(self): """Stop Home-Assistant container. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/homeassistant/stop") @@ -372,7 +372,7 @@ class HassIO: def refresh_updates(self): """Refresh available updates. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/refresh_updates", timeout=None) @@ -380,7 +380,7 @@ class HassIO: def retrieve_discovery_messages(self): """Return all discovery data from Hass.io API. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/discovery", method="get", timeout=60) @@ -388,7 +388,7 @@ class HassIO: def get_discovery_message(self, uuid): """Return a single discovery data message. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command(f"/discovery/{uuid}", method="get") @@ -396,7 +396,7 @@ class HassIO: def get_resolution_info(self): """Return data for Supervisor resolution center. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/resolution/info", method="get") @@ -424,7 +424,7 @@ class HassIO: def update_hass_timezone(self, timezone): """Update Home-Assistant timezone data on Hass.io. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command("/supervisor/options", payload={"timezone": timezone}) @@ -432,7 +432,7 @@ class HassIO: def update_diagnostics(self, diagnostics: bool): """Update Supervisor diagnostics setting. - This method return a coroutine. + This method returns a coroutine. """ return self.send_command( "/supervisor/options", payload={"diagnostics": diagnostics} diff --git a/homeassistant/components/hassio/translations/hu.json b/homeassistant/components/hassio/translations/hu.json index 14f2d995ff6..87b8eb4ba0a 100644 --- a/homeassistant/components/hassio/translations/hu.json +++ b/homeassistant/components/hassio/translations/hu.json @@ -1,7 +1,7 @@ { "issues": { "unhealthy": { - "description": "A rendszer jelenleg rendellenes \u00e1llapotban van a k\u00f6vetkez\u0151 miatt: {reason}. Haszn\u00e1lja a linket, ha t\u00f6bbet szeretne megtudni, \u00e9s hogyan jav\u00edthatja meg.", + "description": "A rendszer jelenleg rendellenes \u00e1llapotban van a k\u00f6vetkez\u0151 miatt: {reason}. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Rendellenes \u00e1llapot \u2013 {reason}" }, "unhealthy_docker": { @@ -13,11 +13,11 @@ "title": "Rendellenes rendszer - Nem privilegiz\u00e1lt" }, "unhealthy_setup": { - "description": "A rendszer jelenleg nem megfelel\u0151, mert a telep\u00edt\u00e9s nem fejez\u0151d\u00f6tt be. Ennek sz\u00e1mos oka lehet, haszn\u00e1lja a linket, hogy t\u00f6bbet megtudjon, \u00e9s hogyan jav\u00edthatja ki ezt.", + "description": "A rendszer jelenleg nem megfelel\u0151, mert a telep\u00edt\u00e9s nem fejez\u0151d\u00f6tt be. Ennek sz\u00e1mos oka lehet. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Rendellenes rendszer \u2013 A telep\u00edt\u00e9s sikertelen" }, "unhealthy_supervisor": { - "description": "A rendszer jelenleg rendellenes \u00e1llapotban van, mert a Supervisor leg\u00fajabb verzi\u00f3ra t\u00f6rt\u00e9n\u0151 friss\u00edt\u00e9s\u00e9nek k\u00eds\u00e9rlete sikertelen volt. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer jelenleg rendellenes \u00e1llapotban van, mert a Supervisor leg\u00fajabb verzi\u00f3ra t\u00f6rt\u00e9n\u0151 friss\u00edt\u00e9s\u00e9nek k\u00eds\u00e9rlete sikertelen volt. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Rendellenes rendszer \u2013 A Supervisor friss\u00edt\u00e9se nem siker\u00fclt" }, "unhealthy_untrusted": { @@ -25,7 +25,7 @@ "title": "Rendellenes rendszer - Nem megb\u00edzhat\u00f3 k\u00f3d" }, "unsupported": { - "description": "A rendszer nem t\u00e1mogatott a k\u00f6vetkez\u0151 miatt: {reason} . Haszn\u00e1lja a linket, ha t\u00f6bbet szeretne megtudni, \u00e9s hogyan jav\u00edthatja meg.", + "description": "A rendszer nem t\u00e1mogatott a k\u00f6vetkez\u0151 miatt: {reason}. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer \u2013 {reason}" }, "unsupported_apparmor": { @@ -41,19 +41,19 @@ "title": "Nem t\u00e1mogatott rendszer - Csatlakoz\u00e1si ellen\u0151rz\u00e9s letiltva" }, "unsupported_content_trust": { - "description": "A rendszer nem t\u00e1mogatott, mivel a Home Assistant nem tudja ellen\u0151rizni, hogy a futtatott tartalom megb\u00edzhat\u00f3 \u00e9s nem t\u00e1mad\u00f3k \u00e1ltal m\u00f3dos\u00edtott. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mivel a Home Assistant nem tudja ellen\u0151rizni, hogy a futtatott tartalom megb\u00edzhat\u00f3 \u00e9s nem t\u00e1mad\u00f3k \u00e1ltal m\u00f3dos\u00edtott. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - Tartalom-megb\u00edzhat\u00f3s\u00e1gi ellen\u0151rz\u00e9s letiltva" }, "unsupported_dbus": { - "description": "A rendszer nem t\u00e1mogatott, mert a D-Bus hib\u00e1san m\u0171k\u00f6dik. E n\u00e9lk\u00fcl sok minden meghib\u00e1sodik, mivel a Supervisor nem tud kommunik\u00e1lni a rendszerrel. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan lehet ezt kijav\u00edtani.", + "description": "A rendszer nem t\u00e1mogatott, mert a D-Bus hib\u00e1san m\u0171k\u00f6dik. E n\u00e9lk\u00fcl sok minden meghib\u00e1sodik, mivel a Supervisor nem tud kommunik\u00e1lni a rendszerrel. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - D-Bus probl\u00e9m\u00e1k" }, "unsupported_dns_server": { - "description": "A rendszer nem t\u00e1mogatott, mert a megadott DNS-kiszolg\u00e1l\u00f3 nem m\u0171k\u00f6dik megfelel\u0151en, \u00e9s a tartal\u00e9k DNS opci\u00f3t letiltott\u00e1k. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert a megadott DNS-kiszolg\u00e1l\u00f3 nem m\u0171k\u00f6dik megfelel\u0151en, \u00e9s a tartal\u00e9k DNS opci\u00f3t letiltott\u00e1k. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - DNS-kiszolg\u00e1l\u00f3 probl\u00e9m\u00e1k" }, "unsupported_docker_configuration": { - "description": "A rendszer nem t\u00e1mogatott, mert a Docker d\u00e9mon nem az elv\u00e1rt m\u00f3don fut. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert a Docker d\u00e9mon nem az elv\u00e1rt m\u00f3don fut. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer \u2013 A Docker helytelen\u00fcl van konfigur\u00e1lva" }, "unsupported_docker_version": { @@ -65,11 +65,11 @@ "title": "Nem t\u00e1mogatott rendszer \u2013 A v\u00e9delem le van tiltva" }, "unsupported_lxc": { - "description": "A rendszer nem t\u00e1mogatott, mert LXC virtu\u00e1lis g\u00e9pben fut. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert LXC virtu\u00e1lis g\u00e9pben fut. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - LXC \u00e9szlelve" }, "unsupported_network_manager": { - "description": "A rendszer nem t\u00e1mogatott, mert a H\u00e1l\u00f3zatkezel\u0151 hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert a H\u00e1l\u00f3zatkezel\u0151 hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - Network Manager probl\u00e9m\u00e1k" }, "unsupported_os": { @@ -77,35 +77,35 @@ "title": "Nem t\u00e1mogatott rendszer - Oper\u00e1ci\u00f3s rendszer" }, "unsupported_os_agent": { - "description": "A rendszer nem t\u00e1mogatott, mert az OS-\u00dcgyn\u00f6k (agent) hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert az OS-\u00dcgyn\u00f6k (agent) hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - OS-\u00dcgyn\u00f6k probl\u00e9m\u00e1k" }, "unsupported_restart_policy": { - "description": "A rendszer nem t\u00e1mogatott, mivel a Docker kont\u00e9nerben olyan \u00fajraind\u00edt\u00e1si h\u00e1zirend van be\u00e1ll\u00edtva, amely ind\u00edt\u00e1skor probl\u00e9m\u00e1kat okozhat. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mivel a Docker kont\u00e9nerben olyan \u00fajraind\u00edt\u00e1si h\u00e1zirend van be\u00e1ll\u00edtva, amely ind\u00edt\u00e1skor probl\u00e9m\u00e1kat okozhat. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - Kont\u00e9ner \u00fajraind\u00edt\u00e1si szab\u00e1lyzat" }, "unsupported_software": { - "description": "A rendszer nem t\u00e1mogatott, mert a rendszer a Home Assistant \u00f6kosziszt\u00e9m\u00e1n k\u00edv\u00fcli tov\u00e1bbi szoftvereket \u00e9szlelt. Haszn\u00e1lja a linket, ha t\u00f6bbet szeretne megtudni, \u00e9s hogyan jav\u00edthatja ezt.", + "description": "A rendszer nem t\u00e1mogatott, mert a rendszer a Home Assistant \u00f6kosziszt\u00e9m\u00e1n k\u00edv\u00fcli tov\u00e1bbi szoftvereket \u00e9szlelt. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Rendellenes rendszer - Nem t\u00e1mogatott szoftver" }, "unsupported_source_mods": { - "description": "A rendszer nem t\u00e1mogatott, mert a Supervisor forr\u00e1sk\u00f3dj\u00e1t m\u00f3dos\u00edtott\u00e1k. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert a Supervisor forr\u00e1sk\u00f3dj\u00e1t m\u00f3dos\u00edtott\u00e1k. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - Supervisor forr\u00e1sm\u00f3dos\u00edt\u00e1sok" }, "unsupported_supervisor_version": { - "description": "A rendszer nem t\u00e1mogatott, mert a Supervisor egy elavult verzi\u00f3ja van haszn\u00e1latban, \u00e9s az automatikus friss\u00edt\u00e9s le van tiltva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert a Supervisor egy elavult verzi\u00f3ja van haszn\u00e1latban, \u00e9s az automatikus friss\u00edt\u00e9s le van tiltva. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - Supervisor verzi\u00f3" }, "unsupported_systemd": { - "description": "A rendszer nem t\u00e1mogatott, mert a Systemd hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert a Systemd hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - Systemd probl\u00e9m\u00e1k" }, "unsupported_systemd_journal": { - "description": "A rendszer nem t\u00e1mogatott, mert a Systemd Journal \u00e9s/vagy az \u00e1tj\u00e1r\u00f3 szolg\u00e1ltat\u00e1s hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva . A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert a Systemd Journal \u00e9s/vagy az \u00e1tj\u00e1r\u00f3 szolg\u00e1ltat\u00e1s hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer - Systemd Journal probl\u00e9m\u00e1k" }, "unsupported_systemd_resolved": { - "description": "A rendszer nem t\u00e1mogatott, mert a Systemd Resolved hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet tudhat meg, \u00e9s megtudhatja, hogyan jav\u00edthatja ezt a hib\u00e1t.", + "description": "A rendszer nem t\u00e1mogatott, mert a Systemd Resolved hi\u00e1nyzik, inakt\u00edv vagy rosszul van konfigur\u00e1lva. A link seg\u00edts\u00e9g\u00e9vel tov\u00e1bbi inform\u00e1ci\u00f3t \u00e9s a probl\u00e9ma megold\u00e1s\u00e1nak m\u00f3dj\u00e1t ismerheti meg.", "title": "Nem t\u00e1mogatott rendszer \u2013 Systemd Resolved probl\u00e9m\u00e1k" } }, diff --git a/homeassistant/components/hassio/translations/nl.json b/homeassistant/components/hassio/translations/nl.json index 3a31897fd67..85995ed1e30 100644 --- a/homeassistant/components/hassio/translations/nl.json +++ b/homeassistant/components/hassio/translations/nl.json @@ -1,4 +1,12 @@ { + "issues": { + "unhealthy": { + "title": "Ongezond systeem - {reason}" + }, + "unsupported": { + "title": "Niet-ondersteund systeem - {reason}" + } + }, "system_health": { "info": { "agent_version": "Agent-versie", diff --git a/homeassistant/components/hassio/translations/tr.json b/homeassistant/components/hassio/translations/tr.json index cf92d597e23..22f8c5568b9 100644 --- a/homeassistant/components/hassio/translations/tr.json +++ b/homeassistant/components/hassio/translations/tr.json @@ -1,4 +1,114 @@ { + "issues": { + "unhealthy": { + "description": "{reason} nedeniyle sistem \u015fu anda sa\u011fl\u0131ks\u0131z. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Sa\u011fl\u0131ks\u0131z sistem - {reason}" + }, + "unhealthy_docker": { + "description": "Docker yanl\u0131\u015f yap\u0131land\u0131r\u0131ld\u0131\u011f\u0131ndan sistem \u015fu anda sa\u011fl\u0131ks\u0131z. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Sa\u011fl\u0131ks\u0131z sistem - Docker yanl\u0131\u015f yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "unhealthy_privileged": { + "description": "Docker \u00e7al\u0131\u015fma zaman\u0131na ayr\u0131cal\u0131kl\u0131 eri\u015fimi olmad\u0131\u011f\u0131 i\u00e7in sistem \u015fu anda sa\u011fl\u0131ks\u0131z. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Sa\u011fl\u0131ks\u0131z sistem - Ayr\u0131cal\u0131kl\u0131 de\u011fil" + }, + "unhealthy_setup": { + "description": "Kurulum tamamlanamad\u0131\u011f\u0131 i\u00e7in sistem \u015fu anda sa\u011fl\u0131ks\u0131z. Bunun birka\u00e7 nedeni olabilir, daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Sa\u011fl\u0131ks\u0131z sistem - Kurulum ba\u015far\u0131s\u0131z oldu" + }, + "unhealthy_supervisor": { + "description": "Supervisor'\u0131 en son s\u00fcr\u00fcme g\u00fcncelleme denemesi ba\u015far\u0131s\u0131z oldu\u011fu i\u00e7in sistem \u015fu anda sa\u011fl\u0131ks\u0131z. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Sa\u011fl\u0131ks\u0131z sistem - G\u00f6zetmen g\u00fcncellemesi ba\u015far\u0131s\u0131z oldu" + }, + "unhealthy_untrusted": { + "description": "Sistem, kullan\u0131mda olan g\u00fcvenilmeyen kod veya g\u00f6r\u00fcnt\u00fcleri alg\u0131lad\u0131\u011f\u0131ndan \u015fu anda sa\u011fl\u0131ks\u0131z durumda. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Sa\u011fl\u0131ks\u0131z sistem - G\u00fcvenilmeyen kod" + }, + "unsupported": { + "description": "{reason} nedeniyle sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - {reason}" + }, + "unsupported_apparmor": { + "description": "AppArmor hatal\u0131 \u00e7al\u0131\u015ft\u0131\u011f\u0131 ve eklentiler korumas\u0131z ve g\u00fcvenli olmayan bir \u015fekilde \u00e7al\u0131\u015ft\u0131\u011f\u0131 i\u00e7in sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - AppArmor sorunlar\u0131" + }, + "unsupported_cgroup_version": { + "description": "Docker CGroup'un yanl\u0131\u015f s\u00fcr\u00fcm\u00fc kullan\u0131mda oldu\u011fu i\u00e7in sistem desteklenmiyor. Do\u011fru s\u00fcr\u00fcm\u00fc ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - CGroup s\u00fcr\u00fcm\u00fc" + }, + "unsupported_connectivity_check": { + "description": "Home Assistant internet ba\u011flant\u0131s\u0131n\u0131n ne zaman kullan\u0131labilir oldu\u011funu belirleyemedi\u011fi i\u00e7in sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Ba\u011flant\u0131 kontrol\u00fc devre d\u0131\u015f\u0131" + }, + "unsupported_content_trust": { + "description": "Home Assistant \u00e7al\u0131\u015ft\u0131r\u0131lmakta olan i\u00e7eri\u011fin g\u00fcvenilir oldu\u011funu ve sald\u0131rganlar taraf\u0131ndan de\u011fi\u015ftirilmedi\u011fini do\u011frulayamad\u0131\u011f\u0131 i\u00e7in sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - \u0130\u00e7erik g\u00fcven denetimi devre d\u0131\u015f\u0131 b\u0131rak\u0131ld\u0131" + }, + "unsupported_dbus": { + "description": "D-Bus hatal\u0131 \u00e7al\u0131\u015ft\u0131\u011f\u0131 i\u00e7in sistem desteklenmiyor. S\u00fcperviz\u00f6r ev sahibi ile ileti\u015fim kuramad\u0131\u011f\u0131 i\u00e7in bu olmadan bir\u00e7ok \u015fey ba\u015far\u0131s\u0131z olur. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - D-Bus sorunlar\u0131" + }, + "unsupported_dns_server": { + "description": "Sa\u011flanan DNS sunucusu d\u00fczg\u00fcn \u00e7al\u0131\u015fmad\u0131\u011f\u0131ndan ve yedek DNS se\u00e7ene\u011fi devre d\u0131\u015f\u0131 b\u0131rak\u0131ld\u0131\u011f\u0131ndan sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - DNS sunucusu sorunlar\u0131" + }, + "unsupported_docker_configuration": { + "description": "Docker arka plan program\u0131 beklenmeyen bir \u015fekilde \u00e7al\u0131\u015ft\u0131\u011f\u0131 i\u00e7in sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Docker yanl\u0131\u015f yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "unsupported_docker_version": { + "description": "Docker'\u0131n yanl\u0131\u015f s\u00fcr\u00fcm\u00fc kullan\u0131mda oldu\u011fu i\u00e7in sistem desteklenmiyor. Do\u011fru s\u00fcr\u00fcm\u00fc ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Docker s\u00fcr\u00fcm\u00fc" + }, + "unsupported_job_conditions": { + "description": "Sistem desteklenmiyor \u00e7\u00fcnk\u00fc beklenmeyen ar\u0131za ve kesintilere kar\u015f\u0131 koruma sa\u011flayan bir veya daha fazla i\u015f ko\u015fulu devre d\u0131\u015f\u0131 b\u0131rak\u0131ld\u0131. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Korumalar devre d\u0131\u015f\u0131 b\u0131rak\u0131ld\u0131" + }, + "unsupported_lxc": { + "description": "Bir LXC sanal makinesinde \u00e7al\u0131\u015ft\u0131r\u0131ld\u0131\u011f\u0131 i\u00e7in sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - LXC alg\u0131land\u0131" + }, + "unsupported_network_manager": { + "description": "A\u011f Y\u00f6neticisi eksik, etkin de\u011fil veya yanl\u0131\u015f yap\u0131land\u0131r\u0131lm\u0131\u015f oldu\u011fundan sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - A\u011f Y\u00f6neticisi sorunlar\u0131" + }, + "unsupported_os": { + "description": "Kullan\u0131lan i\u015fletim sistemi Supervisor ile kullan\u0131m i\u00e7in test edilmedi\u011finden veya bak\u0131m\u0131 yap\u0131lmad\u0131\u011f\u0131ndan sistem desteklenmiyor. Hangi i\u015fletim sistemlerinin desteklendi\u011fini ve bunun nas\u0131l d\u00fczeltilece\u011fini \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - \u0130\u015fletim Sistemi" + }, + "unsupported_os_agent": { + "description": "OS-Agent eksik, etkin de\u011fil veya yanl\u0131\u015f yap\u0131land\u0131r\u0131lm\u0131\u015f oldu\u011fundan sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - OS-Agent sorunlar\u0131" + }, + "unsupported_restart_policy": { + "description": "Bir Docker kapsay\u0131c\u0131s\u0131, ba\u015flang\u0131\u00e7ta sorunlara neden olabilecek bir yeniden ba\u015flatma ilkesi k\u00fcmesine sahip oldu\u011fundan sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Konteyner yeniden ba\u015flatma politikas\u0131" + }, + "unsupported_software": { + "description": "Ev Asistan\u0131 ekosistemi d\u0131\u015f\u0131nda ek yaz\u0131l\u0131m tespit edildi\u011finden sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Desteklenmeyen yaz\u0131l\u0131m" + }, + "unsupported_source_mods": { + "description": "G\u00f6zetmen kaynak kodu de\u011fi\u015ftirildi\u011finden sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - G\u00f6zetmen kaynak de\u011fi\u015fiklikleri" + }, + "unsupported_supervisor_version": { + "description": "Supervisor'\u0131n g\u00fcncel olmayan bir s\u00fcr\u00fcm\u00fc kullan\u0131mda oldu\u011fu ve otomatik g\u00fcncelleme devre d\u0131\u015f\u0131 b\u0131rak\u0131ld\u0131\u011f\u0131 i\u00e7in sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Y\u00f6netici s\u00fcr\u00fcm\u00fc" + }, + "unsupported_systemd": { + "description": "Systemd eksik, etkin de\u011fil veya yanl\u0131\u015f yap\u0131land\u0131r\u0131lm\u0131\u015f oldu\u011fundan sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Systemd sorunlar\u0131" + }, + "unsupported_systemd_journal": { + "description": "Systemd Journal ve/veya a\u011f ge\u00e7idi hizmeti eksik, etkin de\u011fil veya yanl\u0131\u015f yap\u0131land\u0131r\u0131lm\u0131\u015f oldu\u011fundan sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Systemd Journal sorunlar\u0131" + }, + "unsupported_systemd_resolved": { + "description": "Systemd Resolved eksik, etkin de\u011fil veya yanl\u0131\u015f yap\u0131land\u0131r\u0131lm\u0131\u015f oldu\u011fundan sistem desteklenmiyor. Daha fazla bilgi edinmek ve bunu nas\u0131l d\u00fczeltece\u011finizi \u00f6\u011frenmek i\u00e7in ba\u011flant\u0131y\u0131 kullan\u0131n.", + "title": "Desteklenmeyen sistem - Systemd-\u00c7\u00f6z\u00fclm\u00fc\u015f sorunlar" + } + }, "system_health": { "info": { "agent_version": "Arac\u0131 S\u00fcr\u00fcm\u00fc", diff --git a/homeassistant/components/hassio/update.py b/homeassistant/components/hassio/update.py index dcb2b18cdd3..285a2663d92 100644 --- a/homeassistant/components/hassio/update.py +++ b/homeassistant/components/hassio/update.py @@ -167,8 +167,8 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity): await async_update_addon(self.hass, slug=self._addon_slug, backup=backup) except HassioAPIError as err: raise HomeAssistantError(f"Error updating {self.title}: {err}") from err - else: - await self.coordinator.force_info_update_supervisor() + + await self.coordinator.force_info_update_supervisor() class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity): diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 2df7c37a51c..e82873c3c23 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -139,11 +139,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } services.register(hass, controller) - - hass.config_entries.async_setup_platforms(entry, PLATFORMS) group_manager.connect_update() entry.async_on_unload(group_manager.disconnect_update) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index cca8ad2bf4f..0c7a4bad42e 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -5,10 +5,9 @@ from collections.abc import Awaitable, Callable, Coroutine from functools import reduce, wraps import logging from operator import ior -from typing import Any +from typing import Any, ParamSpec from pyheos import HeosError, const as heos_const -from typing_extensions import ParamSpec from homeassistant.components import media_source from homeassistant.components.media_player import ( diff --git a/homeassistant/components/here_travel_time/const.py b/homeassistant/components/here_travel_time/const.py index 300bbf617cf..a2a14a445d7 100644 --- a/homeassistant/components/here_travel_time/const.py +++ b/homeassistant/components/here_travel_time/const.py @@ -1,4 +1,6 @@ """Constants for the HERE Travel Time integration.""" +from typing import Final + DOMAIN = "here_travel_time" DEFAULT_SCAN_INTERVAL = 300 @@ -51,11 +53,11 @@ ICONS = { TRAVEL_MODE_TRUCK: ICON_TRUCK, } -ATTR_DURATION = "duration" -ATTR_DISTANCE = "distance" -ATTR_ORIGIN = "origin" -ATTR_DESTINATION = "destination" +ATTR_DURATION: Final = "duration" +ATTR_DISTANCE: Final = "distance" +ATTR_ORIGIN: Final = "origin" +ATTR_DESTINATION: Final = "destination" -ATTR_DURATION_IN_TRAFFIC = "duration_in_traffic" -ATTR_ORIGIN_NAME = "origin_name" -ATTR_DESTINATION_NAME = "destination_name" +ATTR_DURATION_IN_TRAFFIC: Final = "duration_in_traffic" +ATTR_ORIGIN_NAME: Final = "origin_name" +ATTR_DESTINATION_NAME: Final = "destination_name" diff --git a/homeassistant/components/here_travel_time/coordinator.py b/homeassistant/components/here_travel_time/coordinator.py index 56430d79a22..e76691253f9 100644 --- a/homeassistant/components/here_travel_time/coordinator.py +++ b/homeassistant/components/here_travel_time/coordinator.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime, time, timedelta import logging -from typing import Any +from typing import Any, Optional import here_routing from here_routing import ( @@ -40,7 +40,7 @@ BACKOFF_MULTIPLIER = 1.1 _LOGGER = logging.getLogger(__name__) -class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator): +class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]): """here_routing DataUpdateCoordinator.""" def __init__( @@ -59,7 +59,7 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator): self._api = HERERoutingApi(api_key) self.config = config - async def _async_update_data(self) -> HERETravelTimeData | None: + async def _async_update_data(self) -> HERETravelTimeData: """Get the latest data from the HERE Routing API.""" origin, destination, arrival, departure = prepare_parameters( self.hass, self.config @@ -117,25 +117,43 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator): def _parse_routing_response(self, response: dict[str, Any]) -> HERETravelTimeData: """Parse the routing response dict to a HERETravelTimeData.""" - section: dict[str, Any] = response["routes"][0]["sections"][0] - summary: dict[str, int] = section["summary"] - mapped_origin_lat: float = section["departure"]["place"]["location"]["lat"] - mapped_origin_lon: float = section["departure"]["place"]["location"]["lng"] - mapped_destination_lat: float = section["arrival"]["place"]["location"]["lat"] - mapped_destination_lon: float = section["arrival"]["place"]["location"]["lng"] - distance: float = DistanceConverter.convert( - summary["length"], UnitOfLength.METERS, UnitOfLength.KILOMETERS - ) + distance: float = 0.0 + duration: float = 0.0 + duration_in_traffic: float = 0.0 + + for section in response["routes"][0]["sections"]: + distance += DistanceConverter.convert( + section["summary"]["length"], + UnitOfLength.METERS, + UnitOfLength.KILOMETERS, + ) + duration += section["summary"]["baseDuration"] + duration_in_traffic += section["summary"]["duration"] + + first_section = response["routes"][0]["sections"][0] + last_section = response["routes"][0]["sections"][-1] + mapped_origin_lat: float = first_section["departure"]["place"]["location"][ + "lat" + ] + mapped_origin_lon: float = first_section["departure"]["place"]["location"][ + "lng" + ] + mapped_destination_lat: float = last_section["arrival"]["place"]["location"][ + "lat" + ] + mapped_destination_lon: float = last_section["arrival"]["place"]["location"][ + "lng" + ] origin_name: str | None = None - if (names := section["spans"][0].get("names")) is not None: + if (names := first_section["spans"][0].get("names")) is not None: origin_name = names[0]["value"] destination_name: str | None = None - if (names := section["spans"][-1].get("names")) is not None: + if (names := last_section["spans"][-1].get("names")) is not None: destination_name = names[0]["value"] return HERETravelTimeData( attribution=None, - duration=round(summary["baseDuration"] / 60), - duration_in_traffic=round(summary["duration"] / 60), + duration=round(duration / 60), + duration_in_traffic=round(duration_in_traffic / 60), distance=distance, origin=f"{mapped_origin_lat},{mapped_origin_lon}", destination=f"{mapped_destination_lat},{mapped_destination_lon}", @@ -144,7 +162,9 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator): ) -class HERETransitDataUpdateCoordinator(DataUpdateCoordinator): +class HERETransitDataUpdateCoordinator( + DataUpdateCoordinator[Optional[HERETravelTimeData]] +): """HERETravelTime DataUpdateCoordinator.""" def __init__( diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 445efb92dcf..91abfbd7652 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -102,7 +102,12 @@ async def async_setup_entry( async_add_entities(sensors) -class HERETravelTimeSensor(CoordinatorEntity, RestoreSensor): +class HERETravelTimeSensor( + CoordinatorEntity[ + HERERoutingDataUpdateCoordinator | HERETransitDataUpdateCoordinator + ], + RestoreSensor, +): """Representation of a HERE travel time sensor.""" def __init__( @@ -144,7 +149,7 @@ class HERETravelTimeSensor(CoordinatorEntity, RestoreSensor): def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" if self.coordinator.data is not None: - self._attr_native_value = self.coordinator.data.get( + self._attr_native_value = self.coordinator.data.get( # type: ignore[assignment] self.entity_description.key ) self.async_write_ha_state() diff --git a/homeassistant/components/here_travel_time/translations/lv.json b/homeassistant/components/here_travel_time/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/here_travel_time/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/translations/nl.json b/homeassistant/components/here_travel_time/translations/nl.json index 7d3438901cc..e9dba21dc7f 100644 --- a/homeassistant/components/here_travel_time/translations/nl.json +++ b/homeassistant/components/here_travel_time/translations/nl.json @@ -43,7 +43,8 @@ "menu_options": { "origin_coordinates": "Een kaartlocatie gebruiken", "origin_entity": "Een entiteit gebruiken" - } + }, + "title": "Kies vertrekpunt" }, "user": { "data": { diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index f07fe82b50d..05d62058351 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -1,45 +1,54 @@ """Provide pre-made queries on top of the recorder component.""" from __future__ import annotations -from collections.abc import Iterable from datetime import datetime as dt, timedelta from http import HTTPStatus import logging import time -from typing import Any, cast +from typing import cast from aiohttp import web import voluptuous as vol -from homeassistant.components import frontend, websocket_api +from homeassistant.components import frontend from homeassistant.components.http import HomeAssistantView -from homeassistant.components.recorder import get_instance, history +from homeassistant.components.recorder import ( + DOMAIN as RECORDER_DOMAIN, + get_instance, + history, +) from homeassistant.components.recorder.filters import ( Filters, + extract_include_exclude_filter_conf, + merge_include_exclude_filters, sqlalchemy_filter_from_include_exclude_conf, ) from homeassistant.components.recorder.util import session_scope -from homeassistant.components.websocket_api import messages from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entityfilter import INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA -from homeassistant.helpers.json import JSON_DUMP +from homeassistant.helpers.entityfilter import ( + INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, + convert_include_exclude_filter, +) from homeassistant.helpers.typing import ConfigType import homeassistant.util.dt as dt_util -_LOGGER = logging.getLogger(__name__) +from . import websocket_api +from .const import DOMAIN +from .helpers import entities_may_have_state_changes_after +from .models import HistoryConfig -DOMAIN = "history" -HISTORY_FILTERS = "history_filters" -HISTORY_USE_INCLUDE_ORDER = "history_use_include_order" +_LOGGER = logging.getLogger(__name__) CONF_ORDER = "use_include_order" - CONFIG_SCHEMA = vol.Schema( { - DOMAIN: INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend( - {vol.Optional(CONF_ORDER, default=False): cv.boolean} + DOMAIN: vol.All( + cv.deprecated(CONF_ORDER), + INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend( + {vol.Optional(CONF_ORDER, default=False): cv.boolean} + ), ) }, extra=vol.ALLOW_EXTRA, @@ -49,134 +58,27 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the history hooks.""" conf = config.get(DOMAIN, {}) + recorder_conf = config.get(RECORDER_DOMAIN, {}) + history_conf = config.get(DOMAIN, {}) + recorder_filter = extract_include_exclude_filter_conf(recorder_conf) + logbook_filter = extract_include_exclude_filter_conf(history_conf) + merged_filter = merge_include_exclude_filters(recorder_filter, logbook_filter) - hass.data[HISTORY_FILTERS] = filters = sqlalchemy_filter_from_include_exclude_conf( - conf - ) - hass.data[HISTORY_USE_INCLUDE_ORDER] = use_include_order = conf.get(CONF_ORDER) + possible_merged_entities_filter = convert_include_exclude_filter(merged_filter) - hass.http.register_view(HistoryPeriodView(filters, use_include_order)) + sqlalchemy_filter = None + entity_filter = None + if not possible_merged_entities_filter.empty_filter: + sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(conf) + entity_filter = possible_merged_entities_filter + + hass.data[DOMAIN] = HistoryConfig(sqlalchemy_filter, entity_filter) + hass.http.register_view(HistoryPeriodView(sqlalchemy_filter)) frontend.async_register_built_in_panel(hass, "history", "history", "hass:chart-box") - websocket_api.async_register_command(hass, ws_get_history_during_period) - + websocket_api.async_setup(hass) return True -def _ws_get_significant_states( - hass: HomeAssistant, - msg_id: int, - start_time: dt, - end_time: dt | None, - entity_ids: list[str] | None, - filters: Filters | None, - use_include_order: bool | None, - include_start_time_state: bool, - significant_changes_only: bool, - minimal_response: bool, - no_attributes: bool, -) -> str: - """Fetch history significant_states and convert them to json in the executor.""" - states = history.get_significant_states( - hass, - start_time, - end_time, - entity_ids, - filters, - include_start_time_state, - significant_changes_only, - minimal_response, - no_attributes, - True, - ) - - if not use_include_order or not filters: - return JSON_DUMP(messages.result_message(msg_id, states)) - - return JSON_DUMP( - messages.result_message( - msg_id, - { - order_entity: states.pop(order_entity) - for order_entity in filters.included_entities - if order_entity in states - } - | states, - ) - ) - - -@websocket_api.websocket_command( - { - vol.Required("type"): "history/history_during_period", - vol.Required("start_time"): str, - vol.Optional("end_time"): str, - vol.Optional("entity_ids"): [str], - vol.Optional("include_start_time_state", default=True): bool, - vol.Optional("significant_changes_only", default=True): bool, - vol.Optional("minimal_response", default=False): bool, - vol.Optional("no_attributes", default=False): bool, - } -) -@websocket_api.async_response -async def ws_get_history_during_period( - hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] -) -> None: - """Handle history during period websocket command.""" - start_time_str = msg["start_time"] - end_time_str = msg.get("end_time") - - if start_time := dt_util.parse_datetime(start_time_str): - start_time = dt_util.as_utc(start_time) - else: - connection.send_error(msg["id"], "invalid_start_time", "Invalid start_time") - return - - if end_time_str: - if end_time := dt_util.parse_datetime(end_time_str): - end_time = dt_util.as_utc(end_time) - else: - connection.send_error(msg["id"], "invalid_end_time", "Invalid end_time") - return - else: - end_time = None - - if start_time > dt_util.utcnow(): - connection.send_result(msg["id"], {}) - return - - entity_ids = msg.get("entity_ids") - include_start_time_state = msg["include_start_time_state"] - - if ( - not include_start_time_state - and entity_ids - and not _entities_may_have_state_changes_after(hass, entity_ids, start_time) - ): - connection.send_result(msg["id"], {}) - return - - significant_changes_only = msg["significant_changes_only"] - no_attributes = msg["no_attributes"] - minimal_response = msg["minimal_response"] - - connection.send_message( - await get_instance(hass).async_add_executor_job( - _ws_get_significant_states, - hass, - msg["id"], - start_time, - end_time, - entity_ids, - hass.data[HISTORY_FILTERS], - hass.data[HISTORY_USE_INCLUDE_ORDER], - include_start_time_state, - significant_changes_only, - minimal_response, - no_attributes, - ) - ) - - class HistoryPeriodView(HomeAssistantView): """Handle history period requests.""" @@ -184,10 +86,9 @@ class HistoryPeriodView(HomeAssistantView): name = "api:history:view-period" extra_urls = ["/api/history/period/{datetime}"] - def __init__(self, filters: Filters | None, use_include_order: bool) -> None: + def __init__(self, filters: Filters | None) -> None: """Initialize the history period view.""" self.filters = filters - self.use_include_order = use_include_order async def get( self, request: web.Request, datetime: str | None = None @@ -232,7 +133,9 @@ class HistoryPeriodView(HomeAssistantView): if ( not include_start_time_state and entity_ids - and not _entities_may_have_state_changes_after(hass, entity_ids, start_time) + and not entities_may_have_state_changes_after( + hass, entity_ids, start_time, no_attributes + ) ): return self.json([]) @@ -285,28 +188,4 @@ class HistoryPeriodView(HomeAssistantView): "Extracted %d states in %fs", sum(map(len, states.values())), elapsed ) - # Optionally reorder the result to respect the ordering given - # by any entities explicitly included in the configuration. - if not self.filters or not self.use_include_order: - return self.json(list(states.values())) - - sorted_result = [ - states.pop(order_entity) - for order_entity in self.filters.included_entities - if order_entity in states - ] - sorted_result.extend(list(states.values())) - return self.json(sorted_result) - - -def _entities_may_have_state_changes_after( - hass: HomeAssistant, entity_ids: Iterable, start_time: dt -) -> bool: - """Check the state machine to see if entities have changed since start time.""" - for entity_id in entity_ids: - state = hass.states.get(entity_id) - - if state is None or state.last_changed > start_time: - return True - - return False + return self.json(list(states.values())) diff --git a/homeassistant/components/history/const.py b/homeassistant/components/history/const.py new file mode 100644 index 00000000000..f8323ed6967 --- /dev/null +++ b/homeassistant/components/history/const.py @@ -0,0 +1,7 @@ +"""History integration constants.""" + +DOMAIN = "history" + +EVENT_COALESCE_TIME = 0.35 + +MAX_PENDING_HISTORY_STATES = 2048 diff --git a/homeassistant/components/history/helpers.py b/homeassistant/components/history/helpers.py new file mode 100644 index 00000000000..523b1fafb7f --- /dev/null +++ b/homeassistant/components/history/helpers.py @@ -0,0 +1,23 @@ +"""Helpers for the history integration.""" +from __future__ import annotations + +from collections.abc import Iterable +from datetime import datetime as dt + +from homeassistant.core import HomeAssistant + + +def entities_may_have_state_changes_after( + hass: HomeAssistant, entity_ids: Iterable, start_time: dt, no_attributes: bool +) -> bool: + """Check the state machine to see if entities have changed since start time.""" + for entity_id in entity_ids: + state = hass.states.get(entity_id) + if state is None: + return True + + state_time = state.last_changed if no_attributes else state.last_updated + if state_time > start_time: + return True + + return False diff --git a/homeassistant/components/history/models.py b/homeassistant/components/history/models.py new file mode 100644 index 00000000000..3998d9f7e00 --- /dev/null +++ b/homeassistant/components/history/models.py @@ -0,0 +1,15 @@ +"""Models for the history integration.""" +from __future__ import annotations + +from dataclasses import dataclass + +from homeassistant.components.recorder.filters import Filters +from homeassistant.helpers.entityfilter import EntityFilter + + +@dataclass +class HistoryConfig: + """Configuration for the history integration.""" + + sqlalchemy_filter: Filters | None = None + entity_filter: EntityFilter | None = None diff --git a/homeassistant/components/history/websocket_api.py b/homeassistant/components/history/websocket_api.py new file mode 100644 index 00000000000..5d0eb59942b --- /dev/null +++ b/homeassistant/components/history/websocket_api.py @@ -0,0 +1,564 @@ +"""Websocket API for the history integration.""" +from __future__ import annotations + +import asyncio +from collections.abc import Callable, Iterable, MutableMapping +from dataclasses import dataclass +from datetime import datetime as dt +import logging +from typing import Any, cast + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.components.recorder import get_instance, history +from homeassistant.components.recorder.filters import Filters +from homeassistant.components.websocket_api import messages +from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.const import ( + COMPRESSED_STATE_ATTRIBUTES, + COMPRESSED_STATE_LAST_CHANGED, + COMPRESSED_STATE_LAST_UPDATED, + COMPRESSED_STATE_STATE, + EVENT_STATE_CHANGED, +) +from homeassistant.core import ( + CALLBACK_TYPE, + Event, + HomeAssistant, + State, + callback, + is_callback, +) +from homeassistant.helpers.entityfilter import EntityFilter +from homeassistant.helpers.event import ( + async_track_point_in_utc_time, + async_track_state_change_event, +) +from homeassistant.helpers.json import JSON_DUMP +import homeassistant.util.dt as dt_util + +from .const import DOMAIN, EVENT_COALESCE_TIME, MAX_PENDING_HISTORY_STATES +from .helpers import entities_may_have_state_changes_after +from .models import HistoryConfig + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class HistoryLiveStream: + """Track a history live stream.""" + + stream_queue: asyncio.Queue[Event] + subscriptions: list[CALLBACK_TYPE] + end_time_unsub: CALLBACK_TYPE | None = None + task: asyncio.Task | None = None + wait_sync_task: asyncio.Task | None = None + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Set up the history websocket API.""" + websocket_api.async_register_command(hass, ws_get_history_during_period) + websocket_api.async_register_command(hass, ws_stream) + + +def _ws_get_significant_states( + hass: HomeAssistant, + msg_id: int, + start_time: dt, + end_time: dt | None, + entity_ids: list[str] | None, + filters: Filters | None, + include_start_time_state: bool, + significant_changes_only: bool, + minimal_response: bool, + no_attributes: bool, +) -> str: + """Fetch history significant_states and convert them to json in the executor.""" + return JSON_DUMP( + messages.result_message( + msg_id, + history.get_significant_states( + hass, + start_time, + end_time, + entity_ids, + filters, + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + True, + ), + ) + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "history/history_during_period", + vol.Required("start_time"): str, + vol.Optional("end_time"): str, + vol.Optional("entity_ids"): [str], + vol.Optional("include_start_time_state", default=True): bool, + vol.Optional("significant_changes_only", default=True): bool, + vol.Optional("minimal_response", default=False): bool, + vol.Optional("no_attributes", default=False): bool, + } +) +@websocket_api.async_response +async def ws_get_history_during_period( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle history during period websocket command.""" + start_time_str = msg["start_time"] + end_time_str = msg.get("end_time") + + if start_time := dt_util.parse_datetime(start_time_str): + start_time = dt_util.as_utc(start_time) + else: + connection.send_error(msg["id"], "invalid_start_time", "Invalid start_time") + return + + if end_time_str: + if end_time := dt_util.parse_datetime(end_time_str): + end_time = dt_util.as_utc(end_time) + else: + connection.send_error(msg["id"], "invalid_end_time", "Invalid end_time") + return + else: + end_time = None + + if start_time > dt_util.utcnow(): + connection.send_result(msg["id"], {}) + return + + entity_ids = msg.get("entity_ids") + include_start_time_state = msg["include_start_time_state"] + no_attributes = msg["no_attributes"] + + if ( + not include_start_time_state + and entity_ids + and not entities_may_have_state_changes_after( + hass, entity_ids, start_time, no_attributes + ) + ): + connection.send_result(msg["id"], {}) + return + + significant_changes_only = msg["significant_changes_only"] + minimal_response = msg["minimal_response"] + history_config: HistoryConfig = hass.data[DOMAIN] + + connection.send_message( + await get_instance(hass).async_add_executor_job( + _ws_get_significant_states, + hass, + msg["id"], + start_time, + end_time, + entity_ids, + history_config.sqlalchemy_filter, + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + ) + ) + + +def _generate_stream_message( + states: MutableMapping[str, list[dict[str, Any]]], + start_day: dt, + end_day: dt, +) -> dict[str, Any]: + """Generate a history stream message response.""" + return { + "states": states, + "start_time": dt_util.utc_to_timestamp(start_day), + "end_time": dt_util.utc_to_timestamp(end_day), + } + + +@callback +def _async_send_empty_response( + connection: ActiveConnection, msg_id: int, start_time: dt, end_time: dt | None +) -> None: + """Send an empty response when we know all results are filtered away.""" + connection.send_result(msg_id) + stream_end_time = end_time or dt_util.utcnow() + _async_send_response(connection, msg_id, start_time, stream_end_time, {}) + + +@callback +def _async_send_response( + connection: ActiveConnection, + msg_id: int, + start_time: dt, + end_time: dt, + states: MutableMapping[str, list[dict[str, Any]]], +) -> None: + """Send a response.""" + empty_stream_message = _generate_stream_message(states, start_time, end_time) + empty_response = messages.event_message(msg_id, empty_stream_message) + connection.send_message(JSON_DUMP(empty_response)) + + +async def _async_send_historical_states( + hass: HomeAssistant, + connection: ActiveConnection, + msg_id: int, + start_time: dt, + end_time: dt, + entity_ids: list[str] | None, + filters: Filters | None, + include_start_time_state: bool, + significant_changes_only: bool, + minimal_response: bool, + no_attributes: bool, + send_empty: bool, +) -> dt | None: + """Fetch history significant_states and send them to the client.""" + states = cast( + MutableMapping[str, list[dict[str, Any]]], + await get_instance(hass).async_add_executor_job( + history.get_significant_states, + hass, + start_time, + end_time, + entity_ids, + filters, + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + True, + ), + ) + last_time = 0 + + for state_list in states.values(): + if ( + state_list + and (state_last_time := state_list[-1][COMPRESSED_STATE_LAST_UPDATED]) + > last_time + ): + last_time = state_last_time + + if last_time == 0: + # If we did not send any states ever, we need to send an empty response + # so the websocket client knows it should render/process/consume the + # data. + if not send_empty: + return None + last_time_dt = end_time + else: + last_time_dt = dt_util.utc_from_timestamp(last_time) + _async_send_response(connection, msg_id, start_time, last_time_dt, states) + return last_time_dt if last_time != 0 else None + + +def _history_compressed_state(state: State, no_attributes: bool) -> dict[str, Any]: + """Convert a state to a compressed state.""" + comp_state: dict[str, Any] = {COMPRESSED_STATE_STATE: state.state} + if not no_attributes or state.domain in history.NEED_ATTRIBUTE_DOMAINS: + comp_state[COMPRESSED_STATE_ATTRIBUTES] = state.attributes + comp_state[COMPRESSED_STATE_LAST_UPDATED] = dt_util.utc_to_timestamp( + state.last_updated + ) + if state.last_changed != state.last_updated: + comp_state[COMPRESSED_STATE_LAST_CHANGED] = dt_util.utc_to_timestamp( + state.last_changed + ) + return comp_state + + +def _events_to_compressed_states( + events: Iterable[Event], no_attributes: bool +) -> MutableMapping[str, list[dict[str, Any]]]: + """Convert events to a compressed states.""" + states_by_entity_ids: dict[str, list[dict[str, Any]]] = {} + for event in events: + state: State = event.data["new_state"] + entity_id: str = state.entity_id + states_by_entity_ids.setdefault(entity_id, []).append( + _history_compressed_state(state, no_attributes) + ) + return states_by_entity_ids + + +async def _async_events_consumer( + subscriptions_setup_complete_time: dt, + connection: ActiveConnection, + msg_id: int, + stream_queue: asyncio.Queue[Event], + no_attributes: bool, +) -> None: + """Stream events from the queue.""" + while True: + events: list[Event] = [await stream_queue.get()] + # If the event is older than the last db + # event we already sent it so we skip it. + if events[0].time_fired <= subscriptions_setup_complete_time: + continue + # We sleep for the EVENT_COALESCE_TIME so + # we can group events together to minimize + # the number of websocket messages when the + # system is overloaded with an event storm + await asyncio.sleep(EVENT_COALESCE_TIME) + while not stream_queue.empty(): + events.append(stream_queue.get_nowait()) + + if history_states := _events_to_compressed_states(events, no_attributes): + connection.send_message( + JSON_DUMP( + messages.event_message( + msg_id, + {"states": history_states}, + ) + ) + ) + + +@callback +def _async_subscribe_events( + hass: HomeAssistant, + subscriptions: list[CALLBACK_TYPE], + target: Callable[[Event], None], + entities_filter: EntityFilter | None, + entity_ids: list[str] | None, + significant_changes_only: bool, + minimal_response: bool, +) -> None: + """Subscribe to events for the entities and devices or all. + + These are the events we need to listen for to do + the live history stream. + """ + assert is_callback(target), "target must be a callback" + + @callback + def _forward_state_events_filtered(event: Event) -> None: + """Filter state events and forward them.""" + if (new_state := event.data.get("new_state")) is None or ( + old_state := event.data.get("old_state") + ) is None: + return + assert isinstance(new_state, State) + assert isinstance(old_state, State) + if (entities_filter and not entities_filter(new_state.entity_id)) or ( + (significant_changes_only or minimal_response) + and new_state.state == old_state.state + and new_state.domain not in history.SIGNIFICANT_DOMAINS + ): + return + target(event) + + if entity_ids: + subscriptions.append( + async_track_state_change_event( + hass, entity_ids, _forward_state_events_filtered + ) + ) + return + + # We want the firehose + subscriptions.append( + hass.bus.async_listen( + EVENT_STATE_CHANGED, + _forward_state_events_filtered, + run_immediately=True, + ) + ) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "history/stream", + vol.Required("start_time"): str, + vol.Optional("end_time"): str, + vol.Optional("entity_ids"): [str], + vol.Optional("include_start_time_state", default=True): bool, + vol.Optional("significant_changes_only", default=True): bool, + vol.Optional("minimal_response", default=False): bool, + vol.Optional("no_attributes", default=False): bool, + } +) +@websocket_api.async_response +async def ws_stream( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: + """Handle history stream websocket command.""" + start_time_str = msg["start_time"] + msg_id: int = msg["id"] + entity_ids: list[str] | None = msg.get("entity_ids") + utc_now = dt_util.utcnow() + filters: Filters | None = None + entities_filter: EntityFilter | None = None + + if not entity_ids: + history_config: HistoryConfig = hass.data[DOMAIN] + filters = history_config.sqlalchemy_filter + entities_filter = history_config.entity_filter + + if start_time := dt_util.parse_datetime(start_time_str): + start_time = dt_util.as_utc(start_time) + + if not start_time or start_time > utc_now: + connection.send_error(msg_id, "invalid_start_time", "Invalid start_time") + return + + end_time_str = msg.get("end_time") + end_time: dt | None = None + if end_time_str: + if not (end_time := dt_util.parse_datetime(end_time_str)): + connection.send_error(msg_id, "invalid_end_time", "Invalid end_time") + return + end_time = dt_util.as_utc(end_time) + if end_time < start_time: + connection.send_error(msg_id, "invalid_end_time", "Invalid end_time") + return + + entity_ids = msg.get("entity_ids") + include_start_time_state = msg["include_start_time_state"] + significant_changes_only = msg["significant_changes_only"] + no_attributes = msg["no_attributes"] + minimal_response = msg["minimal_response"] + + if end_time and end_time <= utc_now: + if ( + not include_start_time_state + and entity_ids + and not entities_may_have_state_changes_after( + hass, entity_ids, start_time, no_attributes + ) + ): + _async_send_empty_response(connection, msg_id, start_time, end_time) + return + + connection.subscriptions[msg_id] = callback(lambda: None) + connection.send_result(msg_id) + await _async_send_historical_states( + hass, + connection, + msg_id, + start_time, + end_time, + entity_ids, + filters, + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + True, + ) + return + + subscriptions: list[CALLBACK_TYPE] = [] + stream_queue: asyncio.Queue[Event] = asyncio.Queue(MAX_PENDING_HISTORY_STATES) + live_stream = HistoryLiveStream( + subscriptions=subscriptions, stream_queue=stream_queue + ) + + @callback + def _unsub(*_utc_time: Any) -> None: + """Unsubscribe from all events.""" + for subscription in subscriptions: + subscription() + subscriptions.clear() + if live_stream.task: + live_stream.task.cancel() + if live_stream.wait_sync_task: + live_stream.wait_sync_task.cancel() + if live_stream.end_time_unsub: + live_stream.end_time_unsub() + live_stream.end_time_unsub = None + + if end_time: + live_stream.end_time_unsub = async_track_point_in_utc_time( + hass, _unsub, end_time + ) + + @callback + def _queue_or_cancel(event: Event) -> None: + """Queue an event to be processed or cancel.""" + try: + stream_queue.put_nowait(event) + except asyncio.QueueFull: + _LOGGER.debug( + "Client exceeded max pending messages of %s", + MAX_PENDING_HISTORY_STATES, + ) + _unsub() + + _async_subscribe_events( + hass, + subscriptions, + _queue_or_cancel, + entities_filter, + entity_ids, + significant_changes_only=significant_changes_only, + minimal_response=minimal_response, + ) + subscriptions_setup_complete_time = dt_util.utcnow() + connection.subscriptions[msg_id] = _unsub + connection.send_result(msg_id) + # Fetch everything from history + last_event_time = await _async_send_historical_states( + hass, + connection, + msg_id, + start_time, + subscriptions_setup_complete_time, + entity_ids, + filters, + include_start_time_state, + significant_changes_only, + minimal_response, + no_attributes, + True, + ) + + if msg_id not in connection.subscriptions: + # Unsubscribe happened while sending historical states + return + + live_stream.task = asyncio.create_task( + _async_events_consumer( + subscriptions_setup_complete_time, + connection, + msg_id, + stream_queue, + no_attributes, + ) + ) + + live_stream.wait_sync_task = asyncio.create_task( + get_instance(hass).async_block_till_done() + ) + await live_stream.wait_sync_task + + # + # Fetch any states from the database that have + # not been committed since the original fetch + # so we can switch over to using the subscriptions + # + # We only want states that happened after the last state + # we had from the last database query + # + await _async_send_historical_states( + hass, + connection, + msg_id, + last_event_time or start_time, + subscriptions_setup_complete_time, + entity_ids, + filters, + False, # We don't want the start time state again + significant_changes_only, + minimal_response, + no_attributes, + send_empty=not last_event_time, + ) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index bd74ecbff11..4d309fe6847 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -4,12 +4,11 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine from functools import wraps import logging -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from aiohttp.web_exceptions import HTTPException from apyhiveapi import Auth, Hive from apyhiveapi.helper.hive_exceptions import HiveReauthRequired -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/hive/translations/el.json b/homeassistant/components/hive/translations/el.json index 3c6be9f4eba..62131fe427f 100644 --- a/homeassistant/components/hive/translations/el.json +++ b/homeassistant/components/hive/translations/el.json @@ -41,7 +41,7 @@ "scan_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03a3\u03ac\u03c1\u03c9\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1)", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03a7\u03c1\u03ae\u03c3\u03c4\u03b7" }, - "description": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03ba\u03b1\u03b9 \u03c4\u03b9\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf Hive.", + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\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 Hive.", "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03b5 Hive" } } diff --git a/homeassistant/components/hive/translations/lt.json b/homeassistant/components/hive/translations/lt.json new file mode 100644 index 00000000000..6557873cf86 --- /dev/null +++ b/homeassistant/components/hive/translations/lt.json @@ -0,0 +1,26 @@ +{ + "config": { + "step": { + "configuration": { + "data": { + "device_name": "\u012erenginio pavadinimas" + }, + "description": "\u012eveskite \u201eHive\u201c konfig\u016bracij\u0105", + "title": "Hive konfig\u016bracija" + }, + "user": { + "title": "Prisijungimas prie Hive" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "scan_interval": "Nuskaitymo intervalas (sekund\u0117mis)" + }, + "title": "Hive parinktys" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hive/translations/uk.json b/homeassistant/components/hive/translations/uk.json new file mode 100644 index 00000000000..ee9ebffabcb --- /dev/null +++ b/homeassistant/components/hive/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "invalid_username": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0432\u0456\u0439\u0442\u0438 \u0432 Hive. \u0412\u0430\u0448\u0430 \u0430\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438 \u043d\u0435 \u0440\u043e\u0437\u043f\u0456\u0437\u043d\u0430\u043d\u0430." + }, + "step": { + "reauth": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/config_flow.py b/homeassistant/components/hlk_sw16/config_flow.py index ca65647a448..83389472607 100644 --- a/homeassistant/components/hlk_sw16/config_flow.py +++ b/homeassistant/components/hlk_sw16/config_flow.py @@ -44,6 +44,7 @@ async def validate_input(hass: HomeAssistant, user_input): client = await connect_client(hass, user_input) except asyncio.TimeoutError as err: raise CannotConnect from err + try: def disconnect_callback(): @@ -56,9 +57,9 @@ async def validate_input(hass: HomeAssistant, user_input): client.disconnect_callback = None client.stop() raise - else: - client.disconnect_callback = None - client.stop() + + client.disconnect_callback = None + client.stop() class SW16FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/hlk_sw16/translations/lt.json b/homeassistant/components/hlk_sw16/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/hu.json b/homeassistant/components/homeassistant/translations/hu.json index 3b89dc26fbc..cb13bf2556e 100644 --- a/homeassistant/components/homeassistant/translations/hu.json +++ b/homeassistant/components/homeassistant/translations/hu.json @@ -5,11 +5,11 @@ "title": "Az orsz\u00e1g nincs konfigur\u00e1lva" }, "historic_currency": { - "description": "{currency} p\u00e9nznem m\u00e1r nincs haszn\u00e1latban, k\u00e9rj\u00fck, konfigur\u00e1lja \u00fajra a p\u00e9nznemet.", + "description": "{currency} p\u00e9nznem m\u00e1r nincs haszn\u00e1latban, k\u00e9rem, konfigur\u00e1lja \u00fajra a p\u00e9nznemet.", "title": "A be\u00e1ll\u00edtott p\u00e9nznem m\u00e1r nincs haszn\u00e1latban" }, "python_version": { - "description": "A Home Assistant futtat\u00e1s\u00e1nak t\u00e1mogat\u00e1sa az aktu\u00e1lisan haszn\u00e1lt Python-verzi\u00f3ban {current_python_version} elavult, \u00e9s a Home Assistant {breaks_in_ha_version} verzi\u00f3j\u00e1ban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl. K\u00e9rj\u00fck, friss\u00edtse a Pythont a {required_python_version} verzi\u00f3ra, hogy megakad\u00e1lyozza a Home Assistant j\u00f6v\u0151beni m\u0171k\u00f6d\u00e9sk\u00e9ptelens\u00e9g\u00e9t.", + "description": "A Home Assistant futtat\u00e1s\u00e1nak t\u00e1mogat\u00e1sa az aktu\u00e1lisan haszn\u00e1lt Python-verzi\u00f3ban {current_python_version} elavult, \u00e9s a Home Assistant {breaks_in_ha_version} verzi\u00f3j\u00e1ban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl. K\u00e9rem, friss\u00edtse a Pythont a {required_python_version} verzi\u00f3ra, hogy megakad\u00e1lyozza a Home Assistant j\u00f6v\u0151beni m\u0171k\u00f6d\u00e9sk\u00e9ptelens\u00e9g\u00e9t.", "title": "A Python {current_python_version} t\u00e1mogat\u00e1sa megsz\u0171nik" } }, diff --git a/homeassistant/components/homeassistant/translations/sk.json b/homeassistant/components/homeassistant/translations/sk.json index 05810dcf8f2..7538c3a78ab 100644 --- a/homeassistant/components/homeassistant/translations/sk.json +++ b/homeassistant/components/homeassistant/translations/sk.json @@ -1,7 +1,7 @@ { "issues": { "country_not_configured": { - "description": "Nebola nakonfigurovan\u00e1 \u017eiadna krajina, aktualizujte konfigur\u00e1ciu kliknut\u00edm na tla\u010didlo \u201eviac inform\u00e1ci\u00ed\u201c ni\u017e\u0161ie.", + "description": "Nebola nakonfigurovan\u00e1 \u017eiadna krajina, aktualizujte konfigur\u00e1ciu kliknut\u00edm na tla\u010didlo \"viac inform\u00e1ci\u00ed\" ni\u017e\u0161ie.", "title": "Krajina nebola nakonfigurovan\u00e1" }, "historic_currency": { diff --git a/homeassistant/components/homeassistant/translations/sv.json b/homeassistant/components/homeassistant/translations/sv.json index 9e6855c5e87..2321ca2a248 100644 --- a/homeassistant/components/homeassistant/translations/sv.json +++ b/homeassistant/components/homeassistant/translations/sv.json @@ -1,4 +1,10 @@ { + "issues": { + "historic_currency": { + "description": "Valutan {currency} anv\u00e4nds inte l\u00e4ngre, v\u00e4nligen konfigurera om valutakonfigurationen.", + "title": "Den konfigurerade valutan anv\u00e4nds inte l\u00e4ngre" + } + }, "system_health": { "info": { "arch": "CPU-arkitektur", diff --git a/homeassistant/components/homeassistant/translations/tr.json b/homeassistant/components/homeassistant/translations/tr.json index 7ec8074702b..efa98c16e54 100644 --- a/homeassistant/components/homeassistant/translations/tr.json +++ b/homeassistant/components/homeassistant/translations/tr.json @@ -1,4 +1,18 @@ { + "issues": { + "country_not_configured": { + "description": "Hi\u00e7bir \u00fclke yap\u0131land\u0131r\u0131lmad\u0131, l\u00fctfen a\u015fa\u011f\u0131daki \"daha fazla bilgi\" d\u00fc\u011fmesine t\u0131klayarak yap\u0131land\u0131rmay\u0131 g\u00fcncelleyin.", + "title": "\u00dclke yap\u0131land\u0131r\u0131lmad\u0131" + }, + "historic_currency": { + "description": "{currency} para birimi art\u0131k kullan\u0131mda de\u011fil, l\u00fctfen para birimi yap\u0131land\u0131rmas\u0131n\u0131 yeniden yap\u0131land\u0131r\u0131n.", + "title": "Yap\u0131land\u0131r\u0131lan para birimi art\u0131k kullan\u0131mda de\u011fil" + }, + "python_version": { + "description": "Home Assistant'\u0131 \u015fu an kullan\u0131lan {current_python_version} Python s\u00fcr\u00fcm\u00fcnde \u00e7al\u0131\u015ft\u0131rma deste\u011fi kullan\u0131mdan kald\u0131r\u0131lm\u0131\u015ft\u0131r ve Home Assistant {breaks_in_ha_version} kald\u0131r\u0131lacakt\u0131r. Home Assistant \u00f6rne\u011finizin bozulmas\u0131n\u0131 \u00f6nlemek i\u00e7in l\u00fctfen Python'u {required_python_version} s\u00fcr\u00fcm\u00fcne y\u00fckseltin.", + "title": "Python {current_python_version} deste\u011fi kald\u0131r\u0131l\u0131yor" + } + }, "system_health": { "info": { "arch": "CPU Mimarisi", diff --git a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py index 921521c182a..20fdb97e384 100644 --- a/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py +++ b/homeassistant/components/homeassistant_hardware/silabs_multiprotocol_addon.py @@ -236,16 +236,7 @@ class OptionsFlowHandler(BaseMultiPanFlow, config_entries.OptionsFlow): if not is_hassio(self.hass): return self.async_abort(reason="not_hassio") - return self.async_abort( - reason="disabled_due_to_bug", - description_placeholders={ - "url": "https://developers.home-assistant.io/blog/2022/12/08/multi-pan-rollback" - }, - ) - - # pylint: disable=unreachable - - return await self.async_step_on_supervisor() # type: ignore[unreachable] + return await self.async_step_on_supervisor() async def async_step_on_supervisor( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/homeassistant_hardware/strings.json b/homeassistant/components/homeassistant_hardware/strings.json index 819ec38925f..47549794fc8 100644 --- a/homeassistant/components/homeassistant_hardware/strings.json +++ b/homeassistant/components/homeassistant_hardware/strings.json @@ -32,8 +32,7 @@ "addon_set_config_failed": "Failed to set Silicon Labs Multiprotocol configuration.", "addon_start_failed": "Failed to start the Silicon Labs Multiprotocol add-on.", "not_hassio": "The hardware options can only be configured on HassOS installations.", - "zha_migration_failed": "The ZHA migration did not succeed.", - "disabled_due_to_bug": "The hardware options are temporarily disabled while we fix a bug. [Learn more]({url})" + "zha_migration_failed": "The ZHA migration did not succeed." }, "progress": { "install_addon": "Please wait while the Silicon Labs Multiprotocol add-on installation finishes. This can take several minutes.", diff --git a/homeassistant/components/homeassistant_hardware/translations/hu.json b/homeassistant/components/homeassistant_hardware/translations/hu.json index 5479ff34a1e..0b4bc8e7720 100644 --- a/homeassistant/components/homeassistant_hardware/translations/hu.json +++ b/homeassistant/components/homeassistant_hardware/translations/hu.json @@ -14,8 +14,8 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "progress": { - "install_addon": "K\u00e9rj\u00fck, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat.", - "start_addon": "K\u00e9rj\u00fck, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny elindul. Ez eltarthat n\u00e9h\u00e1ny m\u00e1sodpercig." + "install_addon": "K\u00e9rem, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat.", + "start_addon": "K\u00e9rem, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny elindul. Ez eltarthat n\u00e9h\u00e1ny m\u00e1sodpercig." }, "step": { "addon_installed_other_device": { @@ -32,7 +32,7 @@ "title": "A Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se folyamatban van" }, "show_revert_guide": { - "description": "Ha csak Zigbee firmware-re szeretne v\u00e1ltani, k\u00e9rj\u00fck, hajtsa v\u00e9gre a k\u00f6vetkez\u0151 manu\u00e1lis l\u00e9p\u00e9seket:\n\n * T\u00e1vol\u00edtsa el a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9nyt.\n\n * Flashelje a csak Zigbee firmware-t, k\u00f6vesse a https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually oldalon tal\u00e1lhat\u00f3 \u00fatmutat\u00f3t.\n\n * Konfigur\u00e1lja \u00fajra a ZHA-t, hogy a be\u00e1ll\u00edt\u00e1sokat \u00e1tvigye az \u00fajraflashelt r\u00e1di\u00f3ra.", + "description": "Ha csak Zigbee firmware-re szeretne v\u00e1ltani, hajtsa v\u00e9gre a k\u00f6vetkez\u0151 manu\u00e1lis l\u00e9p\u00e9seket:\n\n * T\u00e1vol\u00edtsa el a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9nyt.\n\n * Flashelje a csak Zigbee firmware-t, k\u00f6vesse a https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually oldalon tal\u00e1lhat\u00f3 \u00fatmutat\u00f3t.\n\n * Konfigur\u00e1lja \u00fajra a ZHA-t, hogy a be\u00e1ll\u00edt\u00e1sokat \u00e1tvigye az \u00fajraflashelt r\u00e1di\u00f3ra.", "title": "A multiprotokoll-t\u00e1mogat\u00e1s enged\u00e9lyezve van az eszk\u00f6z\u00f6n." }, "start_addon": { diff --git a/homeassistant/components/homeassistant_hardware/translations/tr.json b/homeassistant/components/homeassistant_hardware/translations/tr.json new file mode 100644 index 00000000000..6fec842abc7 --- /dev/null +++ b/homeassistant/components/homeassistant_hardware/translations/tr.json @@ -0,0 +1,44 @@ +{ + "silabs_multiprotocol_hardware": { + "options": { + "abort": { + "addon_info_failed": "Silicon Labs Multiprotocol eklenti bilgisi al\u0131namad\u0131.", + "addon_install_failed": "Silicon Labs Multiprotocol eklentisi y\u00fcklenemedi.", + "addon_set_config_failed": "Silicon Labs \u00c7oklu protokol yap\u0131land\u0131rmas\u0131 ayarlanamad\u0131.", + "addon_start_failed": "Silicon Labs Multiprotocol eklentisi ba\u015flat\u0131lamad\u0131.", + "disabled_due_to_bug": "Biz bir hatay\u0131 d\u00fczeltirken donan\u0131m se\u00e7enekleri ge\u00e7ici olarak devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131r. [Daha fazla bilgi edinin]( {url} )", + "not_hassio": "Donan\u0131m se\u00e7enekleri yaln\u0131zca HassOS kurulumlar\u0131nda yap\u0131land\u0131r\u0131labilir.", + "zha_migration_failed": "ZHA ge\u00e7i\u015fi ba\u015far\u0131l\u0131 olmad\u0131." + }, + "error": { + "unknown": "Beklenmeyen hata" + }, + "progress": { + "install_addon": "Silicon Labs Multiprotocol eklenti kurulumu tamamlanana kadar l\u00fctfen bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir.", + "start_addon": "Silicon Labs Multiprotocol eklenti ba\u015flatma i\u015flemi tamamlanana kadar l\u00fctfen bekleyin. Bu birka\u00e7 saniye s\u00fcrebilir." + }, + "step": { + "addon_installed_other_device": { + "title": "Ba\u015fka bir cihaz i\u00e7in \u00e7oklu protokol deste\u011fi zaten etkin" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "\u00c7oklu protokol deste\u011fini etkinle\u015ftir" + }, + "description": "\u00c7oklu protokol deste\u011fi etkinle\u015ftirildi\u011finde, {hardware_name} cihaz\u0131n\u0131n IEEE 802.15.4 radyosu ayn\u0131 anda hem Zigbee hem de Thread (Matter taraf\u0131ndan kullan\u0131l\u0131r) i\u00e7in kullan\u0131labilir. Telsiz zaten ZHA Zigbee entegrasyonu taraf\u0131ndan kullan\u0131l\u0131yorsa, ZHA \u00e7oklu protokol bellenimini kullanmak \u00fczere yeniden yap\u0131land\u0131r\u0131lacakt\u0131r. \n\n Not: Bu deneysel bir \u00f6zelliktir.", + "title": "IEEE 802.15.4 radyosunda \u00e7oklu protokol deste\u011fini etkinle\u015ftirin" + }, + "install_addon": { + "title": "Silicon Labs Multiprotocol eklenti kurulumu ba\u015flad\u0131" + }, + "show_revert_guide": { + "description": "Yaln\u0131zca Zigbee \u00fcretici yaz\u0131l\u0131m\u0131na ge\u00e7mek istiyorsan\u0131z, l\u00fctfen a\u015fa\u011f\u0131daki manuel ad\u0131mlar\u0131 tamamlay\u0131n: \n\n * Silicon Labs Multiprotocol eklentisini kald\u0131r\u0131n \n\n * Yaln\u0131zca Zigbee bellenimini y\u00fckleyin, https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually adresindeki k\u0131lavuzu izleyin. \n\n * Ayarlar\u0131 yeniden yan\u0131p s\u00f6nen radyoya ta\u015f\u0131mak i\u00e7in ZHA'y\u0131 yeniden yap\u0131land\u0131r\u0131n", + "title": "Bu cihaz i\u00e7in \u00e7oklu protokol deste\u011fi etkinle\u015ftirildi" + }, + "start_addon": { + "title": "Silicon Labs Multiprotocol eklentisi ba\u015fl\u0131yor." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_sky_connect/__init__.py b/homeassistant/components/homeassistant_sky_connect/__init__.py index af6df6b519d..1de919b8c70 100644 --- a/homeassistant/components/homeassistant_sky_connect/__init__.py +++ b/homeassistant/components/homeassistant_sky_connect/__init__.py @@ -1,4 +1,4 @@ -"""The Home Assistant Sky Connect integration.""" +"""The Home Assistant SkyConnect integration.""" from __future__ import annotations import logging @@ -72,7 +72,7 @@ async def _multi_pan_addon_info( async def _async_usb_scan_done(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Finish Home Assistant Sky Connect config entry setup.""" + """Finish Home Assistant SkyConnect config entry setup.""" matcher = usb.USBCallbackMatcher( domain=DOMAIN, vid=entry.data["vid"].upper(), @@ -99,7 +99,7 @@ async def _async_usb_scan_done(hass: HomeAssistant, entry: ConfigEntry) -> None: return hw_discovery_data = { - "name": "Sky Connect Multi-PAN", + "name": "SkyConnect Multi-PAN", "port": { "path": get_zigbee_socket(hass, addon_info), }, @@ -113,7 +113,7 @@ async def _async_usb_scan_done(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up a Home Assistant Sky Connect config entry.""" + """Set up a Home Assistant SkyConnect config entry.""" await _wait_multi_pan_addon(hass, entry) diff --git a/homeassistant/components/homeassistant_sky_connect/config_flow.py b/homeassistant/components/homeassistant_sky_connect/config_flow.py index 1e4fd8701cd..7bc514d5615 100644 --- a/homeassistant/components/homeassistant_sky_connect/config_flow.py +++ b/homeassistant/components/homeassistant_sky_connect/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for the Home Assistant Sky Connect integration.""" +"""Config flow for the Home Assistant SkyConnect integration.""" from __future__ import annotations from typing import Any @@ -14,7 +14,7 @@ from .util import get_usb_service_info class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): - """Handle a config flow for Home Assistant Sky Connect.""" + """Handle a config flow for Home Assistant SkyConnect.""" VERSION = 1 @@ -38,7 +38,7 @@ class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): if await self.async_set_unique_id(unique_id): self._abort_if_unique_id_configured(updates={"device": device}) return self.async_create_entry( - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", data={ "device": device, "vid": vid, @@ -51,7 +51,7 @@ class HomeAssistantSkyConnectConfigFlow(ConfigFlow, domain=DOMAIN): class HomeAssistantSkyConnectOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandler): - """Handle an option flow for Home Assistant Sky Connect.""" + """Handle an option flow for Home Assistant SkyConnect.""" async def _async_serial_port_settings( self, @@ -75,8 +75,8 @@ class HomeAssistantSkyConnectOptionsFlow(silabs_multiprotocol_addon.OptionsFlowH def _zha_name(self) -> str: """Return the ZHA name.""" - return "Sky Connect Multi-PAN" + return "SkyConnect Multi-PAN" def _hardware_name(self) -> str: """Return the name of the hardware.""" - return "Home Assistant Sky Connect" + return "Home Assistant SkyConnect" diff --git a/homeassistant/components/homeassistant_sky_connect/const.py b/homeassistant/components/homeassistant_sky_connect/const.py index 1deb8fd4603..c504cead9cb 100644 --- a/homeassistant/components/homeassistant_sky_connect/const.py +++ b/homeassistant/components/homeassistant_sky_connect/const.py @@ -1,3 +1,3 @@ -"""Constants for the Home Assistant Sky Connect integration.""" +"""Constants for the Home Assistant SkyConnect integration.""" DOMAIN = "homeassistant_sky_connect" diff --git a/homeassistant/components/homeassistant_sky_connect/hardware.py b/homeassistant/components/homeassistant_sky_connect/hardware.py index f48e1763dd5..217a6e57543 100644 --- a/homeassistant/components/homeassistant_sky_connect/hardware.py +++ b/homeassistant/components/homeassistant_sky_connect/hardware.py @@ -1,4 +1,4 @@ -"""The Home Assistant Sky Connect hardware platform.""" +"""The Home Assistant SkyConnect hardware platform.""" from __future__ import annotations from homeassistant.components.hardware.models import HardwareInfo, USBInfo @@ -6,7 +6,7 @@ from homeassistant.core import HomeAssistant, callback from .const import DOMAIN -DONGLE_NAME = "Home Assistant Sky Connect" +DONGLE_NAME = "Home Assistant SkyConnect" @callback diff --git a/homeassistant/components/homeassistant_sky_connect/manifest.json b/homeassistant/components/homeassistant_sky_connect/manifest.json index 34bb2ad701c..58deb883aab 100644 --- a/homeassistant/components/homeassistant_sky_connect/manifest.json +++ b/homeassistant/components/homeassistant_sky_connect/manifest.json @@ -1,6 +1,6 @@ { "domain": "homeassistant_sky_connect", - "name": "Home Assistant Sky Connect", + "name": "Home Assistant SkyConnect", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homeassistant_sky_connect", "dependencies": ["hardware", "usb", "homeassistant_hardware"], diff --git a/homeassistant/components/homeassistant_sky_connect/strings.json b/homeassistant/components/homeassistant_sky_connect/strings.json index d18d2620318..970f9d97a4c 100644 --- a/homeassistant/components/homeassistant_sky_connect/strings.json +++ b/homeassistant/components/homeassistant_sky_connect/strings.json @@ -31,8 +31,7 @@ "addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]", "addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]", "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", - "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]", - "disabled_due_to_bug": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::disabled_due_to_bug%]" + "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]" }, "progress": { "install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]", diff --git a/homeassistant/components/homeassistant_sky_connect/translations/cs.json b/homeassistant/components/homeassistant_sky_connect/translations/cs.json new file mode 100644 index 00000000000..110abc55cb2 --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/translations/cs.json @@ -0,0 +1,7 @@ +{ + "options": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_sky_connect/translations/hu.json b/homeassistant/components/homeassistant_sky_connect/translations/hu.json index f02ebb7b333..46070e9e954 100644 --- a/homeassistant/components/homeassistant_sky_connect/translations/hu.json +++ b/homeassistant/components/homeassistant_sky_connect/translations/hu.json @@ -13,8 +13,8 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "progress": { - "install_addon": "K\u00e9rj\u00fck, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat.", - "start_addon": "K\u00e9rj\u00fck, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny elindul. Ez eltarthat n\u00e9h\u00e1ny m\u00e1sodpercig." + "install_addon": "K\u00e9rem, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat.", + "start_addon": "K\u00e9rem, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny elindul. Ez eltarthat n\u00e9h\u00e1ny m\u00e1sodpercig." }, "step": { "addon_installed_other_device": { @@ -31,7 +31,7 @@ "title": "A Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se folyamatban van" }, "show_revert_guide": { - "description": "Ha csak Zigbee firmware-re szeretne v\u00e1ltani, k\u00e9rj\u00fck, hajtsa v\u00e9gre a k\u00f6vetkez\u0151 manu\u00e1lis l\u00e9p\u00e9seket:\n\n * T\u00e1vol\u00edtsa el a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9nyt.\n\n * Flashelje a csak Zigbee firmware-t, k\u00f6vesse a https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually oldalon tal\u00e1lhat\u00f3 \u00fatmutat\u00f3t.\n\n * Konfigur\u00e1lja \u00fajra a ZHA-t, hogy a be\u00e1ll\u00edt\u00e1sokat \u00e1tvigye az \u00fajraflashelt r\u00e1di\u00f3ra.", + "description": "Ha csak Zigbee firmware-re szeretne v\u00e1ltani, hajtsa v\u00e9gre a k\u00f6vetkez\u0151 manu\u00e1lis l\u00e9p\u00e9seket:\n\n * T\u00e1vol\u00edtsa el a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9nyt.\n\n * Flashelje a csak Zigbee firmware-t, k\u00f6vesse a https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually oldalon tal\u00e1lhat\u00f3 \u00fatmutat\u00f3t.\n\n * Konfigur\u00e1lja \u00fajra a ZHA-t, hogy a be\u00e1ll\u00edt\u00e1sokat \u00e1tvigye az \u00fajraflashelt r\u00e1di\u00f3ra.", "title": "A multiprotokoll-t\u00e1mogat\u00e1s enged\u00e9lyezve van az eszk\u00f6z\u00f6n." }, "start_addon": { diff --git a/homeassistant/components/homeassistant_sky_connect/translations/tr.json b/homeassistant/components/homeassistant_sky_connect/translations/tr.json new file mode 100644 index 00000000000..e4a276c7c0f --- /dev/null +++ b/homeassistant/components/homeassistant_sky_connect/translations/tr.json @@ -0,0 +1,42 @@ +{ + "options": { + "abort": { + "addon_info_failed": "Silicon Labs Multiprotocol eklenti bilgisi al\u0131namad\u0131.", + "addon_install_failed": "Silicon Labs Multiprotocol eklentisi y\u00fcklenemedi.", + "addon_set_config_failed": "Silicon Labs \u00c7oklu protokol yap\u0131land\u0131rmas\u0131 ayarlanamad\u0131.", + "addon_start_failed": "Silicon Labs Multiprotocol eklentisi ba\u015flat\u0131lamad\u0131.", + "disabled_due_to_bug": "Biz bir hatay\u0131 d\u00fczeltirken donan\u0131m se\u00e7enekleri ge\u00e7ici olarak devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131r. [Daha fazla bilgi edinin]( {url} )", + "not_hassio": "Donan\u0131m se\u00e7enekleri yaln\u0131zca HassOS kurulumlar\u0131nda yap\u0131land\u0131r\u0131labilir.", + "zha_migration_failed": "ZHA ge\u00e7i\u015fi ba\u015far\u0131l\u0131 olmad\u0131." + }, + "error": { + "unknown": "Beklenmeyen hata" + }, + "progress": { + "install_addon": "Silicon Labs Multiprotocol eklenti kurulumu tamamlanana kadar l\u00fctfen bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir.", + "start_addon": "Silicon Labs Multiprotocol eklenti ba\u015flatma i\u015flemi tamamlanana kadar l\u00fctfen bekleyin. Bu birka\u00e7 saniye s\u00fcrebilir." + }, + "step": { + "addon_installed_other_device": { + "title": "Ba\u015fka bir cihaz i\u00e7in \u00e7oklu protokol deste\u011fi zaten etkin" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "\u00c7oklu protokol deste\u011fini etkinle\u015ftir" + }, + "description": "\u00c7oklu protokol deste\u011fi etkinle\u015ftirildi\u011finde, {hardware_name} cihaz\u0131n\u0131n IEEE 802.15.4 radyosu ayn\u0131 anda hem Zigbee hem de Thread (Matter taraf\u0131ndan kullan\u0131l\u0131r) i\u00e7in kullan\u0131labilir. Telsiz zaten ZHA Zigbee entegrasyonu taraf\u0131ndan kullan\u0131l\u0131yorsa, ZHA \u00e7oklu protokol bellenimini kullanmak \u00fczere yeniden yap\u0131land\u0131r\u0131lacakt\u0131r. \n\n Not: Bu deneysel bir \u00f6zelliktir.", + "title": "IEEE 802.15.4 radyosunda \u00e7oklu protokol deste\u011fini etkinle\u015ftirin" + }, + "install_addon": { + "title": "Silicon Labs Multiprotocol eklenti kurulumu ba\u015flad\u0131" + }, + "show_revert_guide": { + "description": "Yaln\u0131zca Zigbee \u00fcretici yaz\u0131l\u0131m\u0131na ge\u00e7mek istiyorsan\u0131z, l\u00fctfen a\u015fa\u011f\u0131daki manuel ad\u0131mlar\u0131 tamamlay\u0131n: \n\n * Silicon Labs Multiprotocol eklentisini kald\u0131r\u0131n \n\n * Yaln\u0131zca Zigbee bellenimini y\u00fckleyin, https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually adresindeki k\u0131lavuzu izleyin. \n\n * Ayarlar\u0131 yeniden yan\u0131p s\u00f6nen radyoya ta\u015f\u0131mak i\u00e7in ZHA'y\u0131 yeniden yap\u0131land\u0131r\u0131n", + "title": "Bu cihaz i\u00e7in \u00e7oklu protokol deste\u011fi etkinle\u015ftirildi" + }, + "start_addon": { + "title": "Silicon Labs Multiprotocol eklentisi ba\u015fl\u0131yor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant_sky_connect/util.py b/homeassistant/components/homeassistant_sky_connect/util.py index 804ce83d063..7a87964f5c4 100644 --- a/homeassistant/components/homeassistant_sky_connect/util.py +++ b/homeassistant/components/homeassistant_sky_connect/util.py @@ -1,4 +1,4 @@ -"""Utility functions for Home Assistant Sky Connect integration.""" +"""Utility functions for Home Assistant SkyConnect integration.""" from __future__ import annotations from homeassistant.components import usb diff --git a/homeassistant/components/homeassistant_yellow/strings.json b/homeassistant/components/homeassistant_yellow/strings.json index d18d2620318..970f9d97a4c 100644 --- a/homeassistant/components/homeassistant_yellow/strings.json +++ b/homeassistant/components/homeassistant_yellow/strings.json @@ -31,8 +31,7 @@ "addon_set_config_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_set_config_failed%]", "addon_start_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::addon_start_failed%]", "not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]", - "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]", - "disabled_due_to_bug": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::disabled_due_to_bug%]" + "zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]" }, "progress": { "install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]", diff --git a/homeassistant/components/homeassistant_yellow/translations/el.json b/homeassistant/components/homeassistant_yellow/translations/el.json index d806ec556fc..74343ebb583 100644 --- a/homeassistant/components/homeassistant_yellow/translations/el.json +++ b/homeassistant/components/homeassistant_yellow/translations/el.json @@ -24,7 +24,7 @@ "data": { "enable_multi_pan": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7\u03c2 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03c9\u03bd" }, - "description": "\u038c\u03c4\u03b1\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03c9\u03bd, \u03b7 \u03c1\u03b1\u03b4\u03b9\u03bf\u03c3\u03c5\u03c7\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1 IEEE 802.15.4 \u03c4\u03bf\u03c5 Home Assistant Yellow \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03c4\u03b1\u03c5\u03c4\u03cc\u03c7\u03c1\u03bf\u03bd\u03b1 \u03c4\u03cc\u03c3\u03bf \u03b3\u03b9\u03b1 \u03c4\u03bf Zigbee \u03cc\u03c3\u03bf \u03ba\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03bf Thread (\u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Matter). \u03a3\u03b7\u03bc\u03b5\u03af\u03c9\u03c3\u03b7: \u03a0\u03c1\u03cc\u03ba\u03b5\u03b9\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03bc\u03b9\u03b1 \u03c0\u03b5\u03b9\u03c1\u03b1\u03bc\u03b1\u03c4\u03b9\u03ba\u03ae \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1.", + "description": "\u038c\u03c4\u03b1\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 \u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03c9\u03bd, \u03bf \u03c0\u03bf\u03bc\u03c0\u03bf\u03b4\u03ad\u03ba\u03c4\u03b7\u03c2 IEEE 802.15.4 \u03c4\u03bf\u03c5 {hardware_name} \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03c4\u03b1\u03c5\u03c4\u03cc\u03c7\u03c1\u03bf\u03bd\u03b1 \u03ba\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03bf Zigbee \u03ba\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03bf Thread (\u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf Matter). \u0395\u03ac\u03bd \u03bf \u03c0\u03bf\u03bc\u03c0\u03bf\u03b4\u03ad\u03ba\u03c4\u03b7\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 ZHA Zigbee, \u03c4\u03bf ZHA \u03b8\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af \u03b5\u03ba \u03bd\u03ad\u03bf\u03c5 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03bf \u03c5\u03bb\u03b9\u03ba\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03c9\u03bd. \n\n \u03a3\u03b7\u03bc\u03b5\u03af\u03c9\u03c3\u03b7: \u0391\u03c5\u03c4\u03cc \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03bd\u03b1 \u03c0\u03b5\u03b9\u03c1\u03b1\u03bc\u03b1\u03c4\u03b9\u03ba\u03cc \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03b7\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc.", "title": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c5\u03c0\u03bf\u03c3\u03c4\u03ae\u03c1\u03b9\u03be\u03b7\u03c2 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ce\u03bd \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03c9\u03bd \u03c3\u03c4\u03bf\u03bd \u03b1\u03c3\u03cd\u03c1\u03bc\u03b1\u03c4\u03bf IEEE 802.15.4" }, "install_addon": { diff --git a/homeassistant/components/homeassistant_yellow/translations/hu.json b/homeassistant/components/homeassistant_yellow/translations/hu.json index f02ebb7b333..46070e9e954 100644 --- a/homeassistant/components/homeassistant_yellow/translations/hu.json +++ b/homeassistant/components/homeassistant_yellow/translations/hu.json @@ -13,8 +13,8 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "progress": { - "install_addon": "K\u00e9rj\u00fck, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat.", - "start_addon": "K\u00e9rj\u00fck, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny elindul. Ez eltarthat n\u00e9h\u00e1ny m\u00e1sodpercig." + "install_addon": "K\u00e9rem, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat.", + "start_addon": "K\u00e9rem, v\u00e1rjon, am\u00edg a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny elindul. Ez eltarthat n\u00e9h\u00e1ny m\u00e1sodpercig." }, "step": { "addon_installed_other_device": { @@ -31,7 +31,7 @@ "title": "A Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se folyamatban van" }, "show_revert_guide": { - "description": "Ha csak Zigbee firmware-re szeretne v\u00e1ltani, k\u00e9rj\u00fck, hajtsa v\u00e9gre a k\u00f6vetkez\u0151 manu\u00e1lis l\u00e9p\u00e9seket:\n\n * T\u00e1vol\u00edtsa el a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9nyt.\n\n * Flashelje a csak Zigbee firmware-t, k\u00f6vesse a https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually oldalon tal\u00e1lhat\u00f3 \u00fatmutat\u00f3t.\n\n * Konfigur\u00e1lja \u00fajra a ZHA-t, hogy a be\u00e1ll\u00edt\u00e1sokat \u00e1tvigye az \u00fajraflashelt r\u00e1di\u00f3ra.", + "description": "Ha csak Zigbee firmware-re szeretne v\u00e1ltani, hajtsa v\u00e9gre a k\u00f6vetkez\u0151 manu\u00e1lis l\u00e9p\u00e9seket:\n\n * T\u00e1vol\u00edtsa el a Silicon Labs Multiprotocol b\u0151v\u00edtm\u00e9nyt.\n\n * Flashelje a csak Zigbee firmware-t, k\u00f6vesse a https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually oldalon tal\u00e1lhat\u00f3 \u00fatmutat\u00f3t.\n\n * Konfigur\u00e1lja \u00fajra a ZHA-t, hogy a be\u00e1ll\u00edt\u00e1sokat \u00e1tvigye az \u00fajraflashelt r\u00e1di\u00f3ra.", "title": "A multiprotokoll-t\u00e1mogat\u00e1s enged\u00e9lyezve van az eszk\u00f6z\u00f6n." }, "start_addon": { diff --git a/homeassistant/components/homeassistant_yellow/translations/tr.json b/homeassistant/components/homeassistant_yellow/translations/tr.json new file mode 100644 index 00000000000..e4a276c7c0f --- /dev/null +++ b/homeassistant/components/homeassistant_yellow/translations/tr.json @@ -0,0 +1,42 @@ +{ + "options": { + "abort": { + "addon_info_failed": "Silicon Labs Multiprotocol eklenti bilgisi al\u0131namad\u0131.", + "addon_install_failed": "Silicon Labs Multiprotocol eklentisi y\u00fcklenemedi.", + "addon_set_config_failed": "Silicon Labs \u00c7oklu protokol yap\u0131land\u0131rmas\u0131 ayarlanamad\u0131.", + "addon_start_failed": "Silicon Labs Multiprotocol eklentisi ba\u015flat\u0131lamad\u0131.", + "disabled_due_to_bug": "Biz bir hatay\u0131 d\u00fczeltirken donan\u0131m se\u00e7enekleri ge\u00e7ici olarak devre d\u0131\u015f\u0131 b\u0131rak\u0131l\u0131r. [Daha fazla bilgi edinin]( {url} )", + "not_hassio": "Donan\u0131m se\u00e7enekleri yaln\u0131zca HassOS kurulumlar\u0131nda yap\u0131land\u0131r\u0131labilir.", + "zha_migration_failed": "ZHA ge\u00e7i\u015fi ba\u015far\u0131l\u0131 olmad\u0131." + }, + "error": { + "unknown": "Beklenmeyen hata" + }, + "progress": { + "install_addon": "Silicon Labs Multiprotocol eklenti kurulumu tamamlanana kadar l\u00fctfen bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir.", + "start_addon": "Silicon Labs Multiprotocol eklenti ba\u015flatma i\u015flemi tamamlanana kadar l\u00fctfen bekleyin. Bu birka\u00e7 saniye s\u00fcrebilir." + }, + "step": { + "addon_installed_other_device": { + "title": "Ba\u015fka bir cihaz i\u00e7in \u00e7oklu protokol deste\u011fi zaten etkin" + }, + "addon_not_installed": { + "data": { + "enable_multi_pan": "\u00c7oklu protokol deste\u011fini etkinle\u015ftir" + }, + "description": "\u00c7oklu protokol deste\u011fi etkinle\u015ftirildi\u011finde, {hardware_name} cihaz\u0131n\u0131n IEEE 802.15.4 radyosu ayn\u0131 anda hem Zigbee hem de Thread (Matter taraf\u0131ndan kullan\u0131l\u0131r) i\u00e7in kullan\u0131labilir. Telsiz zaten ZHA Zigbee entegrasyonu taraf\u0131ndan kullan\u0131l\u0131yorsa, ZHA \u00e7oklu protokol bellenimini kullanmak \u00fczere yeniden yap\u0131land\u0131r\u0131lacakt\u0131r. \n\n Not: Bu deneysel bir \u00f6zelliktir.", + "title": "IEEE 802.15.4 radyosunda \u00e7oklu protokol deste\u011fini etkinle\u015ftirin" + }, + "install_addon": { + "title": "Silicon Labs Multiprotocol eklenti kurulumu ba\u015flad\u0131" + }, + "show_revert_guide": { + "description": "Yaln\u0131zca Zigbee \u00fcretici yaz\u0131l\u0131m\u0131na ge\u00e7mek istiyorsan\u0131z, l\u00fctfen a\u015fa\u011f\u0131daki manuel ad\u0131mlar\u0131 tamamlay\u0131n: \n\n * Silicon Labs Multiprotocol eklentisini kald\u0131r\u0131n \n\n * Yaln\u0131zca Zigbee bellenimini y\u00fckleyin, https://github.com/NabuCasa/silabs-firmware/wiki/Flash-Silicon-Labs-radio-firmware-manually adresindeki k\u0131lavuzu izleyin. \n\n * Ayarlar\u0131 yeniden yan\u0131p s\u00f6nen radyoya ta\u015f\u0131mak i\u00e7in ZHA'y\u0131 yeniden yap\u0131land\u0131r\u0131n", + "title": "Bu cihaz i\u00e7in \u00e7oklu protokol deste\u011fi etkinle\u015ftirildi" + }, + "start_addon": { + "title": "Silicon Labs Multiprotocol eklentisi ba\u015fl\u0131yor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/el.json b/homeassistant/components/homekit/translations/el.json index 1031fcca85d..5536d239fcf 100644 --- a/homeassistant/components/homekit/translations/el.json +++ b/homeassistant/components/homekit/translations/el.json @@ -5,7 +5,7 @@ }, "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\".", + "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\u03ae\u03c3\u03c4\u03b5 \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": { diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json index 913a5d2195f..0fdc65d21a1 100644 --- a/homeassistant/components/homekit/translations/es.json +++ b/homeassistant/components/homekit/translations/es.json @@ -27,7 +27,7 @@ }, "advanced": { "data": { - "devices": "Dispositivos (Disparadores)" + "devices": "Dispositivos (Desencadenantes)" }, "description": "Se crean interruptores programables para cada dispositivo seleccionado. Cuando se dispara un dispositivo, HomeKit se puede configurar para ejecutar una automatizaci\u00f3n o una escena.", "title": "Configuraci\u00f3n avanzada" diff --git a/homeassistant/components/homekit/translations/hu.json b/homeassistant/components/homekit/translations/hu.json index eefbf2ad381..5b937706ad8 100644 --- a/homeassistant/components/homekit/translations/hu.json +++ b/homeassistant/components/homekit/translations/hu.json @@ -5,7 +5,7 @@ }, "step": { "pairing": { - "description": "A p\u00e1ros\u00edt\u00e1s befejez\u00e9s\u00e9hez k\u00f6vesse a \u201eHomeKit p\u00e1ros\u00edt\u00e1s\u201d szakasz \u201e\u00c9rtes\u00edt\u00e9sek\u201d szakasz\u00e1ban tal\u00e1lhat\u00f3 utas\u00edt\u00e1sokat.", + "description": "A p\u00e1ros\u00edt\u00e1s befejez\u00e9s\u00e9hez k\u00f6vesse a \u201eHomeKit p\u00e1ros\u00edt\u00e1s\u201d utas\u00edt\u00e1sokat az \u00e9rtes\u00edt\u00e9sekben.", "title": "HomeKit p\u00e1ros\u00edt\u00e1s" }, "user": { diff --git a/homeassistant/components/homekit/translations/sk.json b/homeassistant/components/homekit/translations/sk.json index 4c16887b508..65c8659a76e 100644 --- a/homeassistant/components/homekit/translations/sk.json +++ b/homeassistant/components/homekit/translations/sk.json @@ -5,7 +5,7 @@ }, "step": { "pairing": { - "description": "Na dokon\u010denie p\u00e1rovania postupujte pod\u013ea pokynov v \u010dasti \u201eUpozornenia\u201c v \u010dasti \u201eP\u00e1rovanie HomeKit\u201c.", + "description": "Na dokon\u010denie p\u00e1rovania postupujte pod\u013ea pokynov v \u010dasti \u201dUpozornenia\u201d v \u010dasti \u201dP\u00e1rovanie HomeKit\u201d.", "title": "P\u00e1rovanie HomeKit" }, "user": { @@ -44,14 +44,14 @@ "data": { "entities": "Entity" }, - "description": "V\u0161etky entity \u201e{domains}\u201c bud\u00fa zahrnut\u00e9 okrem vyl\u00fa\u010den\u00fdch ent\u00edt a kategorizovan\u00fdch ent\u00edt.", + "description": "V\u0161etky entity \u201d{domains}\u201d bud\u00fa zahrnut\u00e9 okrem vyl\u00fa\u010den\u00fdch ent\u00edt a kategorizovan\u00fdch ent\u00edt.", "title": "Vyberte entity, ktor\u00e9 chcete vyl\u00fa\u010di\u0165" }, "include": { "data": { "entities": "Entity" }, - "description": "V\u0161etky entity \u201e{domains}\u201c bud\u00fa zahrnut\u00e9, pokia\u013e nevyberiete konkr\u00e9tne entity.", + "description": "V\u0161etky entity \u201d{domains}\u201d bud\u00fa zahrnut\u00e9, pokia\u013e nevyberiete konkr\u00e9tne entity.", "title": "Vyberte entity, ktor\u00e9 chcete zahrn\u00fa\u0165" }, "init": { @@ -60,7 +60,7 @@ "include_exclude_mode": "Re\u017eim za\u010dlenenia", "mode": "Re\u017eim HomeKit" }, - "description": "HomeKit je mo\u017en\u00e9 nakonfigurova\u0165 tak, aby vystavil bridge alebo jedno pr\u00edslu\u0161enstvo. V re\u017eime pr\u00edslu\u0161enstva je mo\u017en\u00e9 pou\u017ei\u0165 iba jednu entitu. Pre spr\u00e1vne fungovanie prehr\u00e1va\u010dov m\u00e9di\u00ed s triedou telev\u00edznych zariaden\u00ed sa vy\u017eaduje re\u017eim pr\u00edslu\u0161enstva. Subjekty v \u010dasti \u201eDom\u00e9ny na zahrnutie\u201c bud\u00fa zahrnut\u00e9 do HomeKitu. Na nasleduj\u00facej obrazovke si budete m\u00f4c\u0165 vybra\u0165, ktor\u00e9 entity chcete zahrn\u00fa\u0165 alebo vyl\u00fa\u010di\u0165 z tohto zoznamu.", + "description": "HomeKit je mo\u017en\u00e9 nakonfigurova\u0165 tak, aby vystavil bridge alebo jedno pr\u00edslu\u0161enstvo. V re\u017eime pr\u00edslu\u0161enstva je mo\u017en\u00e9 pou\u017ei\u0165 iba jednu entitu. Pre spr\u00e1vne fungovanie prehr\u00e1va\u010dov m\u00e9di\u00ed s triedou telev\u00edznych zariaden\u00ed sa vy\u017eaduje re\u017eim pr\u00edslu\u0161enstva. Subjekty v \u010dasti \u201dDom\u00e9ny na zahrnutie\u201d bud\u00fa zahrnut\u00e9 do HomeKitu. Na nasleduj\u00facej obrazovke si budete m\u00f4c\u0165 vybra\u0165, ktor\u00e9 entity chcete zahrn\u00fa\u0165 alebo vyl\u00fa\u010di\u0165 z tohto zoznamu.", "title": "Vyberte re\u017eim a dom\u00e9ny." }, "yaml": { diff --git a/homeassistant/components/homekit/translations/tr.json b/homeassistant/components/homekit/translations/tr.json index f6ac036ff84..db7855c3de0 100644 --- a/homeassistant/components/homekit/translations/tr.json +++ b/homeassistant/components/homekit/translations/tr.json @@ -5,7 +5,7 @@ }, "step": { "pairing": { - "description": "\u201cHomeKit E\u015fle\u015ftirme\u201d alt\u0131ndaki \u201cBildirimler\u201d b\u00f6l\u00fcm\u00fcndeki talimatlar\u0131 izleyerek e\u015fle\u015ftirmeyi tamamlamak i\u00e7in.", + "description": "E\u015fle\u015ftirmeyi tamamlamak i\u00e7in \"HomeKit E\u015fle\u015ftirme\" alt\u0131ndaki \"Bildirimler\" b\u00f6l\u00fcm\u00fcndeki talimatlar\u0131 izleyin.", "title": "HomeKit'i E\u015fle\u015ftir" }, "user": { diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 24a137e4957..c34e9066160 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -188,8 +188,14 @@ class Thermostat(HomeAccessory): (CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) ) + if ( + ATTR_CURRENT_HUMIDITY in attributes + or features & ClimateEntityFeature.TARGET_HUMIDITY + ): + self.chars.append(CHAR_CURRENT_HUMIDITY) + if features & ClimateEntityFeature.TARGET_HUMIDITY: - self.chars.extend((CHAR_TARGET_HUMIDITY, CHAR_CURRENT_HUMIDITY)) + self.chars.append(CHAR_TARGET_HUMIDITY) serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars) self.set_primary_service(serv_thermostat) @@ -253,7 +259,6 @@ class Thermostat(HomeAccessory): properties={PROP_MIN_VALUE: hc_min_temp, PROP_MAX_VALUE: hc_max_temp}, ) self.char_target_humidity = None - self.char_current_humidity = None if CHAR_TARGET_HUMIDITY in self.chars: self.char_target_humidity = serv_thermostat.configure_char( CHAR_TARGET_HUMIDITY, @@ -265,6 +270,8 @@ class Thermostat(HomeAccessory): # of 0-80% properties={PROP_MIN_VALUE: min_humidity}, ) + self.char_current_humidity = None + if CHAR_CURRENT_HUMIDITY in self.chars: self.char_current_humidity = serv_thermostat.configure_char( CHAR_CURRENT_HUMIDITY, value=50 ) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index aa343b045ce..9edcba5bf72 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -6,7 +6,7 @@ "requirements": ["aiohomekit==2.4.4"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }], - "dependencies": ["bluetooth", "zeroconf"], + "dependencies": ["bluetooth_adapters", "zeroconf"], "codeowners": ["@Jc2k", "@bdraco"], "iot_class": "local_push", "loggers": ["aiohomekit", "commentjson"] diff --git a/homeassistant/components/homekit_controller/translations/el.json b/homeassistant/components/homekit_controller/translations/el.json index 424b0253019..9c7af309a2b 100644 --- a/homeassistant/components/homekit_controller/translations/el.json +++ b/homeassistant/components/homekit_controller/translations/el.json @@ -14,11 +14,11 @@ "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.", + "pairing_failed": "\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ad\u03bd\u03b1 \u03bc\u03b7 \u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\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\u03bd \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \u0391\u03c5\u03c4\u03cc \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\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 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03bc\u03b7\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: {error}", "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}", + "flow_title": "{name} ({category})", "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.", @@ -33,7 +33,7 @@ "allow_insecure_setup_codes": "\u039d\u03b1 \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03c4\u03b1\u03b9 \u03b7 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd\u03c2 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2.", "pairing_code": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2" }, - "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.", + "description": "\u03a4\u03bf HomeKit Controller \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf {name} ( {category} ) \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\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b6\u03b5\u03cd\u03be\u03b7\u03c2 \u03c4\u03bf\u03c5 HomeKit (\u03c3\u03b5 \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": { diff --git a/homeassistant/components/homekit_controller/translations/nl.json b/homeassistant/components/homekit_controller/translations/nl.json index 403e997e52d..33747ad17b5 100644 --- a/homeassistant/components/homekit_controller/translations/nl.json +++ b/homeassistant/components/homekit_controller/translations/nl.json @@ -73,7 +73,21 @@ "select": { "ecobee_mode": { "state": { - "home": "Thuis" + "home": "Thuis", + "sleep": "Slaap" + } + } + }, + "sensor": { + "thread_node_capabilities": { + "state": { + "none": "Geen" + } + }, + "thread_status": { + "state": { + "detached": "Losgekoppeld", + "disabled": "Uitgeschakeld" } } } diff --git a/homeassistant/components/homekit_controller/translations/sensor.nl.json b/homeassistant/components/homekit_controller/translations/sensor.nl.json index be137558599..7e1a6b452a2 100644 --- a/homeassistant/components/homekit_controller/translations/sensor.nl.json +++ b/homeassistant/components/homekit_controller/translations/sensor.nl.json @@ -1,11 +1,15 @@ { "state": { "homekit_controller__thread_node_capabilities": { + "full": "Volledig eindapparaat", + "minimal": "Minimaal eindapparaat", "none": "Geen" }, "homekit_controller__thread_status": { + "child": "Kind", "detached": "Ontkoppeld", "disabled": "Uitgeschakeld", + "leader": "Leider", "router": "Router" } } diff --git a/homeassistant/components/homekit_controller/translations/tr.json b/homeassistant/components/homekit_controller/translations/tr.json index 3086e3e34fb..c60ae70cd81 100644 --- a/homeassistant/components/homekit_controller/translations/tr.json +++ b/homeassistant/components/homekit_controller/translations/tr.json @@ -14,7 +14,7 @@ "authentication_error": "Yanl\u0131\u015f HomeKit kodu. L\u00fctfen kontrol edip tekrar deneyin.", "insecure_setup_code": "\u0130stenen kurulum kodu, \u00f6nemsiz do\u011fas\u0131 nedeniyle g\u00fcvenli de\u011fil. Bu aksesuar, temel g\u00fcvenlik gereksinimlerini kar\u015f\u0131lam\u0131yor.", "max_peers_error": "Cihaz, \u00fccretsiz e\u015fle\u015ftirme depolama alan\u0131 olmad\u0131\u011f\u0131 i\u00e7in e\u015fle\u015ftirme eklemeyi reddetti.", - "pairing_failed": "Bu cihazla e\u015fle\u015fmeye \u00e7al\u0131\u015f\u0131l\u0131rken i\u015flenmeyen bir hata olu\u015ftu. Bu ge\u00e7ici bir hata olabilir veya cihaz\u0131n\u0131z \u015fu anda desteklenmiyor olabilir.", + "pairing_failed": "Bu cihazla e\u015fle\u015ftirilmeye \u00e7al\u0131\u015f\u0131l\u0131rken i\u015flenmeyen bir hata olu\u015ftu. Bu ge\u00e7ici bir ar\u0131za olabilir veya cihaz\u0131n\u0131z \u015fu anda desteklenmiyor olabilir: {error}", "unable_to_pair": "E\u015fle\u015ftirilemiyor, l\u00fctfen tekrar deneyin.", "unknown_error": "Cihaz bilinmeyen bir hata bildirdi. E\u015fle\u015ftirme ba\u015far\u0131s\u0131z oldu." }, @@ -69,5 +69,39 @@ "single_press": "\" {subtype} \" bas\u0131ld\u0131" } }, + "entity": { + "select": { + "ecobee_mode": { + "state": { + "away": "D\u0131\u015far\u0131da", + "home": "Evde", + "sleep": "Uyku" + } + } + }, + "sensor": { + "thread_node_capabilities": { + "state": { + "border_router_capable": "S\u0131n\u0131r Y\u00f6nlendirici \u00d6zelli\u011fi", + "full": "Tam Son Cihaz", + "minimal": "Minimal Son Cihaz", + "none": "Hi\u00e7biri", + "router_eligible": "Y\u00f6nlendiriciye Uygun Son Cihaz", + "sleepy": "Uykudaki Son Cihaz" + } + }, + "thread_status": { + "state": { + "border_router": "S\u0131n\u0131r Y\u00f6nlendirici", + "child": "\u00c7ocuk", + "detached": "Tarafs\u0131z", + "disabled": "Devre d\u0131\u015f\u0131", + "joining": "Kat\u0131l\u0131yor", + "leader": "Lider", + "router": "Y\u00f6nlendirici" + } + } + } + }, "title": "HomeKit Denetleyicisi" } \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 7ba30372451..cc4414c9069 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -107,7 +107,9 @@ class HomematicipHAP: "Connected to HomematicIP with HAP %s", self.config_entry.unique_id ) - self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) + await self.hass.config_entries.async_forward_entry_setups( + self.config_entry, PLATFORMS + ) return True diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 073d54fcf1e..cf1f8afe67a 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "requirements": ["homematicip==1.0.13"], "codeowners": [], - "quality_scale": "platinum", + "quality_scale": "silver", "iot_class": "cloud_push", "loggers": ["homematicip"] } diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index b4eb89f06c8..04fba2cfd92 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -177,10 +177,10 @@ class HomematicipHeatingThermostat(HomematicipGenericEntity, SensorEntity): return "mdi:radiator" @property - def native_value(self) -> int: + def native_value(self) -> int | None: """Return the state of the radiator valve.""" if self._device.valveState != ValveState.ADAPTION_DONE: - return self._device.valveState + return None return round(self._device.valvePosition * 100) diff --git a/homeassistant/components/homematicip_cloud/translations/hu.json b/homeassistant/components/homematicip_cloud/translations/hu.json index 975daa36126..544d9b61058 100644 --- a/homeassistant/components/homematicip_cloud/translations/hu.json +++ b/homeassistant/components/homematicip_cloud/translations/hu.json @@ -21,7 +21,7 @@ "title": "V\u00e1lasszon HomematicIP hozz\u00e1f\u00e9r\u00e9si pontot" }, "link": { - "description": "A HomematicIP regisztr\u00e1l\u00e1s\u00e1hoz a Home Assistant alkalmaz\u00e1sban nyomja meg a hozz\u00e1f\u00e9r\u00e9si pont k\u00e9k gombj\u00e1t \u00e9s a bek\u00fcld\u00e9s gombot. \n\n ! [A gomb helye a h\u00eddon] (/ static / images / config_flows / config_homematicip_cloud.png)", + "description": "A HomematicIP regisztr\u00e1l\u00e1s\u00e1hoz a Home Assistant alkalmaz\u00e1sban nyomja meg a hozz\u00e1f\u00e9r\u00e9si pont k\u00e9k gombj\u00e1t \u00e9s a bek\u00fcld\u00e9s gombot. \n\n![Gomb elhelyez\u00e9se](/static/images/config_flows/config_homematicip_cloud.png)", "title": "Link Hozz\u00e1f\u00e9r\u00e9si pont" } } diff --git a/homeassistant/components/homematicip_cloud/translations/id.json b/homeassistant/components/homematicip_cloud/translations/id.json index 26679d3b37f..0d54ab29725 100644 --- a/homeassistant/components/homematicip_cloud/translations/id.json +++ b/homeassistant/components/homematicip_cloud/translations/id.json @@ -9,7 +9,7 @@ "invalid_sgtin_or_pin": "SGTIN atau Kode PIN tidak valid, coba lagi.", "press_the_button": "Tekan tombol biru.", "register_failed": "Gagal mendaftar, coba lagi.", - "timeout_button": "Tenggang waktu penekanan tombol biru berakhir, coba lagi." + "timeout_button": "Tenggang waktu penekanan tombol biru habis, coba lagi." }, "step": { "init": { diff --git a/homeassistant/components/homematicip_cloud/translations/lt.json b/homeassistant/components/homematicip_cloud/translations/lt.json index a270a8acbc2..f5a6d1385c5 100644 --- a/homeassistant/components/homematicip_cloud/translations/lt.json +++ b/homeassistant/components/homematicip_cloud/translations/lt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u012erenginys jau sukonfig\u016bruotas" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/homematicip_cloud/translations/lv.json b/homeassistant/components/homematicip_cloud/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homewizard/__init__.py b/homeassistant/components/homewizard/__init__.py index df58ccccd30..01705d66f50 100644 --- a/homeassistant/components/homewizard/__init__.py +++ b/homeassistant/components/homewizard/__init__.py @@ -1,75 +1,20 @@ """The Homewizard integration.""" -import logging - -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import device_registry as dr, entity_registry as er from .const import DOMAIN, PLATFORMS from .coordinator import HWEnergyDeviceUpdateCoordinator as Coordinator -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Homewizard from a config entry.""" - - _LOGGER.debug("__init__ async_setup_entry") - - # Migrate `homewizard_energy` (custom_component) to `homewizard` - if entry.source == SOURCE_IMPORT and "old_config_entry_id" in entry.data: - # Remove the old config entry ID from the entry data so we don't try this again - # on the next setup - data = entry.data.copy() - old_config_entry_id = data.pop("old_config_entry_id") - - hass.config_entries.async_update_entry(entry, data=data) - _LOGGER.debug( - ( - "Setting up imported homewizard_energy entry %s for the first time as " - "homewizard entry %s" - ), - old_config_entry_id, - entry.entry_id, - ) - - ent_reg = er.async_get(hass) - for entity in er.async_entries_for_config_entry(ent_reg, old_config_entry_id): - _LOGGER.debug("Removing %s", entity.entity_id) - ent_reg.async_remove(entity.entity_id) - - _LOGGER.debug("Re-creating %s for the new config entry", entity.entity_id) - # We will precreate the entity so that any customizations can be preserved - new_entity = ent_reg.async_get_or_create( - entity.domain, - DOMAIN, - entity.unique_id, - suggested_object_id=entity.entity_id.split(".")[1], - disabled_by=entity.disabled_by, - config_entry=entry, - original_name=entity.original_name, - original_icon=entity.original_icon, - ) - _LOGGER.debug("Re-created %s", new_entity.entity_id) - - # If there are customizations on the old entity, apply them to the new one - if entity.name or entity.icon: - ent_reg.async_update_entity( - new_entity.entity_id, name=entity.name, icon=entity.icon - ) - - # Remove the old config entry and now the entry is fully migrated - hass.async_create_task(hass.config_entries.async_remove(old_config_entry_id)) - - # Create coordinator - coordinator = Coordinator(hass, entry.entry_id, entry.data[CONF_IP_ADDRESS]) + coordinator = Coordinator(hass, entry, entry.data[CONF_IP_ADDRESS]) try: await coordinator.async_config_entry_first_refresh() except ConfigEntryNotReady: - await coordinator.api.close() if coordinator.api_disabled: @@ -77,26 +22,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + # Abort reauth config flow if active for progress_flow in hass.config_entries.flow.async_progress_by_handler(DOMAIN): - if progress_flow["context"].get("source") == SOURCE_REAUTH: + if ( + "context" in progress_flow + and progress_flow["context"].get("source") == SOURCE_REAUTH + ): hass.config_entries.flow.async_abort(progress_flow["flow_id"]) - # Setup entry - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = coordinator - - # Register device - device_registry = dr.async_get(hass) - device_registry.async_get_or_create( - config_entry_id=entry.entry_id, - name=entry.title, - manufacturer="HomeWizard", - sw_version=coordinator.data["device"].firmware_version, - model=coordinator.data["device"].product_type, - identifiers={(DOMAIN, coordinator.data["device"].serial)}, - ) - # Finalize await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -105,12 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - _LOGGER.debug("__init__ async_unload_entry") - - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - - if unload_ok: + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): coordinator = hass.data[DOMAIN].pop(entry.entry_id) await coordinator.api.close() - return unload_ok diff --git a/homeassistant/components/homewizard/button.py b/homeassistant/components/homewizard/button.py index 4bcc5016dec..00fd3c38ef9 100644 --- a/homeassistant/components/homewizard/button.py +++ b/homeassistant/components/homewizard/button.py @@ -1,18 +1,14 @@ """Support for HomeWizard buttons.""" - -import logging - from homeassistant.components.button import ButtonEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN from .coordinator import HWEnergyDeviceUpdateCoordinator - -_LOGGER = logging.getLogger(__name__) +from .entity import HomeWizardEntity +from .helpers import homewizard_exception_handler async def async_setup_entry( @@ -20,18 +16,16 @@ async def async_setup_entry( ) -> None: """Set up the Identify button.""" coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - - features = await coordinator.api.features() - if features.has_identify: + if coordinator.data.features.has_identify: async_add_entities([HomeWizardIdentifyButton(coordinator, entry)]) -class HomeWizardIdentifyButton( - CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], ButtonEntity -): +class HomeWizardIdentifyButton(HomeWizardEntity, ButtonEntity): """Representation of a identify button.""" - _attr_has_entity_name = True + _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_icon = "mdi:magnify" + _attr_name = "Identify" def __init__( self, @@ -41,17 +35,8 @@ class HomeWizardIdentifyButton( """Initialize button.""" super().__init__(coordinator) self._attr_unique_id = f"{entry.unique_id}_identify" - self._attr_device_info = { - "name": entry.title, - "manufacturer": "HomeWizard", - "sw_version": coordinator.data["device"].firmware_version, - "model": coordinator.data["device"].product_type, - "identifiers": {(DOMAIN, coordinator.data["device"].serial)}, - } - self._attr_name = "Identify" - self._attr_icon = "mdi:magnify" - self._attr_entity_category = EntityCategory.DIAGNOSTIC + @homewizard_exception_handler async def async_press(self) -> None: """Identify the device.""" await self.coordinator.api.identify() diff --git a/homeassistant/components/homewizard/config_flow.py b/homeassistant/components/homewizard/config_flow.py index bdd84c0d07e..82c808a0f13 100644 --- a/homeassistant/components/homewizard/config_flow.py +++ b/homeassistant/components/homewizard/config_flow.py @@ -1,18 +1,20 @@ -"""Config flow for Homewizard.""" +"""Config flow for HomeWizard.""" from __future__ import annotations from collections.abc import Mapping import logging -from typing import Any, cast +from typing import Any, NamedTuple from homewizard_energy import HomeWizardEnergy from homewizard_energy.errors import DisabledError, RequestError, UnsupportedError +from homewizard_energy.models import Device from voluptuous import Required, Schema -from homeassistant import config_entries -from homeassistant.components import persistent_notification, zeroconf +from homeassistant.components import onboarding, zeroconf +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_IP_ADDRESS from homeassistant.data_entry_flow import AbortFlow, FlowResult +from homeassistant.exceptions import HomeAssistantError from .const import ( CONF_API_ENABLED, @@ -26,96 +28,58 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class DiscoveryData(NamedTuple): + """User metadata.""" + + ip: str + product_name: str + product_type: str + serial: str + + +class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for P1 meter.""" VERSION = 1 - def __init__(self) -> None: - """Initialize the HomeWizard config flow.""" - self.config: dict[str, str | int] = {} - self.entry: config_entries.ConfigEntry | None = None - - 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") - - 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." - ), - notification_id=f"homewizard_energy_to_{DOMAIN}", - ) - - return await self.async_step_user({CONF_IP_ADDRESS: import_config["host"]}) + discovery: DiscoveryData + entry: ConfigEntry | None async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initiated by the user.""" + errors: dict[str, str] | None = None + if user_input is not None: + try: + device_info = await self._async_try_connect(user_input[CONF_IP_ADDRESS]) + except RecoverableError as ex: + _LOGGER.error(ex) + errors = {"base": ex.error_code} + else: + await self.async_set_unique_id( + f"{device_info.product_type}_{device_info.serial}" + ) + self._abort_if_unique_id_configured(updates=user_input) + return self.async_create_entry( + title=f"{device_info.product_name} ({device_info.serial})", + data=user_input, + ) - _LOGGER.debug("config_flow async_step_user") - - data_schema = Schema( - { - Required(CONF_IP_ADDRESS): str, - } - ) - - if user_input is None: - return self.async_show_form( - step_id="user", - data_schema=data_schema, - errors=None, - ) - - error = await self._async_try_connect(user_input[CONF_IP_ADDRESS]) - if error is not None: - return self.async_show_form( - step_id="user", - data_schema=data_schema, - errors={"base": error}, - ) - - # Fetch device information - api = HomeWizardEnergy(user_input[CONF_IP_ADDRESS]) - device_info = await api.device() - await api.close() - - # Sets unique ID and aborts if it is already exists - await self._async_set_and_check_unique_id( - { - CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS], - CONF_PRODUCT_TYPE: device_info.product_type, - CONF_SERIAL: device_info.serial, - } - ) - - data: dict[str, str] = {CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS]} - - if self.source == config_entries.SOURCE_IMPORT: - old_config_entry_id = self.context["old_config_entry_id"] - assert self.hass.config_entries.async_get_entry(old_config_entry_id) - data["old_config_entry_id"] = old_config_entry_id - - # Add entry - return self.async_create_entry( - title=f"{device_info.product_name} ({device_info.serial})", - data=data, + return self.async_show_form( + step_id="user", + data_schema=Schema( + { + Required(CONF_IP_ADDRESS): str, + } + ), + errors=errors, ) async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" - - _LOGGER.debug("config_flow async_step_zeroconf") - - # Validate doscovery entry if ( CONF_API_ENABLED not in discovery_info.properties or CONF_PATH not in discovery_info.properties @@ -128,64 +92,56 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if (discovery_info.properties[CONF_PATH]) != "/api/v1": return self.async_abort(reason="unsupported_api_version") - # 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[CONF_PRODUCT_TYPE], - CONF_SERIAL: discovery_info.properties[CONF_SERIAL], - } + self.discovery = DiscoveryData( + ip=discovery_info.host, + product_type=discovery_info.properties[CONF_PRODUCT_TYPE], + product_name=discovery_info.properties[CONF_PRODUCT_NAME], + serial=discovery_info.properties[CONF_SERIAL], + ) + + await self.async_set_unique_id( + f"{self.discovery.product_type}_{self.discovery.serial}" + ) + self._abort_if_unique_id_configured( + updates={CONF_IP_ADDRESS: 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: 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() async def async_step_discovery_confirm( self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm discovery.""" - if user_input is not None: - - # Check connection - error = await self._async_try_connect(str(self.config[CONF_IP_ADDRESS])) - if error is not None: - return self.async_show_form( - step_id="discovery_confirm", - errors={"base": error}, + errors: dict[str, str] | None = None + if user_input is not None or not onboarding.async_is_onboarded(self.hass): + try: + await self._async_try_connect(self.discovery.ip) + except RecoverableError as ex: + _LOGGER.error(ex) + errors = {"base": ex.error_code} + else: + return self.async_create_entry( + title=f"{self.discovery.product_name} ({self.discovery.serial})", + data={CONF_IP_ADDRESS: self.discovery.ip}, ) - return self.async_create_entry( - title=f"{self.config[CONF_PRODUCT_NAME]} ({self.config[CONF_SERIAL]})", - data={ - CONF_IP_ADDRESS: self.config[CONF_IP_ADDRESS], - }, - ) - self._set_confirm_only() - self.context["title_placeholders"] = { - "name": f"{self.config[CONF_PRODUCT_NAME]} ({self.config[CONF_SERIAL]})" + "name": f"{self.discovery.product_name} ({self.discovery.serial})" } return self.async_show_form( step_id="discovery_confirm", description_placeholders={ - CONF_PRODUCT_TYPE: cast(str, self.config[CONF_PRODUCT_TYPE]), - CONF_SERIAL: cast(str, self.config[CONF_SERIAL]), - CONF_IP_ADDRESS: cast(str, self.config[CONF_IP_ADDRESS]), + CONF_PRODUCT_TYPE: self.discovery.product_type, + CONF_SERIAL: self.discovery.serial, + CONF_IP_ADDRESS: self.discovery.ip, }, + errors=errors, ) async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Handle re-auth if API was disabled.""" - self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) return await self.async_step_reauth_confirm() @@ -193,48 +149,47 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Confirm reauth dialog.""" - + errors: dict[str, str] | None = None if user_input is not None: assert self.entry is not None - - error = await self._async_try_connect(self.entry.data[CONF_IP_ADDRESS]) - if error is not None: - return self.async_show_form( - step_id="reauth_confirm", - errors={"base": error}, - ) - - await self.hass.config_entries.async_reload(self.entry.entry_id) - return self.async_abort(reason="reauth_successful") + try: + await self._async_try_connect(self.entry.data[CONF_IP_ADDRESS]) + except RecoverableError as ex: + _LOGGER.error(ex) + errors = {"base": ex.error_code} + else: + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") return self.async_show_form( step_id="reauth_confirm", + errors=errors, ) @staticmethod - async def _async_try_connect(ip_address: str) -> str | None: - """Try to connect.""" + async def _async_try_connect(ip_address: str) -> Device: + """Try to connect. - _LOGGER.debug("config_flow _async_try_connect") - - # Make connection with device - # This is to test the connection and to get info for unique_id + Make connection with device to test the connection + and to get info for unique_id. + """ energy_api = HomeWizardEnergy(ip_address) - try: - await energy_api.device() + return await energy_api.device() - except DisabledError: - _LOGGER.error("API disabled, API must be enabled in the app") - return "api_not_enabled" + except DisabledError as ex: + raise RecoverableError( + "API disabled, API must be enabled in the app", "api_not_enabled" + ) from ex except UnsupportedError as ex: _LOGGER.error("API version unsuppored") raise AbortFlow("unsupported_api_version") from ex except RequestError as ex: - _LOGGER.exception(ex) - return "network_error" + raise RecoverableError( + "Device unreachable or unexpected response", "network_error" + ) from ex except Exception as ex: _LOGGER.exception(ex) @@ -243,16 +198,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): finally: await energy_api.close() - return None - async def _async_set_and_check_unique_id(self, entry_info: dict[str, Any]) -> None: - """Validate if entry exists.""" +class RecoverableError(HomeAssistantError): + """Raised when a connection has been failed but can be retried.""" - _LOGGER.debug("config_flow _async_set_and_check_unique_id") - - await self.async_set_unique_id( - f"{entry_info[CONF_PRODUCT_TYPE]}_{entry_info[CONF_SERIAL]}" - ) - self._abort_if_unique_id_configured( - updates={CONF_IP_ADDRESS: entry_info[CONF_IP_ADDRESS]} - ) + def __init__(self, message: str, error_code: str) -> None: + """Init RecoverableError.""" + super().__init__(message) + self.error_code = error_code diff --git a/homeassistant/components/homewizard/const.py b/homeassistant/components/homewizard/const.py index e5ceb5ab23d..34c83626f86 100644 --- a/homeassistant/components/homewizard/const.py +++ b/homeassistant/components/homewizard/const.py @@ -1,10 +1,10 @@ """Constants for the Homewizard integration.""" from __future__ import annotations +from dataclasses import dataclass from datetime import timedelta -from typing import TypedDict -# Set up. +from homewizard_energy.features import Features from homewizard_energy.models import Data, Device, State, System from homeassistant.const import Platform @@ -24,10 +24,12 @@ CONF_SERIAL = "serial" UPDATE_INTERVAL = timedelta(seconds=5) -class DeviceResponseEntry(TypedDict): +@dataclass +class DeviceResponseEntry: """Dict describing a single response entry.""" device: Device data: Data - state: State - system: System + features: Features + state: State | None + system: System | None = None diff --git a/homeassistant/components/homewizard/coordinator.py b/homeassistant/components/homewizard/coordinator.py index d8c20a6cc92..2da618eeb27 100644 --- a/homeassistant/components/homewizard/coordinator.py +++ b/homeassistant/components/homewizard/coordinator.py @@ -1,23 +1,20 @@ """Update coordinator for HomeWizard.""" from __future__ import annotations -from datetime import timedelta import logging from homewizard_energy import HomeWizardEnergy from homewizard_energy.errors import DisabledError, RequestError +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, UPDATE_INTERVAL, DeviceResponseEntry _LOGGER = logging.getLogger(__name__) -MAX_UPDATE_INTERVAL = timedelta(minutes=30) - class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry]): """Gather data for the energy device.""" @@ -28,37 +25,26 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry] def __init__( self, hass: HomeAssistant, - entry_id: str, + entry: ConfigEntry, host: str, ) -> None: - """Initialize Update Coordinator.""" - + """Initialize update coordinator.""" super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) - self.entry_id = entry_id + self.entry = entry self.api = HomeWizardEnergy(host, clientsession=async_get_clientsession(hass)) - @property - def device_info(self) -> DeviceInfo: - """Return device_info.""" - return DeviceInfo( - identifiers={(DOMAIN, self.data["device"].serial)}, - ) - async def _async_update_data(self) -> DeviceResponseEntry: """Fetch all device and sensor data from api.""" - - # Update all properties try: - data: DeviceResponseEntry = { - "device": await self.api.device(), - "data": await self.api.data(), - "state": await self.api.state(), - "system": None, - } + data = DeviceResponseEntry( + device=await self.api.device(), + data=await self.api.data(), + features=await self.api.features(), + state=await self.api.state(), + ) - features = await self.api.features() - if features.has_system: - data["system"] = await self.api.system() + if data.features.has_system: + data.system = await self.api.system() except RequestError as ex: raise UpdateFailed(ex) from ex @@ -69,11 +55,10 @@ class HWEnergyDeviceUpdateCoordinator(DataUpdateCoordinator[DeviceResponseEntry] # Do not reload when performing first refresh if self.data is not None: - await self.hass.config_entries.async_reload(self.entry_id) + await self.hass.config_entries.async_reload(self.entry.entry_id) raise UpdateFailed(ex) from ex - else: - self.api_disabled = False + self.api_disabled = False return data diff --git a/homeassistant/components/homewizard/diagnostics.py b/homeassistant/components/homewizard/diagnostics.py index a0c852cf4b6..a8f89b67ce9 100644 --- a/homeassistant/components/homewizard/diagnostics.py +++ b/homeassistant/components/homewizard/diagnostics.py @@ -12,7 +12,14 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN from .coordinator import HWEnergyDeviceUpdateCoordinator -TO_REDACT = {CONF_IP_ADDRESS, "serial", "wifi_ssid"} +TO_REDACT = { + CONF_IP_ADDRESS, + "serial", + "wifi_ssid", + "unique_meter_id", + "unique_id", + "gas_unique_id", +} async def async_get_config_entry_diagnostics( @@ -22,13 +29,13 @@ async def async_get_config_entry_diagnostics( coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] meter_data = { - "device": asdict(coordinator.data["device"]), - "data": asdict(coordinator.data["data"]), - "state": asdict(coordinator.data["state"]) - if coordinator.data["state"] is not None + "device": asdict(coordinator.data.device), + "data": asdict(coordinator.data.data), + "state": asdict(coordinator.data.state) + if coordinator.data.state is not None else None, - "system": asdict(coordinator.data["system"]) - if coordinator.data["system"] is not None + "system": asdict(coordinator.data.system) + if coordinator.data.system is not None else None, } diff --git a/homeassistant/components/homewizard/entity.py b/homeassistant/components/homewizard/entity.py new file mode 100644 index 00000000000..2aa1b0369d9 --- /dev/null +++ b/homeassistant/components/homewizard/entity.py @@ -0,0 +1,30 @@ +"""Base entity for the HomeWizard integration.""" +from __future__ import annotations + +from homeassistant.const import ATTR_IDENTIFIERS +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import HWEnergyDeviceUpdateCoordinator + + +class HomeWizardEntity(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator]): + """Defines a HomeWizard entity.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: HWEnergyDeviceUpdateCoordinator) -> None: + """Initialize the HomeWizard entity.""" + super().__init__(coordinator=coordinator) + self._attr_device_info = DeviceInfo( + name=coordinator.entry.title, + manufacturer="HomeWizard", + sw_version=coordinator.data.device.firmware_version, + model=coordinator.data.device.product_type, + ) + + if coordinator.data.device.serial is not None: + self._attr_device_info[ATTR_IDENTIFIERS] = { + (DOMAIN, coordinator.data.device.serial) + } diff --git a/homeassistant/components/homewizard/helpers.py b/homeassistant/components/homewizard/helpers.py new file mode 100644 index 00000000000..d2d1b7c0119 --- /dev/null +++ b/homeassistant/components/homewizard/helpers.py @@ -0,0 +1,38 @@ +"""Helpers for HomeWizard.""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from typing import Any, Concatenate, ParamSpec, TypeVar + +from homewizard_energy.errors import DisabledError, RequestError + +from homeassistant.exceptions import HomeAssistantError + +from .entity import HomeWizardEntity + +_HomeWizardEntityT = TypeVar("_HomeWizardEntityT", bound=HomeWizardEntity) +_P = ParamSpec("_P") + + +def homewizard_exception_handler( + func: Callable[Concatenate[_HomeWizardEntityT, _P], Coroutine[Any, Any, Any]] +) -> Callable[Concatenate[_HomeWizardEntityT, _P], Coroutine[Any, Any, None]]: + """Decorate HomeWizard Energy calls to handle HomeWizardEnergy exceptions. + + A decorator that wraps the passed in function, catches HomeWizardEnergy errors, + and reloads the integration when the API was disabled so the reauth flow is + triggered. + """ + + async def handler( + self: _HomeWizardEntityT, *args: _P.args, **kwargs: _P.kwargs + ) -> None: + try: + await func(self, *args, **kwargs) + except RequestError as ex: + raise HomeAssistantError from ex + except DisabledError as ex: + await self.hass.config_entries.async_reload(self.coordinator.entry.entry_id) + raise HomeAssistantError from ex + + return handler diff --git a/homeassistant/components/homewizard/manifest.json b/homeassistant/components/homewizard/manifest.json index baec844cc26..4aa1316d489 100644 --- a/homeassistant/components/homewizard/manifest.json +++ b/homeassistant/components/homewizard/manifest.json @@ -4,9 +4,10 @@ "documentation": "https://www.home-assistant.io/integrations/homewizard", "codeowners": ["@DCSBL"], "dependencies": [], - "requirements": ["python-homewizard-energy==1.3.1"], + "requirements": ["python-homewizard-energy==1.8.0"], "zeroconf": ["_hwenergy._tcp.local."], "config_flow": true, + "quality_scale": "platinum", "iot_class": "local_polling", "loggers": ["homewizard_energy"] } diff --git a/homeassistant/components/homewizard/number.py b/homeassistant/components/homewizard/number.py index 783841168ed..605d0371386 100644 --- a/homeassistant/components/homewizard/number.py +++ b/homeassistant/components/homewizard/number.py @@ -1,18 +1,17 @@ """Creates HomeWizard Number entities.""" from __future__ import annotations -from typing import Optional, cast - from homeassistant.components.number import NumberEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN from .coordinator import HWEnergyDeviceUpdateCoordinator +from .entity import HomeWizardEntity +from .helpers import homewizard_exception_handler async def async_setup_entry( @@ -22,22 +21,17 @@ async def async_setup_entry( ) -> None: """Set up numbers for device.""" coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - - if coordinator.data["state"]: - async_add_entities( - [ - HWEnergyNumberEntity(coordinator, entry), - ] - ) + if coordinator.data.state: + async_add_entities([HWEnergyNumberEntity(coordinator, entry)]) -class HWEnergyNumberEntity( - CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], NumberEntity -): +class HWEnergyNumberEntity(HomeWizardEntity, NumberEntity): """Representation of status light number.""" _attr_entity_category = EntityCategory.CONFIG - _attr_has_entity_name = True + _attr_icon = "mdi:lightbulb-on" + _attr_name = "Status light brightness" + _attr_native_unit_of_measurement = PERCENTAGE def __init__( self, @@ -47,20 +41,19 @@ class HWEnergyNumberEntity( """Initialize the control number.""" super().__init__(coordinator) self._attr_unique_id = f"{entry.unique_id}_status_light_brightness" - self._attr_name = "Status light brightness" - self._attr_native_unit_of_measurement = PERCENTAGE - self._attr_icon = "mdi:lightbulb-on" - self._attr_device_info = coordinator.device_info + @homewizard_exception_handler async def async_set_native_value(self, value: float) -> None: """Set a new value.""" - await self.coordinator.api.state_set(brightness=value * (255 / 100)) + await self.coordinator.api.state_set(brightness=int(value * (255 / 100))) await self.coordinator.async_refresh() @property def native_value(self) -> float | None: """Return the current value.""" - brightness = cast(Optional[float], self.coordinator.data["state"].brightness) - if brightness is None: + if ( + self.coordinator.data.state is None + or self.coordinator.data.state.brightness is None + ): return None - return round(brightness * (100 / 255)) + return round(self.coordinator.data.state.brightness * (100 / 255)) diff --git a/homeassistant/components/homewizard/sensor.py b/homeassistant/components/homewizard/sensor.py index edd6a40a7ef..10a75a580b4 100644 --- a/homeassistant/components/homewizard/sensor.py +++ b/homeassistant/components/homewizard/sensor.py @@ -1,7 +1,11 @@ -"""Creates Homewizard sensor entities.""" +"""Creates HomeWizard sensor entities.""" from __future__ import annotations -from typing import Final, cast +from collections.abc import Callable +from dataclasses import dataclass +from typing import Final + +from homewizard_energy.models import Data from homeassistant.components.sensor import ( SensorDeviceClass, @@ -10,38 +14,80 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, UnitOfEnergy, UnitOfPower, UnitOfVolume +from homeassistant.const import ( + PERCENTAGE, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfFrequency, + UnitOfPower, + UnitOfVolume, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, DeviceResponseEntry +from .const import DOMAIN from .coordinator import HWEnergyDeviceUpdateCoordinator +from .entity import HomeWizardEntity PARALLEL_UPDATES = 1 -SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( - SensorEntityDescription( + +@dataclass +class HomeWizardEntityDescriptionMixin: + """Mixin values for HomeWizard entities.""" + + value_fn: Callable[[Data], float | int | str | None] + + +@dataclass +class HomeWizardSensorEntityDescription( + SensorEntityDescription, HomeWizardEntityDescriptionMixin +): + """Class describing HomeWizard sensor entities.""" + + +SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = ( + HomeWizardSensorEntityDescription( key="smr_version", name="DSMR version", icon="mdi:counter", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.smr_version, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="meter_model", name="Smart meter model", icon="mdi:gauge", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.meter_model, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( + key="unique_meter_id", + name="Smart meter identifier", + icon="mdi:alphabetical-variant", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.unique_meter_id, + ), + HomeWizardSensorEntityDescription( key="wifi_ssid", name="Wi-Fi SSID", icon="mdi:wifi", entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.wifi_ssid, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( + key="active_tariff", + name="Active tariff", + icon="mdi:calendar-clock", + value_fn=lambda data: ( + None if data.active_tariff is None else str(data.active_tariff) + ), + device_class=SensorDeviceClass.ENUM, + options=["1", "2", "3", "4"], + ), + HomeWizardSensorEntityDescription( key="wifi_strength", name="Wi-Fi strength", icon="mdi:wifi", @@ -49,84 +95,284 @@ SENSORS: Final[tuple[SensorEntityDescription, ...]] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, + value_fn=lambda data: data.wifi_strength, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( + key="total_power_import_kwh", + name="Total power import", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_import_kwh, + ), + HomeWizardSensorEntityDescription( key="total_power_import_t1_kwh", name="Total power import T1", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_import_t1_kwh, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="total_power_import_t2_kwh", name="Total power import T2", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_import_t2_kwh, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( + key="total_power_import_t3_kwh", + name="Total power import T3", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_import_t3_kwh, + ), + HomeWizardSensorEntityDescription( + key="total_power_import_t4_kwh", + name="Total power import T4", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_import_t4_kwh, + ), + HomeWizardSensorEntityDescription( + key="total_power_export_kwh", + name="Total power export", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_export_kwh, + ), + HomeWizardSensorEntityDescription( key="total_power_export_t1_kwh", name="Total power export T1", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_export_t1_kwh, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="total_power_export_t2_kwh", name="Total power export T2", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_export_t2_kwh, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( + key="total_power_export_t3_kwh", + name="Total power export T3", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_export_t3_kwh, + ), + HomeWizardSensorEntityDescription( + key="total_power_export_t4_kwh", + name="Total power export T4", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_power_export_t4_kwh, + ), + HomeWizardSensorEntityDescription( key="active_power_w", name="Active power", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.active_power_w, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="active_power_l1_w", name="Active power L1", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.active_power_l1_w, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="active_power_l2_w", name="Active power L2", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.active_power_l2_w, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="active_power_l3_w", name="Active power L3", native_unit_of_measurement=UnitOfPower.WATT, device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.active_power_l3_w, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( + key="active_voltage_l1_v", + name="Active voltage L1", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + value_fn=lambda data: data.active_voltage_l1_v, + ), + HomeWizardSensorEntityDescription( + key="active_voltage_l2_v", + name="Active voltage L2", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + value_fn=lambda data: data.active_voltage_l2_v, + ), + HomeWizardSensorEntityDescription( + key="active_voltage_l3_v", + name="Active voltage L3", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + value_fn=lambda data: data.active_voltage_l3_v, + ), + HomeWizardSensorEntityDescription( + key="active_current_l1_a", + name="Active current L1", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + value_fn=lambda data: data.active_current_l1_a, + ), + HomeWizardSensorEntityDescription( + key="active_current_l2_a", + name="Active current L2", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + value_fn=lambda data: data.active_current_l2_a, + ), + HomeWizardSensorEntityDescription( + key="active_current_l3_a", + name="Active current L3", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + value_fn=lambda data: data.active_current_l3_a, + ), + HomeWizardSensorEntityDescription( + key="active_frequency_hz", + name="Active frequency", + native_unit_of_measurement=UnitOfFrequency.HERTZ, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + value_fn=lambda data: data.active_frequency_hz, + ), + HomeWizardSensorEntityDescription( + key="voltage_sag_l1_count", + name="Voltage sags detected L1", + icon="mdi:alert", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.voltage_sag_l1_count, + ), + HomeWizardSensorEntityDescription( + key="voltage_sag_l2_count", + name="Voltage sags detected L2", + icon="mdi:alert", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.voltage_sag_l2_count, + ), + HomeWizardSensorEntityDescription( + key="voltage_sag_l3_count", + name="Voltage sags detected L3", + icon="mdi:alert", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.voltage_sag_l3_count, + ), + HomeWizardSensorEntityDescription( + key="voltage_swell_l1_count", + name="Voltage swells detected L1", + icon="mdi:alert", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.voltage_swell_l1_count, + ), + HomeWizardSensorEntityDescription( + key="voltage_swell_l2_count", + name="Voltage swells detected L2", + icon="mdi:alert", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.voltage_swell_l2_count, + ), + HomeWizardSensorEntityDescription( + key="voltage_swell_l3_count", + name="Voltage swells detected L3", + icon="mdi:alert", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.voltage_swell_l3_count, + ), + HomeWizardSensorEntityDescription( + key="any_power_fail_count", + name="Power failures detected", + icon="mdi:transmission-tower-off", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.any_power_fail_count, + ), + HomeWizardSensorEntityDescription( + key="long_power_fail_count", + name="Long power failures detected", + icon="mdi:transmission-tower-off", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.long_power_fail_count, + ), + HomeWizardSensorEntityDescription( + key="active_power_average_w", + name="Active average demand", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + value_fn=lambda data: data.active_power_average_w, + ), + HomeWizardSensorEntityDescription( + key="monthly_power_peak_w", + name="Peak demand current month", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + value_fn=lambda data: data.monthly_power_peak_w, + ), + HomeWizardSensorEntityDescription( key="total_gas_m3", name="Total gas", native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, device_class=SensorDeviceClass.GAS, state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_gas_m3, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( + key="gas_unique_id", + name="Gas meter identifier", + icon="mdi:alphabetical-variant", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.gas_unique_id, + ), + HomeWizardSensorEntityDescription( key="active_liter_lpm", name="Active water usage", native_unit_of_measurement="l/min", icon="mdi:water", state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda data: data.active_liter_lpm, ), - SensorEntityDescription( + HomeWizardSensorEntityDescription( key="total_liter_m3", name="Total water usage", native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, icon="mdi:gauge", device_class=SensorDeviceClass.WATER, state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda data: data.total_liter_m3, ), ) @@ -137,54 +383,48 @@ async def async_setup_entry( """Initialize sensors.""" coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - entities = [] - if coordinator.data["data"] is not None: - for description in SENSORS: - if getattr(coordinator.data["data"], description.key) is not None: - entities.append(HWEnergySensor(coordinator, entry, description)) - async_add_entities(entities) + async_add_entities( + HomeWizardSensorEntity(coordinator, entry, description) + for description in SENSORS + if description.value_fn(coordinator.data.data) is not None + ) -class HWEnergySensor(CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SensorEntity): +class HomeWizardSensorEntity(HomeWizardEntity, SensorEntity): """Representation of a HomeWizard Sensor.""" - _attr_has_entity_name = True + entity_description: HomeWizardSensorEntityDescription def __init__( self, coordinator: HWEnergyDeviceUpdateCoordinator, entry: ConfigEntry, - description: SensorEntityDescription, + description: HomeWizardSensorEntityDescription, ) -> None: """Initialize Sensor Domain.""" - super().__init__(coordinator) self.entity_description = description - self.entry = entry - - # Config attributes. - self.data_type = description.key self._attr_unique_id = f"{entry.unique_id}_{description.key}" - self._attr_device_info = coordinator.device_info - # Special case for export, not everyone has solarpanels + # Special case for export, not everyone has solar panels # The chance that 'export' is non-zero when you have solar panels is nil - if self.data_type in [ - "total_power_export_t1_kwh", - "total_power_export_t2_kwh", - ]: - if self.native_value == 0: - self._attr_entity_registry_enabled_default = False + if ( + description.key + in [ + "total_power_export_kwh", + "total_power_export_t1_kwh", + "total_power_export_t2_kwh", + "total_power_export_t3_kwh", + "total_power_export_t4_kwh", + ] + and self.native_value == 0 + ): + self._attr_entity_registry_enabled_default = False @property - def data(self) -> DeviceResponseEntry: - """Return data object from DataUpdateCoordinator.""" - return self.coordinator.data - - @property - def native_value(self) -> StateType: - """Return state of meter.""" - return cast(StateType, getattr(self.data["data"], self.data_type)) + def native_value(self) -> float | int | str | None: + """Return the sensor value.""" + return self.entity_description.value_fn(self.coordinator.data.data) @property def available(self) -> bool: diff --git a/homeassistant/components/homewizard/switch.py b/homeassistant/components/homewizard/switch.py index 3b255d195b1..498beb7ebe4 100644 --- a/homeassistant/components/homewizard/switch.py +++ b/homeassistant/components/homewizard/switch.py @@ -1,17 +1,79 @@ -"""Creates Homewizard Energy switch entities.""" +"""Creates HomeWizard Energy switch entities.""" from __future__ import annotations +from collections.abc import Awaitable, Callable +from dataclasses import dataclass from typing import Any -from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity +from homewizard_energy import HomeWizardEnergy + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN +from .const import DOMAIN, DeviceResponseEntry from .coordinator import HWEnergyDeviceUpdateCoordinator +from .entity import HomeWizardEntity +from .helpers import homewizard_exception_handler + + +@dataclass +class HomeWizardEntityDescriptionMixin: + """Mixin values for HomeWizard entities.""" + + create_fn: Callable[[DeviceResponseEntry], bool] + available_fn: Callable[[DeviceResponseEntry], bool] + is_on_fn: Callable[[DeviceResponseEntry], bool | None] + set_fn: Callable[[HomeWizardEnergy, bool], Awaitable[Any]] + + +@dataclass +class HomeWizardSwitchEntityDescription( + SwitchEntityDescription, HomeWizardEntityDescriptionMixin +): + """Class describing HomeWizard switch entities.""" + + icon_off: str | None = None + + +SWITCHES = [ + HomeWizardSwitchEntityDescription( + key="power_on", + device_class=SwitchDeviceClass.OUTLET, + create_fn=lambda data: data.state is not None, + available_fn=lambda data: data.state is not None and not data.state.switch_lock, + is_on_fn=lambda data: data.state.power_on if data.state else None, + set_fn=lambda api, active: api.state_set(power_on=active), + ), + HomeWizardSwitchEntityDescription( + key="switch_lock", + name="Switch lock", + entity_category=EntityCategory.CONFIG, + icon="mdi:lock", + icon_off="mdi:lock-open", + create_fn=lambda data: data.state is not None, + available_fn=lambda data: data.state is not None, + is_on_fn=lambda data: data.state.switch_lock if data.state else None, + set_fn=lambda api, active: api.state_set(switch_lock=active), + ), + HomeWizardSwitchEntityDescription( + key="cloud_connection", + name="Cloud connection", + entity_category=EntityCategory.CONFIG, + icon="mdi:cloud", + icon_off="mdi:cloud-off-outline", + create_fn=lambda data: data.system is not None, + available_fn=lambda data: data.system is not None, + is_on_fn=lambda data: data.system.cloud_enabled if data.system else None, + set_fn=lambda api, active: api.system_set(cloud_enabled=active), + ), +] async def async_setup_entry( @@ -22,146 +84,60 @@ async def async_setup_entry( """Set up switches.""" coordinator: HWEnergyDeviceUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - entities: list[SwitchEntity] = [] - - if coordinator.data["state"]: - entities.append(HWEnergyMainSwitchEntity(coordinator, entry)) - entities.append(HWEnergySwitchLockEntity(coordinator, entry)) - - if coordinator.data["system"]: - entities.append(HWEnergyEnableCloudEntity(hass, coordinator, entry)) - - async_add_entities(entities) + async_add_entities( + HomeWizardSwitchEntity( + coordinator=coordinator, + description=description, + entry=entry, + ) + for description in SWITCHES + if description.available_fn(coordinator.data) + ) -class HWEnergySwitchEntity( - CoordinatorEntity[HWEnergyDeviceUpdateCoordinator], SwitchEntity -): - """Representation switchable entity.""" +class HomeWizardSwitchEntity(HomeWizardEntity, SwitchEntity): + """Representation of a HomeWizard switch.""" - _attr_has_entity_name = True + entity_description: HomeWizardSwitchEntityDescription def __init__( self, coordinator: HWEnergyDeviceUpdateCoordinator, + description: HomeWizardSwitchEntityDescription, entry: ConfigEntry, - key: str, ) -> None: """Initialize the switch.""" super().__init__(coordinator) - self._attr_unique_id = f"{entry.unique_id}_{key}" - self._attr_device_info = coordinator.device_info - - -class HWEnergyMainSwitchEntity(HWEnergySwitchEntity): - """Representation of the main power switch.""" - - _attr_device_class = SwitchDeviceClass.OUTLET - - def __init__( - self, coordinator: HWEnergyDeviceUpdateCoordinator, entry: ConfigEntry - ) -> None: - """Initialize the switch.""" - super().__init__(coordinator, entry, "power_on") - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the switch on.""" - await self.coordinator.api.state_set(power_on=True) - await self.coordinator.async_refresh() - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the switch off.""" - await self.coordinator.api.state_set(power_on=False) - await self.coordinator.async_refresh() - - @property - def available(self) -> bool: - """ - Return availability of power_on. - - This switch becomes unavailable when switch_lock is enabled. - """ - return super().available and not self.coordinator.data["state"].switch_lock - - @property - def is_on(self) -> bool: - """Return true if switch is on.""" - return bool(self.coordinator.data["state"].power_on) - - -class HWEnergySwitchLockEntity(HWEnergySwitchEntity): - """ - Representation of the switch-lock configuration. - - Switch-lock is a feature that forces the relay in 'on' state. - It disables any method that can turn of the relay. - """ - - _attr_name = "Switch lock" - _attr_device_class = SwitchDeviceClass.SWITCH - _attr_entity_category = EntityCategory.CONFIG - - def __init__( - self, coordinator: HWEnergyDeviceUpdateCoordinator, entry: ConfigEntry - ) -> None: - """Initialize the switch.""" - super().__init__(coordinator, entry, "switch_lock") - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn switch-lock on.""" - await self.coordinator.api.state_set(switch_lock=True) - await self.coordinator.async_refresh() - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn switch-lock off.""" - await self.coordinator.api.state_set(switch_lock=False) - await self.coordinator.async_refresh() - - @property - def is_on(self) -> bool: - """Return true if switch is on.""" - return bool(self.coordinator.data["state"].switch_lock) - - -class HWEnergyEnableCloudEntity(HWEnergySwitchEntity): - """ - Representation of the enable cloud configuration. - - Turning off 'cloud connection' turns off all communication to HomeWizard Cloud. - At this point, the device is fully local. - """ - - _attr_name = "Cloud connection" - _attr_device_class = SwitchDeviceClass.SWITCH - _attr_entity_category = EntityCategory.CONFIG - - def __init__( - self, - hass: HomeAssistant, - coordinator: HWEnergyDeviceUpdateCoordinator, - entry: ConfigEntry, - ) -> None: - """Initialize the switch.""" - super().__init__(coordinator, entry, "cloud_connection") - self.hass = hass - self.entry = entry - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn cloud connection on.""" - await self.coordinator.api.system_set(cloud_enabled=True) - await self.coordinator.async_refresh() - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn cloud connection off.""" - await self.coordinator.api.system_set(cloud_enabled=False) - await self.coordinator.async_refresh() + self.entity_description = description + self._attr_unique_id = f"{entry.unique_id}_{description.key}" @property def icon(self) -> str | None: """Return the icon.""" - return "mdi:cloud" if self.is_on else "mdi:cloud-off-outline" + if self.entity_description.icon_off and self.is_on is False: + return self.entity_description.icon_off + return super().icon @property - def is_on(self) -> bool: - """Return true if cloud connection is active.""" - return bool(self.coordinator.data["system"].cloud_enabled) + def available(self) -> bool: + """Return if entity is available.""" + return super().available and self.entity_description.available_fn( + self.coordinator.data + ) + + @property + def is_on(self) -> bool | None: + """Return state of the switch.""" + return self.entity_description.is_on_fn(self.coordinator.data) + + @homewizard_exception_handler + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + await self.entity_description.set_fn(self.coordinator.api, True) + await self.coordinator.async_refresh() + + @homewizard_exception_handler + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + await self.entity_description.set_fn(self.coordinator.api, False) + await self.coordinator.async_refresh() diff --git a/homeassistant/components/homewizard/translations/ca.json b/homeassistant/components/homewizard/translations/ca.json index 2cf2933a77b..e9b89928827 100644 --- a/homeassistant/components/homewizard/translations/ca.json +++ b/homeassistant/components/homewizard/translations/ca.json @@ -2,15 +2,14 @@ "config": { "abort": { "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": "Versi\u00f3 d'API no compatible detectada", "reauth_successful": "S'ha activat l'API correctament", "unknown_error": "Error inesperat" }, "error": { - "api_not_enabled": "L'API no est\u00e0 activada. Activa-la a la configuraci\u00f3 de l'aplicaci\u00f3 HomeWizard Energy", - "network_error": "Dispositiu inaccessible, assegura't que has introdu\u00eft l'adre\u00e7a IP correcta i que el dispositiu est\u00e0 disponible a la xarxa" + "api_not_enabled": "L'API no est\u00e0 activada. Activeu-la a la configuraci\u00f3 de l'aplicaci\u00f3 HomeWizard Energy", + "network_error": "El dispositiu \u00e9s inaccessible, assegureu-vos que heu introdu\u00eft l'adre\u00e7a IP correcta i que el dispositiu est\u00e0 disponible a la xarxa" }, "step": { "discovery_confirm": { @@ -18,7 +17,7 @@ "title": "Confirmaci\u00f3" }, "reauth_confirm": { - "description": "L'API local no est\u00e0 activada. V\u00e9s l'aplicaci\u00f3 HomeWizard Energy i activa l'API a la configuraci\u00f3 de dispositiu." + "description": "L'API local no est\u00e0 activada. Aneu a l'aplicaci\u00f3 HomeWizard Energy i activeu l'API a la configuraci\u00f3 de dispositiu." }, "user": { "data": { diff --git a/homeassistant/components/homewizard/translations/de.json b/homeassistant/components/homewizard/translations/de.json index 56065e8e498..48255d48db4 100644 --- a/homeassistant/components/homewizard/translations/de.json +++ b/homeassistant/components/homewizard/translations/de.json @@ -2,7 +2,6 @@ "config": { "abort": { "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": "Nicht unterst\u00fctzte API-Version erkannt", "reauth_successful": "Das Aktivieren der API war erfolgreich", diff --git a/homeassistant/components/homewizard/translations/el.json b/homeassistant/components/homewizard/translations/el.json index f546a0974f4..14178fc095f 100644 --- a/homeassistant/components/homewizard/translations/el.json +++ b/homeassistant/components/homewizard/translations/el.json @@ -2,7 +2,6 @@ "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": "\u0395\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bc\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b7 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 API", "reauth_successful": "\u0397 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 API \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", diff --git a/homeassistant/components/homewizard/translations/en.json b/homeassistant/components/homewizard/translations/en.json index 58645b03249..aee19d49a09 100644 --- a/homeassistant/components/homewizard/translations/en.json +++ b/homeassistant/components/homewizard/translations/en.json @@ -2,7 +2,6 @@ "config": { "abort": { "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": "Detected unsupported API version", "reauth_successful": "Enabling API was successful", diff --git a/homeassistant/components/homewizard/translations/es.json b/homeassistant/components/homewizard/translations/es.json index 98d12d7612b..1957b462705 100644 --- a/homeassistant/components/homewizard/translations/es.json +++ b/homeassistant/components/homewizard/translations/es.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "api_not_enabled": "La API no est\u00e1 habilitada. Habilita la API en la aplicaci\u00f3n HomeWizard Energy dentro de Configuraci\u00f3n", "device_not_supported": "Este dispositivo no es compatible", "invalid_discovery_parameters": "Se ha detectado una versi\u00f3n de API no compatible", "reauth_successful": "La API se ha habilitado correctamente", diff --git a/homeassistant/components/homewizard/translations/et.json b/homeassistant/components/homewizard/translations/et.json index 1db845301d2..9a2a0e5e58b 100644 --- a/homeassistant/components/homewizard/translations/et.json +++ b/homeassistant/components/homewizard/translations/et.json @@ -2,7 +2,6 @@ "config": { "abort": { "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": "Leiti toetuseta API versioon", "reauth_successful": "API lubamine \u00f5nnestus", diff --git a/homeassistant/components/homewizard/translations/fr.json b/homeassistant/components/homewizard/translations/fr.json index f935f830ac9..c03ba01a219 100644 --- a/homeassistant/components/homewizard/translations/fr.json +++ b/homeassistant/components/homewizard/translations/fr.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", - "api_not_enabled": "L'API n'est pas activ\u00e9e. Activez l'API dans les param\u00e8tres de l'application HomeWizard Energy", "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" diff --git a/homeassistant/components/homewizard/translations/hu.json b/homeassistant/components/homewizard/translations/hu.json index d6fa0b87e9f..03ef79ec299 100644 --- a/homeassistant/components/homewizard/translations/hu.json +++ b/homeassistant/components/homewizard/translations/hu.json @@ -2,7 +2,6 @@ "config": { "abort": { "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 \u00e9szlel\u00e9se", "reauth_successful": "Az API enged\u00e9lyez\u00e9se sikeres volt", diff --git a/homeassistant/components/homewizard/translations/id.json b/homeassistant/components/homewizard/translations/id.json index 4227c6f425b..8798b5581e2 100644 --- a/homeassistant/components/homewizard/translations/id.json +++ b/homeassistant/components/homewizard/translations/id.json @@ -2,7 +2,6 @@ "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", "reauth_successful": "Pengaktifkan API berhasil", diff --git a/homeassistant/components/homewizard/translations/it.json b/homeassistant/components/homewizard/translations/it.json index f840ac84341..54be336bc14 100644 --- a/homeassistant/components/homewizard/translations/it.json +++ b/homeassistant/components/homewizard/translations/it.json @@ -2,7 +2,6 @@ "config": { "abort": { "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": "Rilevata versione API non supportata", "reauth_successful": "L'abilitazione dell'API \u00e8 riuscita", diff --git a/homeassistant/components/homewizard/translations/ja.json b/homeassistant/components/homewizard/translations/ja.json index f7e1e33ee5f..37b81e181e7 100644 --- a/homeassistant/components/homewizard/translations/ja.json +++ b/homeassistant/components/homewizard/translations/ja.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "\u30c7\u30d0\u30a4\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059", - "api_not_enabled": "API\u304c\u6709\u52b9\u306b\u306a\u3063\u3066\u3044\u307e\u305b\u3093\u3002HomeWizard Energy App\u306esettings\u3067API\u3092\u6709\u52b9\u306b\u3057\u3066\u304f\u3060\u3055\u3044\u3002", "device_not_supported": "\u3053\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093", "invalid_discovery_parameters": "\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044 API \u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u691c\u51fa\u3055\u308c\u307e\u3057\u305f", "unknown_error": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" diff --git a/homeassistant/components/homewizard/translations/lv.json b/homeassistant/components/homewizard/translations/lv.json index 2f9c5d4ac20..5b1690d42a9 100644 --- a/homeassistant/components/homewizard/translations/lv.json +++ b/homeassistant/components/homewizard/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "step": { "discovery_confirm": { "title": "Apstiprin\u0101t" diff --git a/homeassistant/components/homewizard/translations/nl.json b/homeassistant/components/homewizard/translations/nl.json index bda434b4439..d5e215b4700 100644 --- a/homeassistant/components/homewizard/translations/nl.json +++ b/homeassistant/components/homewizard/translations/nl.json @@ -2,16 +2,23 @@ "config": { "abort": { "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": "Niet-ondersteunde API-versie gedetecteerd", + "reauth_successful": "De API is succesvol ingeschakeld", "unknown_error": "Onverwachte fout" }, + "error": { + "api_not_enabled": "De API is niet ingeschakeld. Schakel de API in via de HomeWizard Energy app via de instellingen.", + "network_error": "Apparaat is niet bereikbaar, controleer dat het juiste IP-adres is gebruikt en dat het apparaat bereikbaar is in het netwerk" + }, "step": { "discovery_confirm": { "description": "Wilt u {product_type} ({serial}) op {ip_address} instellen?", "title": "Bevestig" }, + "reauth_confirm": { + "description": "De lokale API is niet ingeschakeld. Ga naar de HomeWizard Energy app en schakel de API in via apparaatinstellingen" + }, "user": { "data": { "ip_address": "IP-adres" diff --git a/homeassistant/components/homewizard/translations/no.json b/homeassistant/components/homewizard/translations/no.json index b158cd72596..60e44d29df6 100644 --- a/homeassistant/components/homewizard/translations/no.json +++ b/homeassistant/components/homewizard/translations/no.json @@ -2,7 +2,6 @@ "config": { "abort": { "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": "Oppdaget API-versjon som ikke st\u00f8ttes", "reauth_successful": "Aktivering av API var vellykket", diff --git a/homeassistant/components/homewizard/translations/pl.json b/homeassistant/components/homewizard/translations/pl.json index 0a75fb88321..fa2c997035a 100644 --- a/homeassistant/components/homewizard/translations/pl.json +++ b/homeassistant/components/homewizard/translations/pl.json @@ -2,7 +2,6 @@ "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 ustawieniach aplikacji HomeWizard Energy.", "device_not_supported": "To urz\u0105dzenie nie jest obs\u0142ugiwane", "invalid_discovery_parameters": "Wykryto nieobs\u0142ugiwan\u0105 wersj\u0119 API", "reauth_successful": "W\u0142\u0105czenie interfejsu API powiod\u0142o si\u0119", diff --git a/homeassistant/components/homewizard/translations/pt-BR.json b/homeassistant/components/homewizard/translations/pt-BR.json index cfd067475cf..7404e32dc56 100644 --- a/homeassistant/components/homewizard/translations/pt-BR.json +++ b/homeassistant/components/homewizard/translations/pt-BR.json @@ -2,7 +2,6 @@ "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", "reauth_successful": "A ativa\u00e7\u00e3o da API foi bem-sucedida", diff --git a/homeassistant/components/homewizard/translations/ru.json b/homeassistant/components/homewizard/translations/ru.json index 0b5438d4d35..eb26894a901 100644 --- a/homeassistant/components/homewizard/translations/ru.json +++ b/homeassistant/components/homewizard/translations/ru.json @@ -2,7 +2,6 @@ "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.", - "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.", "reauth_successful": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 API \u043f\u0440\u043e\u0448\u043b\u043e \u0443\u0441\u043f\u0435\u0448\u043d\u043e", diff --git a/homeassistant/components/homewizard/translations/sk.json b/homeassistant/components/homewizard/translations/sk.json index 9db3eac1ee0..594a3a5c76b 100644 --- a/homeassistant/components/homewizard/translations/sk.json +++ b/homeassistant/components/homewizard/translations/sk.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", - "api_not_enabled": "Rozhranie API nie je povolen\u00e9. Povo\u013ete API v aplik\u00e1cii HomeWizard Energy v nastaveniach", "device_not_supported": "Toto zariadenie nie je podporovan\u00e9", "invalid_discovery_parameters": "Zisten\u00e1 nepodporovan\u00e1 verzia API", "reauth_successful": "Povolenie API bolo \u00faspe\u0161n\u00e9", diff --git a/homeassistant/components/homewizard/translations/sv.json b/homeassistant/components/homewizard/translations/sv.json index 554f347eee1..b3aaf036753 100644 --- a/homeassistant/components/homewizard/translations/sv.json +++ b/homeassistant/components/homewizard/translations/sv.json @@ -2,7 +2,6 @@ "config": { "abort": { "already_configured": "Enheten \u00e4r redan konfigurerad", - "api_not_enabled": "API:et \u00e4r inte aktiverat. Aktivera API i HomeWizard Energy App under inst\u00e4llningar", "device_not_supported": "Den h\u00e4r enheten st\u00f6ds inte", "invalid_discovery_parameters": "Uppt\u00e4ckte en API-version som inte st\u00f6ds", "unknown_error": "Ov\u00e4ntat fel" diff --git a/homeassistant/components/homewizard/translations/tr.json b/homeassistant/components/homewizard/translations/tr.json index 3ed1eaf6488..d2662671ed0 100644 --- a/homeassistant/components/homewizard/translations/tr.json +++ b/homeassistant/components/homewizard/translations/tr.json @@ -2,16 +2,23 @@ "config": { "abort": { "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": "Desteklenmeyen API s\u00fcr\u00fcm\u00fc alg\u0131land\u0131", + "reauth_successful": "API'yi etkinle\u015ftirme ba\u015far\u0131l\u0131 oldu", "unknown_error": "Beklenmeyen hata" }, + "error": { + "api_not_enabled": "API etkin de\u011fil. Ayarlar alt\u0131nda HomeWizard Energy Uygulamas\u0131nda API'yi etkinle\u015ftirin", + "network_error": "Cihaza eri\u015filemiyor, do\u011fru IP adresini girdi\u011finizden ve cihaz\u0131n a\u011f\u0131n\u0131zda mevcut oldu\u011fundan emin olun." + }, "step": { "discovery_confirm": { - "description": "{ip_address} konumuna {product_type} ({serial}) kurmak istiyor musunuz?", + "description": "{product_type} ( {serial} ) {ip_address} adresinde kurmak istiyor musunuz?", "title": "Onayla" }, + "reauth_confirm": { + "description": "Yerel API devre d\u0131\u015f\u0131 b\u0131rak\u0131ld\u0131. HomeWizard Energy uygulamas\u0131na gidin ve cihaz ayarlar\u0131nda API'yi etkinle\u015ftirin." + }, "user": { "data": { "ip_address": "IP Adresi" diff --git a/homeassistant/components/homewizard/translations/uk.json b/homeassistant/components/homewizard/translations/uk.json new file mode 100644 index 00000000000..c3c307dc7ba --- /dev/null +++ b/homeassistant/components/homewizard/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u044f API \u043f\u0440\u043e\u0439\u0448\u043b\u043e \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homewizard/translations/zh-Hant.json b/homeassistant/components/homewizard/translations/zh-Hant.json index d66f4d66cb8..e0f66a97937 100644 --- a/homeassistant/components/homewizard/translations/zh-Hant.json +++ b/homeassistant/components/homewizard/translations/zh-Hant.json @@ -2,7 +2,6 @@ "config": { "abort": { "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": "\u5075\u6e2c\u5230\u4e0d\u652f\u63f4 API \u7248\u672c", "reauth_successful": "\u555f\u7528 API \u6210\u529f", diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index 99f682fd7a4..3316d2852e7 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -1,14 +1,14 @@ """Support for Honeywell (US) Total Connect Comfort climate systems.""" import asyncio -from datetime import timedelta +from dataclasses import dataclass -import somecomfort +import AIOSomecomfort from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.util import Throttle +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( _LOGGER, @@ -20,7 +20,6 @@ from .const import ( ) UPDATE_LOOP_SLEEP_TIME = 5 -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=300) PLATFORMS = [Platform.CLIMATE, Platform.SENSOR] MIGRATE_OPTIONS_KEYS = {CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE} @@ -51,18 +50,33 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b username = config_entry.data[CONF_USERNAME] password = config_entry.data[CONF_PASSWORD] - client = await hass.async_add_executor_job( - get_somecomfort_client, username, password + client = AIOSomecomfort.AIOSomeComfort( + username, password, session=async_get_clientsession(hass) ) + try: + await client.login() + await client.discover() - if client is None: - return False + except AIOSomecomfort.AuthError as ex: + raise ConfigEntryNotReady( + "Failed to initialize the Honeywell client: " + "Check your configuration (username, password), " + ) from ex + + except ( + AIOSomecomfort.ConnectionError, + AIOSomecomfort.ConnectionTimeout, + asyncio.TimeoutError, + ) as ex: + raise ConfigEntryNotReady( + "Failed to initialize the Honeywell client: " + "Connection error: maybe you have exceeded the API rate limit?" + ) from ex loc_id = config_entry.data.get(CONF_LOC_ID) dev_id = config_entry.data.get(CONF_DEV_ID) devices = {} - for location in client.locations_by_id.values(): if not loc_id or location.locationid == loc_id: for device in location.devices_by_id.values(): @@ -73,8 +87,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b _LOGGER.debug("No devices found") return False - data = HoneywellData(hass, config_entry, client, username, password, devices) - await data.async_update() + data = HoneywellData(config_entry.entry_id, client, devices) hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][config_entry.entry_id] = data await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) @@ -99,108 +112,10 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok -def get_somecomfort_client(username: str, password: str) -> somecomfort.SomeComfort: - """Initialize the somecomfort client.""" - try: - return somecomfort.SomeComfort(username, password) - except somecomfort.AuthError: - _LOGGER.error("Failed to login to honeywell account %s", username) - return None - except somecomfort.SomeComfortError as ex: - raise ConfigEntryNotReady( - "Failed to initialize the Honeywell client: " - "Check your configuration (username, password), " - "or maybe you have exceeded the API rate limit?" - ) from ex - - +@dataclass class HoneywellData: - """Get the latest data and update.""" + """Shared data for Honeywell.""" - def __init__( - self, - hass: HomeAssistant, - config_entry: ConfigEntry, - client: somecomfort.SomeComfort, - username: str, - password: str, - devices: dict[str, somecomfort.Device], - ) -> None: - """Initialize the data object.""" - self._hass = hass - self._config = config_entry - self._client = client - self._username = username - self._password = password - self.devices = devices - - async def _retry(self) -> bool: - """Recreate a new somecomfort client. - - When we got an error, the best way to be sure that the next query - will succeed, is to recreate a new somecomfort client. - """ - self._client = await self._hass.async_add_executor_job( - get_somecomfort_client, self._username, self._password - ) - - if self._client is None: - return False - - refreshed_devices = [ - device - for location in self._client.locations_by_id.values() - for device in location.devices_by_id.values() - ] - - if len(refreshed_devices) == 0: - _LOGGER.error("Failed to find any devices after retry") - return False - - for updated_device in refreshed_devices: - if updated_device.deviceid in self.devices: - self.devices[updated_device.deviceid] = updated_device - else: - _LOGGER.info( - "New device with ID %s detected, reload the honeywell integration" - " if you want to access it in Home Assistant" - ) - - await self._hass.config_entries.async_reload(self._config.entry_id) - return True - - async def _refresh_devices(self): - """Refresh each enabled device.""" - for device in self.devices.values(): - await self._hass.async_add_executor_job(device.refresh) - await asyncio.sleep(UPDATE_LOOP_SLEEP_TIME) - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self) -> None: - """Update the state.""" - retries = 3 - while retries > 0: - try: - await self._refresh_devices() - break - except ( - somecomfort.client.APIRateLimited, - somecomfort.client.ConnectionError, - somecomfort.client.ConnectionTimeout, - OSError, - ) as exp: - retries -= 1 - if retries == 0: - _LOGGER.error( - "Ran out of retry attempts (3 attempts allocated). Error: %s", - exp, - ) - raise exp - - result = await self._retry() - - if not result: - _LOGGER.error("Retry result was empty. Error: %s", exp) - raise exp - - _LOGGER.info("SomeComfort update failed, retrying. Error: %s", exp) + entry_id: str + client: AIOSomecomfort.AIOSomeComfort + devices: dict[str, AIOSomecomfort.device.Device] diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 3bc5e0e7ef8..0267eb32e47 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -4,7 +4,7 @@ from __future__ import annotations import datetime from typing import Any -import somecomfort +import AIOSomecomfort from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, @@ -26,6 +26,7 @@ from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import HoneywellData from .const import ( _LOGGER, CONF_COOL_AWAY_TEMPERATURE, @@ -39,6 +40,9 @@ ATTR_PERMANENT_HOLD = "permanent_hold" PRESET_HOLD = "Hold" +HEATING_MODES = {"heat", "emheat", "auto"} +COOLING_MODES = {"cool", "auto"} + HVAC_MODE_TO_HW_MODE = { "SwitchOffAllowed": {HVACMode.OFF: "off"}, "SwitchAutoAllowed": {HVACMode.HEAT_COOL: "auto"}, @@ -70,7 +74,7 @@ HW_FAN_MODE_TO_HA = { "follow schedule": FAN_AUTO, } -PARALLEL_UPDATES = 1 +SCAN_INTERVAL = datetime.timedelta(seconds=30) async def async_setup_entry( @@ -80,7 +84,7 @@ async def async_setup_entry( cool_away_temp = entry.options.get(CONF_COOL_AWAY_TEMPERATURE) heat_away_temp = entry.options.get(CONF_HEAT_AWAY_TEMPERATURE) - data = hass.data[DOMAIN][entry.entry_id] + data: HoneywellData = hass.data[DOMAIN][entry.entry_id] async_add_entities( [ @@ -93,7 +97,13 @@ async def async_setup_entry( class HoneywellUSThermostat(ClimateEntity): """Representation of a Honeywell US Thermostat.""" - def __init__(self, data, device, cool_away_temp, heat_away_temp): + def __init__( + self, + data: HoneywellData, + device: AIOSomecomfort.device.Device, + cool_away_temp: int | None, + heat_away_temp: int | None, + ) -> None: """Initialize the thermostat.""" self._data = data self._device = device @@ -110,8 +120,13 @@ class HoneywellUSThermostat(ClimateEntity): self._attr_is_aux_heat = device.system_mode == "emheat" # not all honeywell HVACs support all modes - mappings = [v for k, v in HVAC_MODE_TO_HW_MODE.items() if device.raw_ui_data[k]] - self._hvac_mode_map = {k: v for d in mappings for k, v in d.items()} + + self._hvac_mode_map = { + key2: value2 + for key1, value1 in HVAC_MODE_TO_HW_MODE.items() + if device.raw_ui_data[key1] + for key2, value2 in value1.items() + } self._attr_hvac_modes = list(self._hvac_mode_map) self._attr_supported_features = ( @@ -130,8 +145,12 @@ class HoneywellUSThermostat(ClimateEntity): return # not all honeywell fans support all modes - mappings = [v for k, v in FAN_MODE_TO_HW.items() if device.raw_fan_data[k]] - self._fan_mode_map = {k: v for d in mappings for k, v in d.items()} + self._fan_mode_map = { + key2: value2 + for key1, value1 in FAN_MODE_TO_HW.items() + if device.raw_fan_data[key1] + for key2, value2 in value1.items() + } self._attr_fan_modes = list(self._fan_mode_map) @@ -150,10 +169,17 @@ class HoneywellUSThermostat(ClimateEntity): @property def min_temp(self) -> float: """Return the minimum temperature.""" - if self.hvac_mode in [HVACMode.COOL, HVACMode.HEAT_COOL]: + if self.hvac_mode == HVACMode.COOL: return self._device.raw_ui_data["CoolLowerSetptLimit"] if self.hvac_mode == HVACMode.HEAT: return self._device.raw_ui_data["HeatLowerSetptLimit"] + if self.hvac_mode == HVACMode.HEAT_COOL: + return min( + [ + self._device.raw_ui_data["CoolLowerSetptLimit"], + self._device.raw_ui_data["HeatLowerSetptLimit"], + ] + ) return DEFAULT_MIN_TEMP @property @@ -161,8 +187,15 @@ class HoneywellUSThermostat(ClimateEntity): """Return the maximum temperature.""" if self.hvac_mode == HVACMode.COOL: return self._device.raw_ui_data["CoolUpperSetptLimit"] - if self.hvac_mode in [HVACMode.HEAT, HVACMode.HEAT_COOL]: + if self.hvac_mode == HVACMode.HEAT: return self._device.raw_ui_data["HeatUpperSetptLimit"] + if self.hvac_mode == HVACMode.HEAT_COOL: + return max( + [ + self._device.raw_ui_data["CoolUpperSetptLimit"], + self._device.raw_ui_data["HeatUpperSetptLimit"], + ] + ) return DEFAULT_MAX_TEMP @property @@ -180,7 +213,7 @@ class HoneywellUSThermostat(ClimateEntity): """Return the current running hvac operation if supported.""" if self.hvac_mode == HVACMode.OFF: return None - return HW_MODE_TO_HA_HVAC_ACTION[self._device.equipment_output_status] + return HW_MODE_TO_HA_HVAC_ACTION.get(self._device.equipment_output_status) @property def current_temperature(self) -> float | None: @@ -223,14 +256,14 @@ class HoneywellUSThermostat(ClimateEntity): @property def fan_mode(self) -> str | None: """Return the fan setting.""" - return HW_FAN_MODE_TO_HA[self._device.fan_mode] + return HW_FAN_MODE_TO_HA.get(self._device.fan_mode) def _is_permanent_hold(self) -> bool: heat_status = self._device.raw_ui_data.get("StatusHeat", 0) cool_status = self._device.raw_ui_data.get("StatusCool", 0) return heat_status == 2 or cool_status == 2 - def _set_temperature(self, **kwargs) -> None: + async def _set_temperature(self, **kwargs) -> None: """Set new target temperature.""" if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None: return @@ -238,43 +271,55 @@ class HoneywellUSThermostat(ClimateEntity): # Get current mode mode = self._device.system_mode # Set hold if this is not the case - if getattr(self._device, f"hold_{mode}", None) is False: - # Get next period key - next_period_key = f"{mode.capitalize()}NextPeriod" - # Get next period raw value - next_period = self._device.raw_ui_data.get(next_period_key) + if self._device.hold_heat is False and self._device.hold_cool is False: # Get next period time - hour, minute = divmod(next_period * 15, 60) + hour_heat, minute_heat = divmod( + self._device.raw_ui_data["HeatNextPeriod"] * 15, 60 + ) + hour_cool, minute_cool = divmod( + self._device.raw_ui_data["CoolNextPeriod"] * 15, 60 + ) # Set hold time - setattr(self._device, f"hold_{mode}", datetime.time(hour, minute)) - # Set temperature - setattr(self._device, f"setpoint_{mode}", temperature) - except somecomfort.SomeComfortError: - _LOGGER.error("Temperature %.1f out of range", temperature) + if mode in COOLING_MODES: + await self._device.set_hold_cool( + datetime.time(hour_cool, minute_cool) + ) + if mode in HEATING_MODES: + await self._device.set_hold_heat( + datetime.time(hour_heat, minute_heat) + ) - def set_temperature(self, **kwargs: Any) -> None: + # Set temperature if not in auto + if mode == "cool": + await self._device.set_setpoint_cool(temperature) + if mode == "heat": + await self._device.set_setpoint_heat(temperature) + + except AIOSomecomfort.SomeComfortError as err: + _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) + + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" if {HVACMode.COOL, HVACMode.HEAT} & set(self._hvac_mode_map): - self._set_temperature(**kwargs) - - try: - if HVACMode.HEAT_COOL in self._hvac_mode_map: + await self._set_temperature(**kwargs) + try: if temperature := kwargs.get(ATTR_TARGET_TEMP_HIGH): - self._device.setpoint_cool = temperature + await self._device.set_setpoint_cool(temperature) if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): - self._device.setpoint_heat = temperature - except somecomfort.SomeComfortError as err: - _LOGGER.error("Invalid temperature %s: %s", temperature, err) + await self._device.set_setpoint_heat(temperature) - def set_fan_mode(self, fan_mode: str) -> None: + except AIOSomecomfort.SomeComfortError as err: + _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) + + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" - self._device.fan_mode = self._fan_mode_map[fan_mode] + await self._device.set_fan_mode(self._fan_mode_map[fan_mode]) - def set_hvac_mode(self, hvac_mode: HVACMode) -> None: + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" - self._device.system_mode = self._hvac_mode_map[hvac_mode] + await self._device.set_system_mode(self._hvac_mode_map[hvac_mode]) - def _turn_away_mode_on(self) -> None: + async def _turn_away_mode_on(self) -> None: """Turn away on. Somecomfort does have a proprietary away mode, but it doesn't really @@ -285,73 +330,95 @@ class HoneywellUSThermostat(ClimateEntity): try: # Get current mode mode = self._device.system_mode - except somecomfort.SomeComfortError: + except AIOSomecomfort.SomeComfortError: _LOGGER.error("Can not get system mode") return try: # Set permanent hold - setattr(self._device, f"hold_{mode}", True) - # Set temperature - setattr( - self._device, - f"setpoint_{mode}", - getattr(self, f"_{mode}_away_temp"), - ) - except somecomfort.SomeComfortError: + # and Set temperature + if mode in COOLING_MODES: + await self._device.set_hold_cool(True) + await self._device.set_setpoint_cool(self._cool_away_temp) + if mode in HEATING_MODES: + await self._device.set_hold_heat(True) + await self._device.set_setpoint_heat(self._heat_away_temp) + + except AIOSomecomfort.SomeComfortError: + _LOGGER.error( - "Temperature %.1f out of range", getattr(self, f"_{mode}_away_temp") + "Temperature out of range. Mode: %s, Heat Temperature: %.1f, Cool Temperature: %.1f", + mode, + self._heat_away_temp, + self._cool_away_temp, ) - def _turn_hold_mode_on(self) -> None: + async def _turn_hold_mode_on(self) -> None: """Turn permanent hold on.""" try: # Get current mode mode = self._device.system_mode - except somecomfort.SomeComfortError: + except AIOSomecomfort.SomeComfortError: _LOGGER.error("Can not get system mode") return # Check that we got a valid mode back if mode in HW_MODE_TO_HVAC_MODE: try: # Set permanent hold - setattr(self._device, f"hold_{mode}", True) - except somecomfort.SomeComfortError: + if mode in COOLING_MODES: + await self._device.set_hold_cool(True) + if mode in HEATING_MODES: + await self._device.set_hold_heat(True) + + except AIOSomecomfort.SomeComfortError: _LOGGER.error("Couldn't set permanent hold") else: _LOGGER.error("Invalid system mode returned: %s", mode) - def _turn_away_mode_off(self) -> None: + async def _turn_away_mode_off(self) -> None: """Turn away/hold off.""" self._away = False try: # Disabling all hold modes - self._device.hold_cool = False - self._device.hold_heat = False - except somecomfort.SomeComfortError: + await self._device.set_hold_cool(False) + await self._device.set_hold_heat(False) + except AIOSomecomfort.SomeComfortError: _LOGGER.error("Can not stop hold mode") - def set_preset_mode(self, preset_mode: str) -> None: + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if preset_mode == PRESET_AWAY: - self._turn_away_mode_on() + await self._turn_away_mode_on() elif preset_mode == PRESET_HOLD: self._away = False - self._turn_hold_mode_on() + await self._turn_hold_mode_on() else: - self._turn_away_mode_off() + await self._turn_away_mode_off() - def turn_aux_heat_on(self) -> None: + async def async_turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" - self._device.system_mode = "emheat" + await self._device.set_system_mode("emheat") - def turn_aux_heat_off(self) -> None: + async def async_turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" if HVACMode.HEAT in self.hvac_modes: - self.set_hvac_mode(HVACMode.HEAT) + await self.async_set_hvac_mode(HVACMode.HEAT) else: - self.set_hvac_mode(HVACMode.OFF) + await self.async_set_hvac_mode(HVACMode.OFF) async def async_update(self) -> None: """Get the latest state from the service.""" - await self._data.async_update() + try: + await self._device.refresh() + except ( + AIOSomecomfort.SomeComfortError, + OSError, + ): + try: + await self._data.client.login() + + except AIOSomecomfort.SomeComfortError: + self._attr_available = False + await self.hass.async_create_task( + self.hass.config_entries.async_reload(self._data.entry_id) + ) diff --git a/homeassistant/components/honeywell/config_flow.py b/homeassistant/components/honeywell/config_flow.py index 7f7d7d7281a..9f630d90fbe 100644 --- a/homeassistant/components/honeywell/config_flow.py +++ b/homeassistant/components/honeywell/config_flow.py @@ -1,13 +1,17 @@ """Config flow to configure the honeywell integration.""" from __future__ import annotations +import asyncio + +import AIOSomecomfort import voluptuous as vol 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 -from . import get_somecomfort_client from .const import ( CONF_COOL_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE, @@ -22,20 +26,27 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None) -> FlowResult: """Create config entry. Show the setup form to the user.""" errors = {} - if user_input is not None: - valid = await self.is_valid(**user_input) - if valid: + try: + await self.is_valid(**user_input) + except AIOSomecomfort.AuthError: + errors["base"] = "invalid_auth" + except ( + AIOSomecomfort.ConnectionError, + AIOSomecomfort.ConnectionTimeout, + asyncio.TimeoutError, + ): + errors["base"] = "cannot_connect" + + if not errors: return self.async_create_entry( title=DOMAIN, data=user_input, ) - errors["base"] = "invalid_auth" - data_schema = { vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, @@ -46,11 +57,14 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def is_valid(self, **kwargs) -> bool: """Check if login credentials are valid.""" - client = await self.hass.async_add_executor_job( - get_somecomfort_client, kwargs[CONF_USERNAME], kwargs[CONF_PASSWORD] + client = AIOSomecomfort.AIOSomeComfort( + kwargs[CONF_USERNAME], + kwargs[CONF_PASSWORD], + session=async_get_clientsession(self.hass), ) - return client is not None + await client.login() + return True @staticmethod @callback @@ -68,7 +82,7 @@ class HoneywellOptionsFlowHandler(config_entries.OptionsFlow): """Initialize Honeywell options flow.""" self.config_entry = entry - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input=None) -> FlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title=DOMAIN, data=user_input) diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index 7ea878f074e..7eb07711b09 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -3,8 +3,8 @@ "name": "Honeywell Total Connect Comfort (US)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/honeywell", - "requirements": ["somecomfort==0.8.0"], - "codeowners": ["@rdfurman"], + "requirements": ["aiosomecomfort==0.0.3"], + "codeowners": ["@rdfurman", "@mkmer"], "iot_class": "cloud_polling", "loggers": ["somecomfort"] } diff --git a/homeassistant/components/honeywell/sensor.py b/homeassistant/components/honeywell/sensor.py index ca7320d7c4c..59f00472700 100644 --- a/homeassistant/components/honeywell/sensor.py +++ b/homeassistant/components/honeywell/sensor.py @@ -5,7 +5,7 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Any -from somecomfort import Device +from AIOSomecomfort.device import Device from homeassistant.components.sensor import ( SensorDeviceClass, diff --git a/homeassistant/components/honeywell/strings.json b/homeassistant/components/honeywell/strings.json index 8e085ad7e86..87f3e025917 100644 --- a/homeassistant/components/honeywell/strings.json +++ b/homeassistant/components/honeywell/strings.json @@ -10,7 +10,8 @@ } }, "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } }, "options": { diff --git a/homeassistant/components/honeywell/translations/bg.json b/homeassistant/components/honeywell/translations/bg.json index e7020268311..e549d766966 100644 --- a/homeassistant/components/honeywell/translations/bg.json +++ b/homeassistant/components/honeywell/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435" }, "step": { diff --git a/homeassistant/components/honeywell/translations/ca.json b/homeassistant/components/honeywell/translations/ca.json index 0830d657173..dcbefadd19e 100644 --- a/homeassistant/components/honeywell/translations/ca.json +++ b/homeassistant/components/honeywell/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { diff --git a/homeassistant/components/honeywell/translations/de.json b/homeassistant/components/honeywell/translations/de.json index 6cbceffce51..eb2133f5891 100644 --- a/homeassistant/components/honeywell/translations/de.json +++ b/homeassistant/components/honeywell/translations/de.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { diff --git a/homeassistant/components/honeywell/translations/el.json b/homeassistant/components/honeywell/translations/el.json index b3fd654ded8..dc8202716c9 100644 --- a/homeassistant/components/honeywell/translations/el.json +++ b/homeassistant/components/honeywell/translations/el.json @@ -1,6 +1,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" }, "step": { diff --git a/homeassistant/components/honeywell/translations/en.json b/homeassistant/components/honeywell/translations/en.json index bf47a15be55..19cf2101f85 100644 --- a/homeassistant/components/honeywell/translations/en.json +++ b/homeassistant/components/honeywell/translations/en.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication" }, "step": { diff --git a/homeassistant/components/honeywell/translations/es-419.json b/homeassistant/components/honeywell/translations/es-419.json new file mode 100644 index 00000000000..e3f8891f3b1 --- /dev/null +++ b/homeassistant/components/honeywell/translations/es-419.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/es.json b/homeassistant/components/honeywell/translations/es.json index ef261013844..705c1c1c4c4 100644 --- a/homeassistant/components/honeywell/translations/es.json +++ b/homeassistant/components/honeywell/translations/es.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { diff --git a/homeassistant/components/honeywell/translations/et.json b/homeassistant/components/honeywell/translations/et.json index 6673959ad31..30d2f910e76 100644 --- a/homeassistant/components/honeywell/translations/et.json +++ b/homeassistant/components/honeywell/translations/et.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus" }, "step": { diff --git a/homeassistant/components/honeywell/translations/hu.json b/homeassistant/components/honeywell/translations/hu.json index b0552b23fe1..8a3c85b3314 100644 --- a/homeassistant/components/honeywell/translations/hu.json +++ b/homeassistant/components/honeywell/translations/hu.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { diff --git a/homeassistant/components/honeywell/translations/id.json b/homeassistant/components/honeywell/translations/id.json index 5ed95a27a76..ee133df20e3 100644 --- a/homeassistant/components/honeywell/translations/id.json +++ b/homeassistant/components/honeywell/translations/id.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid" }, "step": { diff --git a/homeassistant/components/honeywell/translations/it.json b/homeassistant/components/honeywell/translations/it.json index 87669762fa2..2618f5bf11d 100644 --- a/homeassistant/components/honeywell/translations/it.json +++ b/homeassistant/components/honeywell/translations/it.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida" }, "step": { diff --git a/homeassistant/components/honeywell/translations/ja.json b/homeassistant/components/honeywell/translations/ja.json index 1af30341d33..976a9e51df4 100644 --- a/homeassistant/components/honeywell/translations/ja.json +++ b/homeassistant/components/honeywell/translations/ja.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" }, "step": { diff --git a/homeassistant/components/honeywell/translations/no.json b/homeassistant/components/honeywell/translations/no.json index e35e8a2b278..2f8a7b3ec7a 100644 --- a/homeassistant/components/honeywell/translations/no.json +++ b/homeassistant/components/honeywell/translations/no.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning" }, "step": { diff --git a/homeassistant/components/honeywell/translations/pl.json b/homeassistant/components/honeywell/translations/pl.json index e484f88cb49..1e1496b3d89 100644 --- a/homeassistant/components/honeywell/translations/pl.json +++ b/homeassistant/components/honeywell/translations/pl.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie" }, "step": { diff --git a/homeassistant/components/honeywell/translations/pt-BR.json b/homeassistant/components/honeywell/translations/pt-BR.json index 4cae96c5b1b..d781971cc54 100644 --- a/homeassistant/components/honeywell/translations/pt-BR.json +++ b/homeassistant/components/honeywell/translations/pt-BR.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Falhou ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { diff --git a/homeassistant/components/honeywell/translations/ru.json b/homeassistant/components/honeywell/translations/ru.json index b370df892f6..d75de8108f6 100644 --- a/homeassistant/components/honeywell/translations/ru.json +++ b/homeassistant/components/honeywell/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { diff --git a/homeassistant/components/honeywell/translations/sk.json b/homeassistant/components/honeywell/translations/sk.json index 35bd0d2c2f7..1516ea4925a 100644 --- a/homeassistant/components/honeywell/translations/sk.json +++ b/homeassistant/components/honeywell/translations/sk.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { diff --git a/homeassistant/components/honeywell/translations/uk.json b/homeassistant/components/honeywell/translations/uk.json new file mode 100644 index 00000000000..2f0757a08ff --- /dev/null +++ b/homeassistant/components/honeywell/translations/uk.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/translations/zh-Hant.json b/homeassistant/components/honeywell/translations/zh-Hant.json index c6a657b13be..6099e5465de 100644 --- a/homeassistant/components/honeywell/translations/zh-Hant.json +++ b/homeassistant/components/honeywell/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 6624d941114..1c201725c00 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -7,7 +7,7 @@ import logging import os import ssl from tempfile import NamedTemporaryFile -from typing import Any, Final, TypedDict, Union, cast +from typing import Any, Final, TypedDict, cast from aiohttp import web from aiohttp.typedefs import StrOrURL @@ -74,6 +74,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_LINE_SIZE: Final = 24570 STORAGE_KEY: Final = DOMAIN STORAGE_VERSION: Final = 1 @@ -234,7 +235,14 @@ class HomeAssistantHTTP: ssl_profile: str, ) -> None: """Initialize the HTTP Home Assistant server.""" - self.app = web.Application(middlewares=[], client_max_size=MAX_CLIENT_SIZE) + self.app = web.Application( + middlewares=[], + client_max_size=MAX_CLIENT_SIZE, + handler_args={ + "max_line_size": MAX_LINE_SIZE, + "max_field_size": MAX_LINE_SIZE, + }, + ) self.hass = hass self.ssl_certificate = ssl_certificate self.ssl_peer_certificate = ssl_peer_certificate @@ -451,7 +459,7 @@ class HomeAssistantHTTP: # However in Home Assistant components can be discovered after boot. # This will now raise a RunTimeError. # To work around this we now prevent the router from getting frozen - # pylint: disable=protected-access + # pylint: disable-next=protected-access self.app._router.freeze = lambda: None # type: ignore[assignment] self.runner = web.AppRunner(self.app) @@ -490,7 +498,7 @@ async def start_http_server_and_save_config( if CONF_TRUSTED_PROXIES in conf: conf[CONF_TRUSTED_PROXIES] = [ - str(cast(Union[IPv4Network, IPv6Network], ip).network_address) + str(cast(IPv4Network | IPv6Network, ip).network_address) for ip in conf[CONF_TRUSTED_PROXIES] ] diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 7c6f445ce80..197c4f34dad 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -99,7 +99,7 @@ def async_user_not_allowed_do_auth( return "No request available to validate local access" if "cloud" in hass.config.components: - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from hass_nabucasa import remote if remote.is_cloud_request.get(): diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 9f300aa9db9..85feb19a24b 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -9,11 +9,10 @@ from http import HTTPStatus from ipaddress import IPv4Address, IPv6Address, ip_address import logging from socket import gethostbyaddr, herror -from typing import Any, Final, TypeVar +from typing import Any, Concatenate, Final, ParamSpec, TypeVar from aiohttp.web import Application, Request, Response, StreamResponse, middleware from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.components import persistent_notification @@ -141,7 +140,7 @@ async def process_wrong_login(request: Request) -> None: # Supervisor IP should never be banned if "hassio" in hass.config.components: - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.components import hassio if hassio.get_supervisor_ip() == str(remote_addr): diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index 97a0530b703..7eb1a5f84fe 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -31,7 +31,7 @@ def setup_cors(app: Application, origins: list[str]) -> None: """Set up CORS.""" # This import should remain here. That way the HTTP integration can always # be imported by other integrations without it's requirements being installed. - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel import aiohttp_cors cors = aiohttp_cors.setup( diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 6647a6436c5..2868bee9432 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -5,10 +5,9 @@ from collections.abc import Awaitable, Callable, Coroutine from functools import wraps from http import HTTPStatus import logging -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from aiohttp import web -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from .view import HomeAssistantView diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 6ab3b2a84a4..32582dbdc92 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -20,7 +20,11 @@ import voluptuous as vol from homeassistant import exceptions from homeassistant.const import CONTENT_TYPE_JSON from homeassistant.core import Context, is_callback -from homeassistant.helpers.json import JSON_ENCODE_EXCEPTIONS, json_bytes +from homeassistant.helpers.json import JSON_ENCODE_EXCEPTIONS, json_bytes, json_dumps +from homeassistant.util.json import ( + find_paths_unserializable_data, + format_unserializable_data, +) from .const import KEY_AUTHENTICATED, KEY_HASS @@ -54,7 +58,12 @@ class HomeAssistantView: try: msg = json_bytes(result) except JSON_ENCODE_EXCEPTIONS as err: - _LOGGER.error("Unable to serialize to JSON: %s\n%s", err, result) + _LOGGER.error( + "Unable to serialize to JSON. Bad data found at %s", + format_unserializable_data( + find_paths_unserializable_data(result, dump=json_dumps) + ), + ) raise HTTPInternalServerError from err response = web.Response( body=msg, diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index b34425e79ee..badf7d0a484 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -55,7 +55,7 @@ from homeassistant.helpers.typing import ConfigType from .const import ( ADMIN_SERVICES, ALL_KEYS, - ATTR_UNIQUE_ID, + ATTR_CONFIG_ENTRY_ID, CONF_MANUFACTURER, CONF_UNAUTHENTICATED_MODE, CONNECTION_TIMEOUT, @@ -365,9 +365,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ): if not entity_entry.unique_id.startswith("None-"): continue - new_unique_id = ( - f"{serial_number}-{entity_entry.unique_id.split('-', 1)[1]}" - ) + new_unique_id = entity_entry.unique_id.removeprefix("None-") + new_unique_id = f"{serial_number}-{new_unique_id}" ent_reg.async_update_entity( entity_entry.entity_id, new_unique_id=new_unique_id ) @@ -387,7 +386,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return False # Store reference to router - hass.data[DOMAIN].routers[entry.unique_id] = router + hass.data[DOMAIN].routers[entry.entry_id] = router # Clear all subscriptions, enabled entities will push back theirs router.subscriptions.clear() @@ -449,7 +448,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: Platform.NOTIFY, DOMAIN, { - ATTR_UNIQUE_ID: entry.unique_id, + ATTR_CONFIG_ENTRY_ID: entry.entry_id, CONF_NAME: entry.options.get(CONF_NAME, DEFAULT_NOTIFY_SERVICE_NAME), CONF_RECIPIENT: entry.options.get(CONF_RECIPIENT), }, @@ -484,7 +483,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) # Forget about the router and invoke its cleanup - router = hass.data[DOMAIN].routers.pop(config_entry.unique_id) + router = hass.data[DOMAIN].routers.pop(config_entry.entry_id) await hass.async_add_executor_job(router.cleanup) return True diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py index 695b7e2e815..9966b9cc5f5 100644 --- a/homeassistant/components/huawei_lte/binary_sensor.py +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -33,7 +33,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up from config entry.""" - router = hass.data[DOMAIN].routers[config_entry.unique_id] + router = hass.data[DOMAIN].routers[config_entry.entry_id] entities: list[Entity] = [] if router.data.get(KEY_MONITORING_STATUS): diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 0b968cee58f..53cc0efb919 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -2,7 +2,7 @@ DOMAIN = "huawei_lte" -ATTR_UNIQUE_ID = "unique_id" +ATTR_CONFIG_ENTRY_ID = "config_entry_id" CONF_MANUFACTURER = "manufacturer" CONF_TRACK_WIRED_CLIENTS = "track_wired_clients" diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 917250eae79..52d12d20005 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -60,7 +60,7 @@ async def async_setup_entry( # Grab hosts list once to examine whether the initial fetch has got some data for # us, i.e. if wlan host list is supported. Only set up a subscription and proceed # with adding and tracking entities if it is. - router = hass.data[DOMAIN].routers[config_entry.unique_id] + router = hass.data[DOMAIN].routers[config_entry.entry_id] if (hosts := _get_hosts(router, True)) is None: return diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index d47249ccc51..4474188ea22 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import Router -from .const import ATTR_UNIQUE_ID, DOMAIN +from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -28,7 +28,7 @@ async def async_get_service( if discovery_info is None: return None - router = hass.data[DOMAIN].routers[discovery_info[ATTR_UNIQUE_ID]] + router = hass.data[DOMAIN].routers[discovery_info[ATTR_CONFIG_ENTRY_ID]] default_targets = discovery_info[CONF_RECIPIENT] or [] return HuaweiLteSmsNotificationService(router, default_targets) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index b9bf741ac48..adb6cb1f06e 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -2,11 +2,14 @@ from __future__ import annotations from bisect import bisect -from collections.abc import Callable +from collections.abc import Callable, Sequence from dataclasses import dataclass, field +from datetime import datetime, timedelta import logging import re +from huawei_lte_api.enums.net import NetworkModeEnum + from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, @@ -17,7 +20,6 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PERCENTAGE, - STATE_UNKNOWN, UnitOfDataRate, UnitOfFrequency, UnitOfInformation, @@ -62,6 +64,45 @@ def format_default(value: StateType) -> tuple[StateType, str | None]: return value, unit +def format_freq_mhz(value: StateType) -> tuple[StateType, UnitOfFrequency]: + """Format a frequency value for which source is in tens of MHz.""" + return ( + round(int(value) / 10) if value is not None else None, + UnitOfFrequency.MEGAHERTZ, + ) + + +def format_last_reset_elapsed_seconds(value: str | None) -> datetime | None: + """Convert elapsed seconds to last reset datetime.""" + if value is None: + return None + try: + last_reset = datetime.now() - timedelta(seconds=int(value)) + last_reset.replace(microsecond=0) + return last_reset + except ValueError: + return None + + +def signal_icon(limits: Sequence[int], value: StateType) -> str: + """Get signal icon.""" + return ( + "mdi:signal-cellular-outline", + "mdi:signal-cellular-1", + "mdi:signal-cellular-2", + "mdi:signal-cellular-3", + )[bisect(limits, value if value is not None else -1000)] + + +def bandwidth_icon(limits: Sequence[int], value: StateType) -> str: + """Get bandwidth icon.""" + return ( + "mdi:speedometer-slow", + "mdi:speedometer-medium", + "mdi:speedometer", + )[bisect(limits, value if value is not None else -1000)] + + @dataclass class HuaweiSensorGroup: """Class describing Huawei LTE sensor groups.""" @@ -75,8 +116,11 @@ class HuaweiSensorGroup: class HuaweiSensorEntityDescription(SensorEntityDescription): """Class describing Huawei LTE sensor entities.""" - formatter: Callable[[str], tuple[StateType, str | None]] = format_default + format_fn: Callable[[str], tuple[StateType, str | None]] = format_default icon_fn: Callable[[StateType], str] | None = None + device_class_fn: Callable[[StateType], SensorDeviceClass | None] | None = None + last_reset_item: str | None = None + last_reset_format_fn: Callable[[str | None], datetime | None] | None = None SENSOR_META: dict[str, HuaweiSensorGroup] = { @@ -114,11 +158,21 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { # KEY_DEVICE_SIGNAL: HuaweiSensorGroup( descriptions={ + "arfcn": HuaweiSensorEntityDescription( + key="arfcn", + name="ARFCN", + entity_category=EntityCategory.DIAGNOSTIC, + ), "band": HuaweiSensorEntityDescription( key="band", name="Band", entity_category=EntityCategory.DIAGNOSTIC, ), + "bsic": HuaweiSensorEntityDescription( + key="bsic", + name="Base station identity code", + entity_category=EntityCategory.DIAGNOSTIC, + ), "cell_id": HuaweiSensorEntityDescription( key="cell_id", name="Cell ID", @@ -144,11 +198,13 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "dlbandwidth": HuaweiSensorEntityDescription( key="dlbandwidth", name="Downlink bandwidth", - icon_fn=lambda x: ( - "mdi:speedometer-slow", - "mdi:speedometer-medium", - "mdi:speedometer", - )[bisect((8, 15), x if x is not None else -1000)], + icon_fn=lambda x: bandwidth_icon((8, 15), x), + entity_category=EntityCategory.DIAGNOSTIC, + ), + "dlfrequency": HuaweiSensorEntityDescription( + key="dlfrequency", + name="Downlink frequency", + device_class=SensorDeviceClass.FREQUENCY, entity_category=EntityCategory.DIAGNOSTIC, ), "earfcn": HuaweiSensorEntityDescription( @@ -161,12 +217,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { name="EC/IO", device_class=SensorDeviceClass.SIGNAL_STRENGTH, # https://wiki.teltonika.lt/view/EC/IO - icon_fn=lambda x: ( - "mdi:signal-cellular-outline", - "mdi:signal-cellular-1", - "mdi:signal-cellular-2", - "mdi:signal-cellular-3", - )[bisect((-20, -10, -6), x if x is not None else -1000)], + icon_fn=lambda x: signal_icon((-20, -10, -6), x), state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -183,29 +234,23 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { ), "ltedlfreq": HuaweiSensorEntityDescription( key="ltedlfreq", - name="Downlink frequency", - formatter=lambda x: ( - round(int(x) / 10) if x is not None else None, - UnitOfFrequency.MEGAHERTZ, - ), + name="LTE downlink frequency", + format_fn=format_freq_mhz, device_class=SensorDeviceClass.FREQUENCY, entity_category=EntityCategory.DIAGNOSTIC, ), "lteulfreq": HuaweiSensorEntityDescription( key="lteulfreq", - name="Uplink frequency", - formatter=lambda x: ( - round(int(x) / 10) if x is not None else None, - UnitOfFrequency.MEGAHERTZ, - ), + name="LTE uplink frequency", + format_fn=format_freq_mhz, device_class=SensorDeviceClass.FREQUENCY, entity_category=EntityCategory.DIAGNOSTIC, ), "mode": HuaweiSensorEntityDescription( key="mode", name="Mode", - formatter=lambda x: ( - {"0": "2G", "2": "3G", "7": "4G"}.get(x, "Unknown"), + format_fn=lambda x: ( + {"0": "2G", "2": "3G", "7": "4G"}.get(x), None, ), icon_fn=lambda x: ( @@ -244,12 +289,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { name="RSCP", device_class=SensorDeviceClass.SIGNAL_STRENGTH, # https://wiki.teltonika.lt/view/RSCP - icon_fn=lambda x: ( - "mdi:signal-cellular-outline", - "mdi:signal-cellular-1", - "mdi:signal-cellular-2", - "mdi:signal-cellular-3", - )[bisect((-95, -85, -75), x if x is not None else -1000)], + icon_fn=lambda x: signal_icon((-95, -85, -75), x), state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), @@ -258,12 +298,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { name="RSRP", device_class=SensorDeviceClass.SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/rsrp.php - icon_fn=lambda x: ( - "mdi:signal-cellular-outline", - "mdi:signal-cellular-1", - "mdi:signal-cellular-2", - "mdi:signal-cellular-3", - )[bisect((-110, -95, -80), x if x is not None else -1000)], + icon_fn=lambda x: signal_icon((-110, -95, -80), x), state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, @@ -273,12 +308,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { name="RSRQ", device_class=SensorDeviceClass.SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/rsrq.php - icon_fn=lambda x: ( - "mdi:signal-cellular-outline", - "mdi:signal-cellular-1", - "mdi:signal-cellular-2", - "mdi:signal-cellular-3", - )[bisect((-11, -8, -5), x if x is not None else -1000)], + icon_fn=lambda x: signal_icon((-11, -8, -5), x), state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, @@ -288,12 +318,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { name="RSSI", device_class=SensorDeviceClass.SIGNAL_STRENGTH, # https://eyesaas.com/wi-fi-signal-strength/ - icon_fn=lambda x: ( - "mdi:signal-cellular-outline", - "mdi:signal-cellular-1", - "mdi:signal-cellular-2", - "mdi:signal-cellular-3", - )[bisect((-80, -70, -60), x if x is not None else -1000)], + icon_fn=lambda x: signal_icon((-80, -70, -60), x), state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, @@ -303,12 +328,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { name="SINR", device_class=SensorDeviceClass.SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/sinr.php - icon_fn=lambda x: ( - "mdi:signal-cellular-outline", - "mdi:signal-cellular-1", - "mdi:signal-cellular-2", - "mdi:signal-cellular-3", - )[bisect((0, 5, 10), x if x is not None else -1000)], + icon_fn=lambda x: signal_icon((0, 5, 10), x), state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=True, @@ -332,7 +352,15 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "txpower": HuaweiSensorEntityDescription( key="txpower", name="Transmit power", - device_class=SensorDeviceClass.SIGNAL_STRENGTH, + # The value we get from the API tends to consist of several, e.g. + # PPusch:15dBm PPucch:2dBm PSrs:42dBm PPrach:1dBm + # Present as SIGNAL_STRENGTH only if it was parsed to a number. + # We could try to parse this to separate component sensors sometime. + device_class_fn=lambda x: ( + SensorDeviceClass.SIGNAL_STRENGTH + if isinstance(x, (float, int)) + else None + ), entity_category=EntityCategory.DIAGNOSTIC, ), "ul_mcs": HuaweiSensorEntityDescription( @@ -343,11 +371,13 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "ulbandwidth": HuaweiSensorEntityDescription( key="ulbandwidth", name="Uplink bandwidth", - icon_fn=lambda x: ( - "mdi:speedometer-slow", - "mdi:speedometer-medium", - "mdi:speedometer", - )[bisect((8, 15), x if x is not None else -1000)], + icon_fn=lambda x: bandwidth_icon((8, 15), x), + entity_category=EntityCategory.DIAGNOSTIC, + ), + "ulfrequency": HuaweiSensorEntityDescription( + key="ulfrequency", + name="Uplink frequency", + device_class=SensorDeviceClass.FREQUENCY, entity_category=EntityCategory.DIAGNOSTIC, ), } @@ -367,15 +397,29 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { }, ), KEY_MONITORING_MONTH_STATISTICS: HuaweiSensorGroup( - exclude=re.compile(r"^month(duration|lastcleartime)$", re.IGNORECASE), + exclude=re.compile( + r"^(currentday|month)(duration|lastcleartime)$", re.IGNORECASE + ), descriptions={ + "CurrentDayUsed": HuaweiSensorEntityDescription( + key="CurrentDayUsed", + name="Current day transfer", + native_unit_of_measurement=UnitOfInformation.BYTES, + device_class=SensorDeviceClass.DATA_SIZE, + icon="mdi:arrow-up-down-bold", + state_class=SensorStateClass.TOTAL, + last_reset_item="CurrentDayDuration", + last_reset_format_fn=format_last_reset_elapsed_seconds, + ), "CurrentMonthDownload": HuaweiSensorEntityDescription( key="CurrentMonthDownload", name="Current month download", native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:download", - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, + last_reset_item="MonthDuration", + last_reset_format_fn=format_last_reset_elapsed_seconds, ), "CurrentMonthUpload": HuaweiSensorEntityDescription( key="CurrentMonthUpload", @@ -383,7 +427,9 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { native_unit_of_measurement=UnitOfInformation.BYTES, device_class=SensorDeviceClass.DATA_SIZE, icon="mdi:upload", - state_class=SensorStateClass.TOTAL_INCREASING, + state_class=SensorStateClass.TOTAL, + last_reset_item="MonthDuration", + last_reset_format_fn=format_last_reset_elapsed_seconds, ), }, ), @@ -521,8 +567,8 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "State": HuaweiSensorEntityDescription( key="State", name="Operator search mode", - formatter=lambda x: ( - {"0": "Auto", "1": "Manual"}.get(x, "Unknown"), + format_fn=lambda x: ( + {"0": "Auto", "1": "Manual"}.get(x), None, ), entity_category=EntityCategory.CONFIG, @@ -535,16 +581,16 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = { "NetworkMode": HuaweiSensorEntityDescription( key="NetworkMode", name="Preferred mode", - formatter=lambda x: ( + format_fn=lambda x: ( { - "00": "4G/3G/2G", - "01": "2G", - "02": "3G", - "03": "4G", - "0301": "4G/2G", - "0302": "4G/3G", - "0201": "3G/2G", - }.get(x, "Unknown"), + NetworkModeEnum.MODE_AUTO.value: "4G/3G/2G", + NetworkModeEnum.MODE_4G_3G_AUTO.value: "4G/3G", + NetworkModeEnum.MODE_4G_2G_AUTO.value: "4G/2G", + NetworkModeEnum.MODE_4G_ONLY.value: "4G", + NetworkModeEnum.MODE_3G_2G_AUTO.value: "3G/2G", + NetworkModeEnum.MODE_3G_ONLY.value: "3G", + NetworkModeEnum.MODE_2G_ONLY.value: "2G", + }.get(x), None, ), entity_category=EntityCategory.CONFIG, @@ -627,7 +673,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up from config entry.""" - router = hass.data[DOMAIN].routers[config_entry.unique_id] + router = hass.data[DOMAIN].routers[config_entry.entry_id] sensors: list[Entity] = [] for key in SENSOR_KEYS: if not (items := router.data.get(key)): @@ -660,8 +706,9 @@ class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity): item: str entity_description: HuaweiSensorEntityDescription - _state: StateType = field(default=STATE_UNKNOWN, init=False) + _state: StateType = field(default=None, init=False) _unit: str | None = field(default=None, init=False) + _last_reset: datetime | None = field(default=None, init=False) def __post_init__(self) -> None: """Initialize remaining attributes.""" @@ -671,11 +718,19 @@ class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity): """Subscribe to needed data on add.""" await super().async_added_to_hass() self.router.subscriptions[self.key].add(f"{SENSOR_DOMAIN}/{self.item}") + if self.entity_description.last_reset_item: + self.router.subscriptions[self.key].add( + f"{SENSOR_DOMAIN}/{self.entity_description.last_reset_item}" + ) async def async_will_remove_from_hass(self) -> None: """Unsubscribe from needed data on remove.""" await super().async_will_remove_from_hass() self.router.subscriptions[self.key].remove(f"{SENSOR_DOMAIN}/{self.item}") + if self.entity_description.last_reset_item: + self.router.subscriptions[self.key].remove( + f"{SENSOR_DOMAIN}/{self.entity_description.last_reset_item}" + ) @property def _device_unique_id(self) -> str: @@ -698,6 +753,19 @@ class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity): return self.entity_description.icon_fn(self.state) return self.entity_description.icon + @property + def device_class(self) -> SensorDeviceClass | None: + """Return device class for sensor.""" + if self.entity_description.device_class_fn: + # Note: using self.state could infloop here. + return self.entity_description.device_class_fn(self.native_value) + return super().device_class + + @property + def last_reset(self) -> datetime | None: + """Return the time when the sensor was last reset, if any.""" + return self._last_reset + async def async_update(self) -> None: """Update state.""" try: @@ -706,7 +774,26 @@ class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity): _LOGGER.debug("%s[%s] not in data", self.key, self.item) value = None - formatter = self.entity_description.formatter + last_reset = None + if ( + self.entity_description.last_reset_item + and self.entity_description.last_reset_format_fn + ): + try: + last_reset_value = self.router.data[self.key][ + self.entity_description.last_reset_item + ] + except KeyError: + _LOGGER.debug( + "%s[%s] not in data", + self.key, + self.entity_description.last_reset_item, + ) + else: + last_reset = self.entity_description.last_reset_format_fn( + last_reset_value + ) - self._state, self._unit = formatter(value) + self._state, self._unit = self.entity_description.format_fn(value) + self._last_reset = last_reset self._available = value is not None diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 3875433888d..0eb68c959ac 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unsupported_device": "Unsupported device" }, diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index 261b77987cf..2fe064d6300 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -31,7 +31,7 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up from config entry.""" - router = hass.data[DOMAIN].routers[config_entry.unique_id] + router = hass.data[DOMAIN].routers[config_entry.entry_id] switches: list[Entity] = [] if router.data.get(KEY_DIALUP_MOBILE_DATASWITCH): diff --git a/homeassistant/components/huawei_lte/translations/bg.json b/homeassistant/components/huawei_lte/translations/bg.json index 2ecb9564113..0403055e24b 100644 --- a/homeassistant/components/huawei_lte/translations/bg.json +++ b/homeassistant/components/huawei_lte/translations/bg.json @@ -1,8 +1,9 @@ { "config": { "abort": { - "not_huawei_lte": "\u041d\u0435 \u0435 Huawei LTE \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", - "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" + "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\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", + "unsupported_device": "\u041d\u0435\u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, "error": { "connection_timeout": "\u0412\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0438\u0437\u0442\u0435\u0447\u0435", diff --git a/homeassistant/components/huawei_lte/translations/ca.json b/homeassistant/components/huawei_lte/translations/ca.json index 7c872862488..dea23f8d789 100644 --- a/homeassistant/components/huawei_lte/translations/ca.json +++ b/homeassistant/components/huawei_lte/translations/ca.json @@ -1,8 +1,10 @@ { "config": { "abort": { - "not_huawei_lte": "No \u00e9s un dispositiu Huawei LTE", - "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "unsupported_device": "Dispositiu no compatible" }, "error": { "connection_timeout": "S'ha acabat el temps d'espera de la connexi\u00f3", diff --git a/homeassistant/components/huawei_lte/translations/cs.json b/homeassistant/components/huawei_lte/translations/cs.json index e5518d722ca..a14674009a0 100644 --- a/homeassistant/components/huawei_lte/translations/cs.json +++ b/homeassistant/components/huawei_lte/translations/cs.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "Nejedn\u00e1 se o za\u0159\u00edzen\u00ed Huawei LTE", "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { diff --git a/homeassistant/components/huawei_lte/translations/da.json b/homeassistant/components/huawei_lte/translations/da.json index 2b1f937b6be..d8f5f90a5e2 100644 --- a/homeassistant/components/huawei_lte/translations/da.json +++ b/homeassistant/components/huawei_lte/translations/da.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "not_huawei_lte": "Ikke en Huawei LTE-enhed" - }, "error": { "connection_timeout": "Timeout for forbindelse", "incorrect_password": "Forkert adgangskode", diff --git a/homeassistant/components/huawei_lte/translations/de.json b/homeassistant/components/huawei_lte/translations/de.json index 8073aef0bf6..be620e3d2d4 100644 --- a/homeassistant/components/huawei_lte/translations/de.json +++ b/homeassistant/components/huawei_lte/translations/de.json @@ -1,8 +1,10 @@ { "config": { "abort": { - "not_huawei_lte": "Kein Huawei LTE-Ger\u00e4t", - "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "unsupported_device": "Nicht unterst\u00fctztes Ger\u00e4t" }, "error": { "connection_timeout": "Verbindungszeit\u00fcberschreitung", diff --git a/homeassistant/components/huawei_lte/translations/el.json b/homeassistant/components/huawei_lte/translations/el.json index f2648a50697..32abb2cd30e 100644 --- a/homeassistant/components/huawei_lte/translations/el.json +++ b/homeassistant/components/huawei_lte/translations/el.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Huawei LTE", - "reauth_successful": "\u0397 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c4\u03b1\u03c5\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2" + "reauth_successful": "\u0397 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c4\u03b1\u03c5\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2", + "unsupported_device": "\u039c\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae" }, "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", diff --git a/homeassistant/components/huawei_lte/translations/en.json b/homeassistant/components/huawei_lte/translations/en.json index 42d28a26871..08616cbc715 100644 --- a/homeassistant/components/huawei_lte/translations/en.json +++ b/homeassistant/components/huawei_lte/translations/en.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", "reauth_successful": "Re-authentication was successful", "unsupported_device": "Unsupported device" }, diff --git a/homeassistant/components/huawei_lte/translations/es-419.json b/homeassistant/components/huawei_lte/translations/es-419.json index 056b8dba886..f9c1b249f53 100644 --- a/homeassistant/components/huawei_lte/translations/es-419.json +++ b/homeassistant/components/huawei_lte/translations/es-419.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "not_huawei_lte": "No es un dispositivo Huawei LTE" - }, "error": { "connection_timeout": "El tiempo de conexi\u00f3n expir\u00f3", "incorrect_password": "Contrase\u00f1a incorrecta", diff --git a/homeassistant/components/huawei_lte/translations/es.json b/homeassistant/components/huawei_lte/translations/es.json index af2a155d5e5..39a84aa7013 100644 --- a/homeassistant/components/huawei_lte/translations/es.json +++ b/homeassistant/components/huawei_lte/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "No es un dispositivo Huawei LTE", - "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", + "unsupported_device": "Dispositivo no compatible" }, "error": { "connection_timeout": "Tiempo de espera de la conexi\u00f3n superado", diff --git a/homeassistant/components/huawei_lte/translations/et.json b/homeassistant/components/huawei_lte/translations/et.json index 82c36cf54d9..576ff505493 100644 --- a/homeassistant/components/huawei_lte/translations/et.json +++ b/homeassistant/components/huawei_lte/translations/et.json @@ -1,8 +1,10 @@ { "config": { "abort": { - "not_huawei_lte": "Pole Huawei LTE seade", - "reauth_successful": "Taastuvastamine \u00f5nnestus" + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "unsupported_device": "Seadet ei toetata" }, "error": { "connection_timeout": "\u00dchenduse ajal\u00f5pp", diff --git a/homeassistant/components/huawei_lte/translations/fr.json b/homeassistant/components/huawei_lte/translations/fr.json index e6cddfe0063..e1464a13f91 100644 --- a/homeassistant/components/huawei_lte/translations/fr.json +++ b/homeassistant/components/huawei_lte/translations/fr.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "Pas un appareil Huawei LTE", "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { diff --git a/homeassistant/components/huawei_lte/translations/hu.json b/homeassistant/components/huawei_lte/translations/hu.json index 34653dcfb72..6d26b1bdac2 100644 --- a/homeassistant/components/huawei_lte/translations/hu.json +++ b/homeassistant/components/huawei_lte/translations/hu.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Nem Huawei LTE eszk\u00f6z", - "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "unsupported_device": "Nem t\u00e1mogatott eszk\u00f6z" }, "error": { "connection_timeout": "Kapcsolat id\u0151t\u00fall\u00e9p\u00e9s", diff --git a/homeassistant/components/huawei_lte/translations/id.json b/homeassistant/components/huawei_lte/translations/id.json index d87b2bba339..1d6ef0204b9 100644 --- a/homeassistant/components/huawei_lte/translations/id.json +++ b/homeassistant/components/huawei_lte/translations/id.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Bukan perangkat Huawei LTE", - "reauth_successful": "Autentikasi ulang berhasil" + "reauth_successful": "Autentikasi ulang berhasil", + "unsupported_device": "Perangkat tidak didukung" }, "error": { "connection_timeout": "Tenggang waktu terhubung habis", diff --git a/homeassistant/components/huawei_lte/translations/it.json b/homeassistant/components/huawei_lte/translations/it.json index 9db6aea063a..232812f0eda 100644 --- a/homeassistant/components/huawei_lte/translations/it.json +++ b/homeassistant/components/huawei_lte/translations/it.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Non \u00e8 un dispositivo Huawei LTE", - "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "unsupported_device": "Dispositivo non supportato" }, "error": { "connection_timeout": "Timeout di connessione", diff --git a/homeassistant/components/huawei_lte/translations/ja.json b/homeassistant/components/huawei_lte/translations/ja.json index 25cf9d1b0e8..5983a9e1032 100644 --- a/homeassistant/components/huawei_lte/translations/ja.json +++ b/homeassistant/components/huawei_lte/translations/ja.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "Huawei LTE\u30c7\u30d0\u30a4\u30b9\u3067\u306f\u3042\u308a\u307e\u305b\u3093", "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { diff --git a/homeassistant/components/huawei_lte/translations/ko.json b/homeassistant/components/huawei_lte/translations/ko.json index 0db0afb11bc..83261ccff20 100644 --- a/homeassistant/components/huawei_lte/translations/ko.json +++ b/homeassistant/components/huawei_lte/translations/ko.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "\ud654\uc6e8\uc774 LTE \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4", "reauth_successful": "\uc7ac\uc778\uc99d\uc5d0 \uc131\uacf5\ud588\uc2b5\ub2c8\ub2e4" }, "error": { diff --git a/homeassistant/components/huawei_lte/translations/lb.json b/homeassistant/components/huawei_lte/translations/lb.json index 2be64393358..e676c97d459 100644 --- a/homeassistant/components/huawei_lte/translations/lb.json +++ b/homeassistant/components/huawei_lte/translations/lb.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "not_huawei_lte": "Keen Huawei LTE Apparat" - }, "error": { "connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen", "incorrect_password": "Ong\u00ebltegt Passwuert", diff --git a/homeassistant/components/huawei_lte/translations/lt.json b/homeassistant/components/huawei_lte/translations/lt.json new file mode 100644 index 00000000000..c0035358539 --- /dev/null +++ b/homeassistant/components/huawei_lte/translations/lt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "incorrect_password": "Neteisingas slapta\u017eodis" + }, + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/translations/nl.json b/homeassistant/components/huawei_lte/translations/nl.json index 6fa91431fd0..a8914e86cef 100644 --- a/homeassistant/components/huawei_lte/translations/nl.json +++ b/homeassistant/components/huawei_lte/translations/nl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Geen Huawei LTE-apparaat", - "reauth_successful": "Herauthenticatie geslaagd" + "reauth_successful": "Herauthenticatie geslaagd", + "unsupported_device": "Niet-ondersteund apparaat" }, "error": { "connection_timeout": "Time-out van de verbinding", diff --git a/homeassistant/components/huawei_lte/translations/no.json b/homeassistant/components/huawei_lte/translations/no.json index 4261d2af9b2..b163fb662c4 100644 --- a/homeassistant/components/huawei_lte/translations/no.json +++ b/homeassistant/components/huawei_lte/translations/no.json @@ -1,8 +1,10 @@ { "config": { "abort": { - "not_huawei_lte": "Ikke en Huawei LTE-enhet", - "reauth_successful": "Re-autentisering var vellykket" + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "reauth_successful": "Re-autentisering var vellykket", + "unsupported_device": "Enhet som ikke st\u00f8ttes" }, "error": { "connection_timeout": "Tilkoblingsavbrudd", diff --git a/homeassistant/components/huawei_lte/translations/pl.json b/homeassistant/components/huawei_lte/translations/pl.json index 1f66183a762..c1445048213 100644 --- a/homeassistant/components/huawei_lte/translations/pl.json +++ b/homeassistant/components/huawei_lte/translations/pl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "To nie jest urz\u0105dzenie Huawei LTE", - "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "unsupported_device": "Nieobs\u0142ugiwane urz\u0105dzenie" }, "error": { "connection_timeout": "Przekroczono limit czasu pr\u00f3by po\u0142\u0105czenia", diff --git a/homeassistant/components/huawei_lte/translations/pt-BR.json b/homeassistant/components/huawei_lte/translations/pt-BR.json index d10fb60a013..5d8bc54ce04 100644 --- a/homeassistant/components/huawei_lte/translations/pt-BR.json +++ b/homeassistant/components/huawei_lte/translations/pt-BR.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "N\u00e3o \u00e9 um dispositivo Huawei LTE", - "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "unsupported_device": "Dispositivo n\u00e3o suportado" }, "error": { "connection_timeout": "Tempo limite de conex\u00e3o atingido", diff --git a/homeassistant/components/huawei_lte/translations/ru.json b/homeassistant/components/huawei_lte/translations/ru.json index feb6209cc81..d6ace6e8cea 100644 --- a/homeassistant/components/huawei_lte/translations/ru.json +++ b/homeassistant/components/huawei_lte/translations/ru.json @@ -1,8 +1,10 @@ { "config": { "abort": { - "not_huawei_lte": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Huawei LTE", - "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." + "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.", + "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.", + "unsupported_device": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e." }, "error": { "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", diff --git a/homeassistant/components/huawei_lte/translations/sk.json b/homeassistant/components/huawei_lte/translations/sk.json index 0b9cdc4f5c4..09a35e161bd 100644 --- a/homeassistant/components/huawei_lte/translations/sk.json +++ b/homeassistant/components/huawei_lte/translations/sk.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Nie je to zariadenie Huawei LTE", - "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "unsupported_device": "Nepodporovan\u00e9 zariadenie" }, "error": { "connection_timeout": "\u010casov\u00fd limit pripojenia", diff --git a/homeassistant/components/huawei_lte/translations/sl.json b/homeassistant/components/huawei_lte/translations/sl.json index 0297db82c7b..3607fc81fce 100644 --- a/homeassistant/components/huawei_lte/translations/sl.json +++ b/homeassistant/components/huawei_lte/translations/sl.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "not_huawei_lte": "Ni naprava Huawei LTE" - }, "error": { "connection_timeout": "\u010casovna omejitev povezave", "incorrect_password": "Nepravilno geslo", diff --git a/homeassistant/components/huawei_lte/translations/sv.json b/homeassistant/components/huawei_lte/translations/sv.json index 96317c05545..6ba2eb684ff 100644 --- a/homeassistant/components/huawei_lte/translations/sv.json +++ b/homeassistant/components/huawei_lte/translations/sv.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "Inte en Huawei LTE-enhet", "reauth_successful": "\u00c5terautentisering lyckades" }, "error": { diff --git a/homeassistant/components/huawei_lte/translations/tr.json b/homeassistant/components/huawei_lte/translations/tr.json index c2791808f65..ef380cdc542 100644 --- a/homeassistant/components/huawei_lte/translations/tr.json +++ b/homeassistant/components/huawei_lte/translations/tr.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "not_huawei_lte": "Huawei LTE cihaz\u0131 de\u011fil", - "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "unsupported_device": "Desteklenmeyen cihaz" }, "error": { "connection_timeout": "Ba\u011flant\u0131 zamana\u015f\u0131m\u0131", diff --git a/homeassistant/components/huawei_lte/translations/uk.json b/homeassistant/components/huawei_lte/translations/uk.json index ae98ce59332..38c7e09325d 100644 --- a/homeassistant/components/huawei_lte/translations/uk.json +++ b/homeassistant/components/huawei_lte/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_huawei_lte": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0454 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c Huawei LTE" + "unsupported_device": "\u041d\u0435\u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" }, "error": { "connection_timeout": "\u0427\u0430\u0441 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043c\u0438\u043d\u0443\u0432.", @@ -15,6 +15,12 @@ }, "flow_title": "Huawei LTE: {name}", "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/huawei_lte/translations/zh-Hans.json b/homeassistant/components/huawei_lte/translations/zh-Hans.json index e229f8f28a4..3fac021b63d 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hans.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hans.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "not_huawei_lte": "\u8be5\u8bbe\u5907\u4e0d\u662f\u534e\u4e3a LTE \u8bbe\u5907", "reauth_successful": "\u91cd\u65b0\u8ba4\u8bc1\u6210\u529f" }, "error": { diff --git a/homeassistant/components/huawei_lte/translations/zh-Hant.json b/homeassistant/components/huawei_lte/translations/zh-Hant.json index df014095c90..18d89a7a0db 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hant.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hant.json @@ -1,8 +1,10 @@ { "config": { "abort": { - "not_huawei_lte": "\u4e26\u975e\u83ef\u70ba LTE \u88dd\u7f6e", - "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "unsupported_device": "\u4e0d\u652f\u63f4\u7684\u88dd\u7f6e" }, "error": { "connection_timeout": "\u9023\u7dda\u903e\u6642", diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 625a623105f..c39fbed180c 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -72,7 +72,7 @@ class HueBridge: async def async_initialize_bridge(self) -> bool: """Initialize Connection with the Hue API.""" try: - with async_timeout.timeout(10): + async with async_timeout.timeout(10): await self.api.initialize() except (LinkButtonNotPressed, Unauthorized): diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 3df17baad16..d87da5b5ac0 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -4,7 +4,6 @@ from __future__ import annotations import asyncio import logging from typing import Any -from urllib.parse import urlparse import aiohttp from aiohue import LinkButtonNotPressed, create_app_key @@ -15,7 +14,7 @@ import slugify as unicode_slug import voluptuous as vol from homeassistant import config_entries -from homeassistant.components import ssdp, zeroconf +from homeassistant.components import zeroconf from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult @@ -201,53 +200,6 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: - """Handle a discovered Hue bridge. - - This flow is triggered by the SSDP component. It will check if the - host is already configured and delegate to the import step if not. - """ - # Filter out non-Hue bridges #1 - if ( - discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER_URL) - not in HUE_MANUFACTURERURL - ): - return self.async_abort(reason="not_hue_bridge") - - # Filter out non-Hue bridges #2 - if any( - name in discovery_info.upnp.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "") - for name in HUE_IGNORED_BRIDGE_NAMES - ): - return self.async_abort(reason="not_hue_bridge") - - if ( - not discovery_info.ssdp_location - or ssdp.ATTR_UPNP_SERIAL not in discovery_info.upnp - ): - return self.async_abort(reason="not_hue_bridge") - - url = urlparse(discovery_info.ssdp_location) - if not url.hostname: - return self.async_abort(reason="not_hue_bridge") - - # Ignore if host is IPv6 - if is_ipv6_address(url.hostname): - return self.async_abort(reason="invalid_host") - - # abort if we already have exactly this bridge id/host - # reload the integration if the host got updated - bridge_id = normalize_bridge_id(discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL]) - await self.async_set_unique_id(bridge_id) - self._abort_if_unique_id_configured( - updates={CONF_HOST: url.hostname}, reload_on_update=True - ) - - self.bridge = await self._get_bridge( - url.hostname, discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] - ) - return await self.async_step_link() - async def async_step_zeroconf( self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index d726f773b9b..efe499357fb 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,21 +3,7 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": ["aiohue==4.5.0"], - "ssdp": [ - { - "manufacturer": "Royal Philips Electronics", - "modelName": "Philips hue bridge 2012" - }, - { - "manufacturer": "Royal Philips Electronics", - "modelName": "Philips hue bridge 2015" - }, - { - "manufacturer": "Signify", - "modelName": "Philips hue bridge 2015" - } - ], + "requirements": ["aiohue==4.6.1"], "homekit": { "models": ["BSB002"] }, diff --git a/homeassistant/components/hue/scene.py b/homeassistant/components/hue/scene.py index 5b0c17ebbf4..b93d8f76fed 100644 --- a/homeassistant/components/hue/scene.py +++ b/homeassistant/components/hue/scene.py @@ -47,10 +47,10 @@ async def async_setup_entry( @callback def async_add_entity(event_type: EventType, resource: HueScene) -> None: """Add entity from Hue resource.""" - async_add_entities([HueSceneEntity(bridge, api.scenes, resource)]) + async_add_entities([HueSceneEntity(bridge, api.scenes.scene, resource)]) # add all current items in controller - for item in api.scenes: + for item in api.scenes.scene: async_add_entity(EventType.RESOURCE_ADDED, item) # register listener for new items only diff --git a/homeassistant/components/hue/switch.py b/homeassistant/components/hue/switch.py index 7fb40cba38f..936b91aeef8 100644 --- a/homeassistant/components/hue/switch.py +++ b/homeassistant/components/hue/switch.py @@ -1,7 +1,7 @@ """Support for switch platform for Hue resources (V2 only).""" from __future__ import annotations -from typing import Any, Union +from typing import Any, TypeAlias from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType @@ -22,9 +22,9 @@ from .bridge import HueBridge from .const import DOMAIN from .v2.entity import HueBaseEntity -ControllerType = Union[LightLevelController, MotionController] +ControllerType: TypeAlias = LightLevelController | MotionController -SensingService = Union[LightLevel, Motion] +SensingService: TypeAlias = LightLevel | Motion async def async_setup_entry( diff --git a/homeassistant/components/hue/translations/el.json b/homeassistant/components/hue/translations/el.json index 5641ef04add..7336f12d267 100644 --- a/homeassistant/components/hue/translations/el.json +++ b/homeassistant/components/hue/translations/el.json @@ -55,15 +55,15 @@ }, "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", + "initial_press": "\"{subtype}\" \u03c0\u03b9\u03ad\u03c3\u03c4\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_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", + "remote_button_short_press": "\u03a0\u03b1\u03c4\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \"{subtype}\"", + "remote_button_short_release": "\u0391\u03c0\u03b5\u03bb\u03b5\u03c5\u03b8\u03b5\u03c1\u03ce\u03b8\u03b7\u03ba\u03b5 \u03c4\u03bf \"{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", + "short_release": "\u03a4\u03bf \"{subtype}\" \u03b1\u03c6\u03ad\u03b8\u03b7\u03ba\u03b5 \u03bc\u03b5\u03c4\u03ac \u03b1\u03c0\u03cc \u03c3\u03cd\u03bd\u03c4\u03bf\u03bc\u03bf \u03c0\u03ac\u03c4\u03b7\u03bc\u03b1", "start": "\"{subtype}\" \u03c0\u03b9\u03ad\u03c3\u03c4\u03b7\u03ba\u03b5 \u03b1\u03c1\u03c7\u03b9\u03ba\u03ac" } }, diff --git a/homeassistant/components/hue/translations/hu.json b/homeassistant/components/hue/translations/hu.json index 32f52c47a7b..63704ecdd73 100644 --- a/homeassistant/components/hue/translations/hu.json +++ b/homeassistant/components/hue/translations/hu.json @@ -23,7 +23,7 @@ "title": "V\u00e1lasszon Hue bridge-t" }, "link": { - "description": "Nyomja meg a gombot a bridge-en a Philips Hue Home Assistantban val\u00f3 regisztr\u00e1l\u00e1s\u00e1hoz.\n\n![Gomb helye](/static/images/config_philips_hue.jpg)", + "description": "Nyomja meg a gombot a bridge-en a Philips Hue Home Assistantban val\u00f3 regisztr\u00e1l\u00e1s\u00e1hoz.\n\n![Gomb elhelyez\u00e9se](/static/images/config_philips_hue.jpg)", "title": "Kapcsol\u00f3d\u00e1s a hubhoz" }, "manual": { diff --git a/homeassistant/components/hue/translations/lt.json b/homeassistant/components/hue/translations/lt.json index 6838078d19d..74e36f8c4dc 100644 --- a/homeassistant/components/hue/translations/lt.json +++ b/homeassistant/components/hue/translations/lt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u012erenginys jau sukonfig\u016bruotas" + }, "step": { "init": { "data": { diff --git a/homeassistant/components/hue/translations/lv.json b/homeassistant/components/hue/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/hue/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/nl.json b/homeassistant/components/hue/translations/nl.json index 57c52c4156f..501e27f2756 100644 --- a/homeassistant/components/hue/translations/nl.json +++ b/homeassistant/components/hue/translations/nl.json @@ -44,6 +44,8 @@ "button_2": "Tweede knop", "button_3": "Derde knop", "button_4": "Vierde knop", + "clock_wise": "Rotatie met de klok mee", + "counter_clock_wise": "Rotatie tegen de klok in", "dim_down": "Dim omlaag", "dim_up": "Dim omhoog", "double_buttons_1_3": "Eerste en derde knop", @@ -61,7 +63,8 @@ "remote_double_button_long_press": "Beide \"{subtype}\" losgelaten na lang indrukken", "remote_double_button_short_press": "Beide \"{subtype}\" losgelaten", "repeat": "Knop \" {subtype} \" ingedrukt gehouden", - "short_release": "Knop \"{subtype}\" losgelaten na kort indrukken" + "short_release": "Knop \"{subtype}\" losgelaten na kort indrukken", + "start": "\"{subtype}\" aanvankelijk ingedrukt" } }, "options": { diff --git a/homeassistant/components/hue/translations/sk.json b/homeassistant/components/hue/translations/sk.json index 46bddda61db..3cf82424254 100644 --- a/homeassistant/components/hue/translations/sk.json +++ b/homeassistant/components/hue/translations/sk.json @@ -54,15 +54,15 @@ "turn_on": "Zapn\u00fa\u0165" }, "trigger_type": { - "double_short_release": "Obe \u201e{subtype}\u201c boli uvo\u013enen\u00e9", + "double_short_release": "Obe \"{subtype}\" boli uvo\u013enen\u00e9", "initial_press": "\"{subtype}\" stla\u010den\u00fd na za\u010diatku", "long_release": "\"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed", "remote_button_long_release": "\"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed", "remote_button_short_press": "\"{subtype}\" stla\u010den\u00e9", - "remote_button_short_release": "\u201c{subtype}\u201c uvo\u013enen\u00e9", + "remote_button_short_release": "\"{subtype}\" uvo\u013enen\u00e9", "remote_double_button_long_press": "Obe \"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed", - "remote_double_button_short_press": "Obe \u201e{subtype}\u201c boli uvo\u013enen\u00e9", - "repeat": "\u201e{subtype}\u201c podr\u017ean\u00e9", + "remote_double_button_short_press": "Obe \"{subtype}\" boli uvo\u013enen\u00e9", + "repeat": "\"{subtype}\" podr\u017ean\u00e9", "short_release": "\"{subtype}\" uvo\u013enen\u00e9 po kr\u00e1tkom stla\u010den\u00ed", "start": "\"{subtype}\" stla\u010den\u00fd na za\u010diatku" } diff --git a/homeassistant/components/hue/translations/sv.json b/homeassistant/components/hue/translations/sv.json index 1707e345983..47c8cc9ca52 100644 --- a/homeassistant/components/hue/translations/sv.json +++ b/homeassistant/components/hue/translations/sv.json @@ -54,16 +54,16 @@ "turn_on": "Starta" }, "trigger_type": { - "double_short_release": "B\u00e5da \"{subtyp}\" sl\u00e4pptes", - "initial_press": "Knappen \" {subtype} \" trycktes f\u00f6rst", - "long_release": "Knappen \" {subtype} \" sl\u00e4pps efter l\u00e5ng tryckning", + "double_short_release": "B\u00e5da \"{subtype}\" sl\u00e4pptes", + "initial_press": "\"{subtype}\" trycktes ned", + "long_release": "\"{subtype}\" sl\u00e4pps efter l\u00e5ng tryckning", "remote_button_long_release": "\"{subtype}\" knappen sl\u00e4pptes efter ett l\u00e5ngt tryck", "remote_button_short_press": "\"{subtype}\" knappen nedtryckt", "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt", "remote_double_button_long_press": "B\u00e5da \"{subtype}\" sl\u00e4pptes efter en l\u00e5ngtryckning", "remote_double_button_short_press": "B\u00e5da \"{subtyp}\" sl\u00e4pptes", - "repeat": "Knappen \" {subtype} \" h\u00f6lls nedtryckt", - "short_release": "Knappen \" {subtype} \" sl\u00e4pps efter kort tryckning", + "repeat": "\"{subtype}\" h\u00e5lls nedtryckt", + "short_release": "\"{subtype}\" sl\u00e4pps efter kort tryckning", "start": "\" {subtype} \" trycktes f\u00f6rst" } }, diff --git a/homeassistant/components/hue/translations/uk.json b/homeassistant/components/hue/translations/uk.json index 8e9c5ca82cb..0a0b7a4bd1e 100644 --- a/homeassistant/components/hue/translations/uk.json +++ b/homeassistant/components/hue/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "all_configured": "\u0412\u0441\u0456 \u0448\u043b\u044e\u0437\u0438 Philips Hue \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0456.", + "all_configured": "UPnP/IGD \u0432\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0454\u043d\u043e", "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", diff --git a/homeassistant/components/hue/v1/light.py b/homeassistant/components/hue/v1/light.py index e840835764b..f0ba0dbac23 100644 --- a/homeassistant/components/hue/v1/light.py +++ b/homeassistant/components/hue/v1/light.py @@ -262,7 +262,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_safe_fetch(bridge, fetch_method): """Safely fetch data.""" try: - with async_timeout.timeout(4): + async with async_timeout.timeout(4): return await bridge.async_request_call(fetch_method) except aiohue.Unauthorized as err: await bridge.handle_unauthorized_error() diff --git a/homeassistant/components/hue/v2/binary_sensor.py b/homeassistant/components/hue/v2/binary_sensor.py index a7077ccf765..0a8f50b8b7a 100644 --- a/homeassistant/components/hue/v2/binary_sensor.py +++ b/homeassistant/components/hue/v2/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Hue binary sensors.""" from __future__ import annotations -from typing import Any, Union +from typing import Any, TypeAlias from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.config import ( @@ -25,8 +25,8 @@ from ..bridge import HueBridge from ..const import DOMAIN from .entity import HueBaseEntity -SensorType = Union[Motion, EntertainmentConfiguration] -ControllerType = Union[MotionController, EntertainmentConfigurationController] +SensorType: TypeAlias = Motion | EntertainmentConfiguration +ControllerType: TypeAlias = MotionController | EntertainmentConfigurationController async def async_setup_entry( diff --git a/homeassistant/components/hue/v2/entity.py b/homeassistant/components/hue/v2/entity.py index 7335bf71058..04bb553cd36 100644 --- a/homeassistant/components/hue/v2/entity.py +++ b/homeassistant/components/hue/v2/entity.py @@ -1,7 +1,7 @@ """Generic Hue Entity Model.""" from __future__ import annotations -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING, TypeAlias from aiohue.v2.controllers.base import BaseResourcesController from aiohue.v2.controllers.events import EventType @@ -23,7 +23,7 @@ if TYPE_CHECKING: from aiohue.v2.models.light_level import LightLevel from aiohue.v2.models.motion import Motion - HueResource = Union[Light, DevicePower, GroupedLight, LightLevel, Motion] + HueResource: TypeAlias = Light | DevicePower | GroupedLight | LightLevel | Motion RESOURCE_TYPE_NAMES = { @@ -71,11 +71,11 @@ class HueBaseEntity(Entity): # creating a pretty name for device-less entities (e.g. groups/scenes) # should be handled in the platform instead return self.resource.type.value - # if resource is a light, use the name from metadata - if self.resource.type == ResourceTypes.LIGHT: - return self.resource.name - # for sensors etc, use devicename + pretty name of type dev_name = self.device.metadata.name + # if resource is a light, use the device name itself + if self.resource.type == ResourceTypes.LIGHT: + return dev_name + # for sensors etc, use devicename + pretty name of type type_title = RESOURCE_TYPE_NAMES.get( self.resource.type, self.resource.type.value.replace("_", " ").title() ) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index cbf974325e0..dd22c98f63e 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -120,7 +120,10 @@ class GroupedHueLight(HueBaseEntity, LightEntity): scenes = { x.metadata.name for x in self.api.scenes if x.group.rid == self.group.id } - lights = {x.metadata.name for x in self.controller.get_lights(self.resource.id)} + lights = { + self.controller.get_device(x.id).metadata.name + for x in self.controller.get_lights(self.resource.id) + } return { "is_hue_group": True, "hue_scenes": scenes, diff --git a/homeassistant/components/hue/v2/sensor.py b/homeassistant/components/hue/v2/sensor.py index 7fa34c59869..208873b93f9 100644 --- a/homeassistant/components/hue/v2/sensor.py +++ b/homeassistant/components/hue/v2/sensor.py @@ -1,7 +1,7 @@ """Support for Hue sensors.""" from __future__ import annotations -from typing import Any, Union +from typing import Any, TypeAlias from aiohue.v2 import HueBridgeV2 from aiohue.v2.controllers.events import EventType @@ -32,13 +32,13 @@ from ..bridge import HueBridge from ..const import DOMAIN from .entity import HueBaseEntity -SensorType = Union[DevicePower, LightLevel, Temperature, ZigbeeConnectivity] -ControllerType = Union[ - DevicePowerController, - LightLevelController, - TemperatureController, - ZigbeeConnectivityController, -] +SensorType: TypeAlias = DevicePower | LightLevel | Temperature | ZigbeeConnectivity +ControllerType: TypeAlias = ( + DevicePowerController + | LightLevelController + | TemperatureController + | ZigbeeConnectivityController +) async def async_setup_entry( @@ -79,8 +79,6 @@ async def async_setup_entry( class HueSensorBase(HueBaseEntity, SensorEntity): """Representation of a Hue sensor.""" - _attr_state_class = SensorStateClass.MEASUREMENT - def __init__( self, bridge: HueBridge, @@ -98,6 +96,7 @@ class HueTemperatureSensor(HueSensorBase): _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS _attr_device_class = SensorDeviceClass.TEMPERATURE + _attr_state_class = SensorStateClass.MEASUREMENT @property def native_value(self) -> float: @@ -115,6 +114,7 @@ class HueLightLevelSensor(HueSensorBase): _attr_native_unit_of_measurement = LIGHT_LUX _attr_device_class = SensorDeviceClass.ILLUMINANCE + _attr_state_class = SensorStateClass.MEASUREMENT @property def native_value(self) -> int: @@ -140,6 +140,7 @@ class HueBatterySensor(HueSensorBase): _attr_native_unit_of_measurement = PERCENTAGE _attr_device_class = SensorDeviceClass.BATTERY _attr_entity_category = EntityCategory.DIAGNOSTIC + _attr_state_class = SensorStateClass.MEASUREMENT @property def native_value(self) -> int: diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py index a3d8863f566..3952e4a1341 100644 --- a/homeassistant/components/huisbaasje/__init__.py +++ b/homeassistant/components/huisbaasje/__init__.py @@ -1,6 +1,7 @@ """The Huisbaasje integration.""" from datetime import timedelta import logging +from typing import Any import async_timeout from energyflip import EnergyFlip, EnergyFlipException @@ -45,7 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.error("Authentication failed: %s", str(exception)) return False - async def async_update_data(): + async def async_update_data() -> dict[str, dict[str, Any]]: return await async_update_huisbaasje(energyflip) # Create a coordinator for polling updates @@ -80,7 +81,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def async_update_huisbaasje(energyflip): +async def async_update_huisbaasje(energyflip: EnergyFlip) -> dict[str, dict[str, Any]]: """Update the data by performing a request to Huisbaasje.""" try: # Note: asyncio.TimeoutError and aiohttp.ClientError are already diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py index 7117a977380..f73d4bf3129 100644 --- a/homeassistant/components/huisbaasje/sensor.py +++ b/homeassistant/components/huisbaasje/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass import logging +from typing import Any from energyflip.const import ( SOURCE_TYPE_ELECTRICITY, @@ -234,7 +235,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor platform.""" - coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + coordinator: DataUpdateCoordinator[dict[str, dict[str, Any]]] = hass.data[DOMAIN][ + config_entry.entry_id + ][DATA_COORDINATOR] user_id = config_entry.data[CONF_ID] async_add_entities( @@ -243,14 +246,16 @@ async def async_setup_entry( ) -class HuisbaasjeSensor(CoordinatorEntity, SensorEntity): +class HuisbaasjeSensor( + CoordinatorEntity[DataUpdateCoordinator[dict[str, dict[str, Any]]]], SensorEntity +): """Defines a Huisbaasje sensor.""" entity_description: HuisbaasjeSensorEntityDescription def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[dict[str, dict[str, Any]]], user_id: str, description: HuisbaasjeSensorEntityDescription, ) -> None: @@ -278,7 +283,7 @@ class HuisbaasjeSensor(CoordinatorEntity, SensorEntity): @property def available(self) -> bool: """Return if entity is available.""" - return ( + return bool( super().available and self.coordinator.data and self._source_type in self.coordinator.data diff --git a/homeassistant/components/huisbaasje/translations/lv.json b/homeassistant/components/huisbaasje/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/uk.json b/homeassistant/components/huisbaasje/translations/uk.json new file mode 100644 index 00000000000..337e9e7fa20 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/intent.py b/homeassistant/components/humidifier/intent.py index 4d28cf5838c..d949874cc67 100644 --- a/homeassistant/components/humidifier/intent.py +++ b/homeassistant/components/humidifier/intent.py @@ -41,10 +41,18 @@ class HumidityHandler(intent.IntentHandler): """Handle the hass intent.""" hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - state = intent.async_match_state( - hass, slots["name"]["value"], hass.states.async_all(DOMAIN) + states = list( + intent.async_match_states( + hass, + name=slots["name"]["value"], + states=hass.states.async_all(DOMAIN), + ) ) + if not states: + raise intent.IntentHandleError("No entities matched") + + state = states[0] service_data = {ATTR_ENTITY_ID: state.entity_id} humidity = slots["humidity"]["value"] @@ -85,12 +93,18 @@ class SetModeHandler(intent.IntentHandler): """Handle the hass intent.""" hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - state = intent.async_match_state( - hass, - slots["name"]["value"], - hass.states.async_all(DOMAIN), + states = list( + intent.async_match_states( + hass, + name=slots["name"]["value"], + states=hass.states.async_all(DOMAIN), + ) ) + if not states: + raise intent.IntentHandleError("No entities matched") + + state = states[0] service_data = {ATTR_ENTITY_ID: state.entity_id} intent.async_test_feature(state, HumidifierEntityFeature.MODES, "modes") diff --git a/homeassistant/components/humidifier/translations/bg.json b/homeassistant/components/humidifier/translations/bg.json index 5bf60744a03..5f4ec821b72 100644 --- a/homeassistant/components/humidifier/translations/bg.json +++ b/homeassistant/components/humidifier/translations/bg.json @@ -3,6 +3,9 @@ "condition_type": { "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u043e", "is_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, + "trigger_type": { + "target_humidity_changed": "\u0416\u0435\u043b\u0430\u043d\u0430\u0442\u0430 \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442 {entity_name} \u043f\u0440\u043e\u043c\u0435\u043d\u0435\u043d\u0430" } }, "state": { diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py index 1666db27d86..7c9bdfcf244 100644 --- a/homeassistant/components/hunterdouglas_powerview/config_flow.py +++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py @@ -97,9 +97,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle zeroconf discovery.""" self.discovered_ip = discovery_info.host - name = discovery_info.name - if name.endswith(POWERVIEW_SUFFIX): - name = name[: -len(POWERVIEW_SUFFIX)] + name = discovery_info.name.removesuffix(POWERVIEW_SUFFIX) self.discovered_name = name return await self.async_step_discovery_confirm() @@ -108,9 +106,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle HomeKit discovery.""" self.discovered_ip = discovery_info.host - name = discovery_info.name - if name.endswith(HAP_SUFFIX): - name = name[: -len(HAP_SUFFIX)] + name = discovery_info.name.removesuffix(HAP_SUFFIX) self.discovered_name = name return await self.async_step_discovery_confirm() diff --git a/homeassistant/components/hunterdouglas_powerview/translations/lv.json b/homeassistant/components/hunterdouglas_powerview/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/tr.json b/homeassistant/components/hunterdouglas_powerview/translations/tr.json index c489b2906d8..a303af6bda7 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/tr.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/tr.json @@ -10,7 +10,7 @@ "flow_title": "{name} ({host})", "step": { "link": { - "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?", + "description": "{name} ( {host} ) kurmak istiyor musunuz?", "title": "PowerView Hub'a ba\u011flan\u0131n" }, "user": { diff --git a/homeassistant/components/hvv_departures/translations/lt.json b/homeassistant/components/hvv_departures/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/hvv_departures/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/lv.json b/homeassistant/components/hvv_departures/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/hvv_departures/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/camera.py b/homeassistant/components/hyperion/camera.py index a9d78a256d6..0366816ef1a 100644 --- a/homeassistant/components/hyperion/camera.py +++ b/homeassistant/components/hyperion/camera.py @@ -165,7 +165,7 @@ class HyperionCamera(Camera): async with self._image_cond: try: self._image = base64.b64decode( - img_data[len(IMAGE_STREAM_JPG_SENTINEL) :] + img_data.removeprefix(IMAGE_STREAM_JPG_SENTINEL) ) except binascii.Error: return diff --git a/homeassistant/components/hyperion/translations/sk.json b/homeassistant/components/hyperion/translations/sk.json index 39c8ef5b4ef..df8b7214c37 100644 --- a/homeassistant/components/hyperion/translations/sk.json +++ b/homeassistant/components/hyperion/translations/sk.json @@ -27,7 +27,7 @@ "title": "Potvr\u010fte pridanie slu\u017eby Hyperion Ambilight" }, "create_token": { - "description": "Ak chcete po\u017eiada\u0165 o nov\u00fd overovac\u00ed token, ni\u017e\u0161ie vyberte mo\u017enos\u0165 **Odosla\u0165**. Budete presmerovan\u00ed do pou\u017e\u00edvate\u013esk\u00e9ho rozhrania Hyperion, aby ste schv\u00e1lili po\u017eiadavku. Overte, \u017ee zobrazen\u00e9 ID je \u201e{auth_id}\u201c", + "description": "Ak chcete po\u017eiada\u0165 o nov\u00fd overovac\u00ed token, ni\u017e\u0161ie vyberte mo\u017enos\u0165 **Odosla\u0165**. Budete presmerovan\u00ed do pou\u017e\u00edvate\u013esk\u00e9ho rozhrania Hyperion, aby ste schv\u00e1lili po\u017eiadavku. Overte, \u017ee zobrazen\u00e9 ID je \"{auth_id}\"", "title": "Automaticky vytvori\u0165 nov\u00fd autentifika\u010dn\u00fd token" }, "create_token_external": { diff --git a/homeassistant/components/ialarm/translations/lv.json b/homeassistant/components/ialarm/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/ialarm/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 1bd9d7d72cf..146e2c2babc 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -5,7 +5,7 @@ import asyncio from collections.abc import Awaitable, Callable, Coroutine from functools import wraps import logging -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar import aiohttp.client_exceptions from iaqualink.client import AqualinkClient @@ -18,7 +18,6 @@ from iaqualink.device import ( AqualinkThermostat, ) from iaqualink.exception import AqualinkServiceException -from typing_extensions import Concatenate, ParamSpec from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN diff --git a/homeassistant/components/iaqualink/translations/lt.json b/homeassistant/components/iaqualink/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/iaqualink/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ibeacon/manifest.json b/homeassistant/components/ibeacon/manifest.json index ade53491a4c..86e7b833b69 100644 --- a/homeassistant/components/ibeacon/manifest.json +++ b/homeassistant/components/ibeacon/manifest.json @@ -2,7 +2,7 @@ "domain": "ibeacon", "name": "iBeacon Tracker", "documentation": "https://www.home-assistant.io/integrations/ibeacon", - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [2, 21] }], "requirements": ["ibeacon_ble==1.0.1"], "codeowners": ["@bdraco"], diff --git a/homeassistant/components/icloud/translations/lt.json b/homeassistant/components/icloud/translations/lt.json new file mode 100644 index 00000000000..883b5c03e2c --- /dev/null +++ b/homeassistant/components/icloud/translations/lt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis", + "username": "El. pa\u0161tas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/sk.json b/homeassistant/components/icloud/translations/sk.json index 6c58bf6984a..e0abfe99daa 100644 --- a/homeassistant/components/icloud/translations/sk.json +++ b/homeassistant/components/icloud/translations/sk.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd", - "no_device": "\u017diadne z va\u0161ich zariaden\u00ed nem\u00e1 aktivovan\u00fa funkciu \u201eN\u00e1js\u0165 m\u00f4j iPhone\u201c.", + "no_device": "\u017diadne z va\u0161ich zariaden\u00ed nem\u00e1 aktivovan\u00fa funkciu \"N\u00e1js\u0165 m\u00f4j iPhone\".", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { diff --git a/homeassistant/components/icloud/translations/uk.json b/homeassistant/components/icloud/translations/uk.json index cd416b8058c..8062d927631 100644 --- a/homeassistant/components/icloud/translations/uk.json +++ b/homeassistant/components/icloud/translations/uk.json @@ -11,6 +11,12 @@ "validate_verification_code": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0438\u0442\u0438 \u043a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f, \u0432\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0434\u043e\u0432\u0456\u0440\u0435\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0442\u0430 \u043f\u043e\u0447\u043d\u0456\u0442\u044c \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0443 \u0437\u043d\u043e\u0432\u0443." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0412\u0430\u0448 \u0440\u0430\u043d\u0456\u0448\u0435 \u0432\u0432\u0435\u0434\u0435\u043d\u0438\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username} \u0431\u0456\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u0440\u0430\u0446\u044e\u0454. \u041e\u043d\u043e\u0432\u0456\u0442\u044c \u0441\u0432\u0456\u0439 \u043f\u0430\u0440\u043e\u043b\u044c, \u0449\u043e\u0431 \u0456 \u043d\u0430\u0434\u0430\u043b\u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0446\u044e \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e." + }, "trusted_device": { "data": { "trusted_device": "\u0414\u043e\u0432\u0456\u0440\u0435\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" diff --git a/homeassistant/components/ifttt/translations/hu.json b/homeassistant/components/ifttt/translations/hu.json index 21bfa86dcec..3e0d339a66f 100644 --- a/homeassistant/components/ifttt/translations/hu.json +++ b/homeassistant/components/ifttt/translations/hu.json @@ -6,7 +6,7 @@ "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." }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, akkor az [IFTTT Webhook applet]({applet_url}) \"Make a web request\" m\u0171velet\u00e9t kell haszn\u00e1lnia. \n\nT\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\nL\u00e1sd [a dokument\u00e1ci\u00f3]({docs_url}), hogyan konfigur\u00e1lhatja az automatizmusokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni Home Assistantba, akkor az [IFTTT Webhook applet]({applet_url}) \"Make a web request\" m\u0171velet\u00e9t kell haszn\u00e1lnia. \n\nT\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - Met\u00f3dus: POST\n - Tartalom t\u00edpusa: application/json \n\nB\u0151vebb inform\u00e1ci\u00f3 [a dokument\u00e1ci\u00f3ban]({docs_url}) olvashat\u00f3, hogyan konfigur\u00e1lhatja az automatizmusokat a be\u00e9rkez\u0151 adatok kezel\u00e9s\u00e9re." }, "step": { "user": { diff --git a/homeassistant/components/ifttt/translations/sk.json b/homeassistant/components/ifttt/translations/sk.json index d0550d90757..24a0533229e 100644 --- a/homeassistant/components/ifttt/translations/sk.json +++ b/homeassistant/components/ifttt/translations/sk.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Va\u0161a in\u0161tancia Home Assistant mus\u00ed by\u0165 pr\u00edstupn\u00e1 z internetu, aby ste mohli prij\u00edma\u0165 spr\u00e1vy webhooku." }, "create_entry": { - "default": "Ak chcete odosla\u0165 udalosti Home Assistant, budete musie\u0165 pou\u017ei\u0165 akciu \u201eVytvori\u0165 webov\u00fa \u017eiados\u0165\u201c z [apletu IFTTT Webhook]({applet_url}).\n\nVypl\u0148te nasleduj\u00face inform\u00e1cie: \n\n - URL: `{webhook_url}`\n - Met\u00f3da: POST\n - Typ obsahu: application/json \n\nPozrite si [dokument\u00e1ciu]({docs_url}), ako nakonfigurova\u0165 automatiz\u00e1ciu na spracovanie prich\u00e1dzaj\u00facich \u00fadajov." + "default": "Ak chcete odosla\u0165 udalosti Home Assistant, budete musie\u0165 pou\u017ei\u0165 akciu \"Vytvori\u0165 webov\u00fa \u017eiados\u0165\" z [apletu IFTTT Webhook]({applet_url}).\n\nVypl\u0148te nasleduj\u00face inform\u00e1cie: \n\n - URL: `{webhook_url}`\n - Met\u00f3da: POST\n - Typ obsahu: application/json \n\nPozrite si [dokument\u00e1ciu]({docs_url}), ako nakonfigurova\u0165 automatiz\u00e1ciu na spracovanie prich\u00e1dzaj\u00facich \u00fadajov." }, "step": { "user": { diff --git a/homeassistant/components/ign_sismologia/manifest.json b/homeassistant/components/ign_sismologia/manifest.json index 927e52f594d..78527bd62b3 100644 --- a/homeassistant/components/ign_sismologia/manifest.json +++ b/homeassistant/components/ign_sismologia/manifest.json @@ -2,7 +2,7 @@ "domain": "ign_sismologia", "name": "IGN Sismolog\u00eda", "documentation": "https://www.home-assistant.io/integrations/ign_sismologia", - "requirements": ["georss_ign_sismologia_client==0.3"], + "requirements": ["georss_ign_sismologia_client==0.6"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", "loggers": ["georss_ign_sismologia_client"], diff --git a/homeassistant/components/image_upload/manifest.json b/homeassistant/components/image_upload/manifest.json index e8b3342d7bf..9e8981fb542 100644 --- a/homeassistant/components/image_upload/manifest.json +++ b/homeassistant/components/image_upload/manifest.json @@ -3,7 +3,7 @@ "name": "Image Upload", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/image_upload", - "requirements": ["pillow==9.3.0"], + "requirements": ["pillow==9.4.0"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", diff --git a/homeassistant/components/imap/__init__.py b/homeassistant/components/imap/__init__.py index d85f295a43e..7e582aa04d4 100644 --- a/homeassistant/components/imap/__init__.py +++ b/homeassistant/components/imap/__init__.py @@ -1 +1,54 @@ -"""The imap component.""" +"""The imap integration.""" +from __future__ import annotations + +import asyncio + +from aioimaplib import IMAP4_SSL, AioImapException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryError, + ConfigEntryNotReady, +) + +from .const import DOMAIN +from .coordinator import ImapDataUpdateCoordinator, connect_to_server +from .errors import InvalidAuth, InvalidFolder + +PLATFORMS: list[Platform] = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up imap from a config entry.""" + try: + imap_client: IMAP4_SSL = await connect_to_server(dict(entry.data)) + except InvalidAuth as err: + raise ConfigEntryAuthFailed from err + except InvalidFolder as err: + raise ConfigEntryError("Selected mailbox folder is invalid.") from err + except (asyncio.TimeoutError, AioImapException) as err: + raise ConfigEntryNotReady from err + + coordinator = ImapDataUpdateCoordinator(hass, imap_client) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.shutdown) + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + coordinator: ImapDataUpdateCoordinator = hass.data[DOMAIN].pop(entry.entry_id) + await coordinator.shutdown() + return unload_ok diff --git a/homeassistant/components/imap/config_flow.py b/homeassistant/components/imap/config_flow.py new file mode 100644 index 00000000000..7306d07d06a --- /dev/null +++ b/homeassistant/components/imap/config_flow.py @@ -0,0 +1,136 @@ +"""Config flow for imap integration.""" +from __future__ import annotations + +import asyncio +from collections.abc import Mapping +from typing import Any + +from aioimaplib import AioImapException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv + +from .const import ( + CONF_CHARSET, + CONF_FOLDER, + CONF_SEARCH, + CONF_SERVER, + DEFAULT_PORT, + DOMAIN, +) +from .coordinator import connect_to_server +from .errors import InvalidAuth, InvalidFolder + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_SERVER): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_CHARSET, default="utf-8"): str, + vol.Optional(CONF_FOLDER, default="INBOX"): str, + vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): str, + } +) + + +async def validate_input(user_input: dict[str, Any]) -> dict[str, str]: + """Validate user input.""" + errors = {} + + try: + imap_client = await connect_to_server(user_input) + result, lines = await imap_client.search( + user_input[CONF_SEARCH], + charset=user_input[CONF_CHARSET], + ) + + except InvalidAuth: + errors[CONF_USERNAME] = errors[CONF_PASSWORD] = "invalid_auth" + except InvalidFolder: + errors[CONF_FOLDER] = "invalid_folder" + except (asyncio.TimeoutError, AioImapException, ConnectionRefusedError): + errors["base"] = "cannot_connect" + else: + if result != "OK": + if "The specified charset is not supported" in lines[0].decode("utf-8"): + errors[CONF_CHARSET] = "invalid_charset" + else: + errors[CONF_SEARCH] = "invalid_search" + return errors + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for imap.""" + + VERSION = 1 + _reauth_entry: config_entries.ConfigEntry | None + + 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 + ) + + self._async_abort_entries_match( + { + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_FOLDER: user_input[CONF_FOLDER], + CONF_SEARCH: user_input[CONF_SEARCH], + } + ) + + if not (errors := await validate_input(user_input)): + # To be removed when YAML import is removed + title = user_input.get(CONF_NAME, user_input[CONF_USERNAME]) + + return self.async_create_entry(title=title, data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + 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, entry_data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Confirm reauth dialog.""" + errors = {} + assert self._reauth_entry + if user_input is not None: + user_input = {**self._reauth_entry.data, **user_input} + if not (errors := await validate_input(user_input)): + self.hass.config_entries.async_update_entry( + self._reauth_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( + description_placeholders={ + CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME] + }, + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/imap/const.py b/homeassistant/components/imap/const.py new file mode 100644 index 00000000000..080f7bf6765 --- /dev/null +++ b/homeassistant/components/imap/const.py @@ -0,0 +1,12 @@ +"""Constants for the imap integration.""" + +from typing import Final + +DOMAIN: Final = "imap" + +CONF_SERVER: Final = "server" +CONF_FOLDER: Final = "folder" +CONF_SEARCH: Final = "search" +CONF_CHARSET: Final = "charset" + +DEFAULT_PORT: Final = 993 diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py new file mode 100644 index 00000000000..8a716fe4786 --- /dev/null +++ b/homeassistant/components/imap/coordinator.py @@ -0,0 +1,104 @@ +"""Coordinator for imag integration.""" +from __future__ import annotations + +import asyncio +from collections.abc import Mapping +from datetime import timedelta +import logging +from typing import Any + +from aioimaplib import AUTH, IMAP4_SSL, SELECTED, AioImapException +import async_timeout + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_PORT, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import CONF_CHARSET, CONF_FOLDER, CONF_SEARCH, CONF_SERVER, DOMAIN +from .errors import InvalidAuth, InvalidFolder + +_LOGGER = logging.getLogger(__name__) + + +async def connect_to_server(data: Mapping[str, Any]) -> IMAP4_SSL: + """Connect to imap server and return client.""" + client = IMAP4_SSL(data[CONF_SERVER], data[CONF_PORT]) + await client.wait_hello_from_server() + await client.login(data[CONF_USERNAME], data[CONF_PASSWORD]) + if client.protocol.state != AUTH: + raise InvalidAuth + await client.select(data[CONF_FOLDER]) + if client.protocol.state != SELECTED: + raise InvalidFolder + return client + + +class ImapDataUpdateCoordinator(DataUpdateCoordinator[int]): + """Class for imap client.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, imap_client: IMAP4_SSL) -> None: + """Initiate imap client.""" + self.hass = hass + self.imap_client = imap_client + self.support_push = imap_client.has_capability("IDLE") + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=10) if not self.support_push else None, + ) + + async def _async_update_data(self) -> int: + """Update the number of unread emails.""" + try: + if self.imap_client is None: + self.imap_client = await connect_to_server(self.config_entry.data) + except (AioImapException, asyncio.TimeoutError) as err: + raise UpdateFailed(err) from err + + return await self.refresh_email_count() + + async def refresh_email_count(self) -> int: + """Check the number of found emails.""" + try: + await self.imap_client.noop() + result, lines = await self.imap_client.search( + self.config_entry.data[CONF_SEARCH], + charset=self.config_entry.data[CONF_CHARSET], + ) + except (AioImapException, asyncio.TimeoutError) as err: + raise UpdateFailed(err) from err + + if result != "OK": + raise UpdateFailed( + f"Invalid response for search '{self.config_entry.data[CONF_SEARCH]}': {result} / {lines[0]}" + ) + if self.support_push: + self.hass.async_create_task(self.async_wait_server_push()) + return len(lines[0].split()) + + async def async_wait_server_push(self) -> None: + """Wait for data push from server.""" + try: + idle: asyncio.Future = await self.imap_client.idle_start() + await self.imap_client.wait_server_push() + self.imap_client.idle_done() + async with async_timeout.timeout(10): + await idle + + except (AioImapException, asyncio.TimeoutError): + _LOGGER.warning( + "Lost %s (will attempt to reconnect)", + self.config_entry.data[CONF_SERVER], + ) + self.imap_client = None + await self.async_request_refresh() + + async def shutdown(self, *_) -> None: + """Close resources.""" + if self.imap_client: + await self.imap_client.stop_wait_server_push() + await self.imap_client.logout() diff --git a/homeassistant/components/imap/errors.py b/homeassistant/components/imap/errors.py new file mode 100644 index 00000000000..8f91b7ab6df --- /dev/null +++ b/homeassistant/components/imap/errors.py @@ -0,0 +1,11 @@ +"""Exceptions raised by IMAP integration.""" + +from homeassistant.exceptions import HomeAssistantError + + +class InvalidAuth(HomeAssistantError): + """Raise exception for invalid credentials.""" + + +class InvalidFolder(HomeAssistantError): + """Raise exception for invalid folder.""" diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index 36004113351..24a9486107a 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -1,9 +1,11 @@ { "domain": "imap", "name": "IMAP", + "config_flow": true, + "dependencies": ["repairs"], "documentation": "https://www.home-assistant.io/integrations/imap", "requirements": ["aioimaplib==1.0.1"], - "codeowners": [], + "codeowners": ["@engrbm87"], "iot_class": "cloud_push", "loggers": ["aioimaplib"] } diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index fa5428ccc06..20457209e99 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -1,37 +1,29 @@ """IMAP sensor support.""" from __future__ import annotations -import asyncio -import logging - -from aioimaplib import IMAP4_SSL, AioImapException -import async_timeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import ( - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, -) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady 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.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import CoordinatorEntity -_LOGGER = logging.getLogger(__name__) - -CONF_SERVER = "server" -CONF_FOLDER = "folder" -CONF_SEARCH = "search" -CONF_CHARSET = "charset" - -DEFAULT_PORT = 993 - -ICON = "mdi:email-outline" +from . import ImapDataUpdateCoordinator +from .const import ( + CONF_CHARSET, + CONF_FOLDER, + CONF_SEARCH, + CONF_SERVER, + DEFAULT_PORT, + DOMAIN, +) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -54,139 +46,60 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the IMAP platform.""" - sensor = ImapSensor( - config.get(CONF_NAME), - config.get(CONF_USERNAME), - config.get(CONF_PASSWORD), - config.get(CONF_SERVER), - config.get(CONF_PORT), - config.get(CONF_CHARSET), - config.get(CONF_FOLDER), - config.get(CONF_SEARCH), + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.4.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) ) - if not await sensor.connection(): - raise PlatformNotReady - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, sensor.shutdown) - async_add_entities([sensor], True) -class ImapSensor(SensorEntity): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Imap sensor.""" + + coordinator: ImapDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities([ImapSensor(coordinator)]) + + +class ImapSensor(CoordinatorEntity[ImapDataUpdateCoordinator], SensorEntity): """Representation of an IMAP sensor.""" - def __init__(self, name, user, password, server, port, charset, folder, search): + _attr_icon = "mdi:email-outline" + _attr_has_entity_name = True + + def __init__(self, coordinator: ImapDataUpdateCoordinator) -> None: """Initialize the sensor.""" - self._name = name or user - self._user = user - self._password = password - self._server = server - self._port = port - self._charset = charset - self._folder = folder - self._email_count = None - self._search = search - self._connection = None - self._does_push = None - self._idle_loop_task = None - - async def async_added_to_hass(self) -> None: - """Handle when an entity is about to be added to Home Assistant.""" - if not self.should_poll: - self._idle_loop_task = self.hass.loop.create_task(self.idle_loop()) + super().__init__(coordinator) + # To be removed when YAML import is removed + if CONF_NAME in coordinator.config_entry.data: + self._attr_name = coordinator.config_entry.data[CONF_NAME] + self._attr_has_entity_name = False + self._attr_unique_id = f"{coordinator.config_entry.entry_id}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + name=f"IMAP ({coordinator.config_entry.data[CONF_USERNAME]})", + entry_type=DeviceEntryType.SERVICE, + ) @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return ICON - - @property - def native_value(self): + def native_value(self) -> int: """Return the number of emails found.""" - return self._email_count - - @property - def available(self) -> bool: - """Return the availability of the device.""" - return self._connection is not None - - @property - def should_poll(self) -> bool: - """Return if polling is needed.""" - return not self._does_push - - async def connection(self): - """Return a connection to the server, establishing it if necessary.""" - if self._connection is None: - try: - self._connection = IMAP4_SSL(self._server, self._port) - await self._connection.wait_hello_from_server() - await self._connection.login(self._user, self._password) - await self._connection.select(self._folder) - self._does_push = self._connection.has_capability("IDLE") - except (AioImapException, asyncio.TimeoutError): - self._connection = None - - return self._connection - - async def idle_loop(self): - """Wait for data pushed from server.""" - while True: - try: - if await self.connection(): - await self.refresh_email_count() - self.async_write_ha_state() - - idle = await self._connection.idle_start() - await self._connection.wait_server_push() - self._connection.idle_done() - async with async_timeout.timeout(10): - await idle - else: - self.async_write_ha_state() - except (AioImapException, asyncio.TimeoutError): - self.disconnected() + return self.coordinator.data async def async_update(self) -> None: - """Periodic polling of state.""" - try: - if await self.connection(): - await self.refresh_email_count() - except (AioImapException, asyncio.TimeoutError): - self.disconnected() - - async def refresh_email_count(self): - """Check the number of found emails.""" - if self._connection: - await self._connection.noop() - result, lines = await self._connection.search( - self._search, charset=self._charset - ) - - if result == "OK": - self._email_count = len(lines[0].split()) - else: - _LOGGER.error( - "Can't parse IMAP server response to search '%s': %s / %s", - self._search, - result, - lines[0], - ) - - def disconnected(self): - """Forget the connection after it was lost.""" - _LOGGER.warning("Lost %s (will attempt to reconnect)", self._server) - self._connection = None - - async def shutdown(self, *_): - """Close resources.""" - if self._connection: - if self._connection.has_pending_idle(): - self._connection.idle_done() - await self._connection.logout() - if self._idle_loop_task: - self._idle_loop_task.cancel() + """Check for idle state before updating.""" + if not await self.coordinator.imap_client.stop_wait_server_push(): + await super().async_update() diff --git a/homeassistant/components/imap/strings.json b/homeassistant/components/imap/strings.json new file mode 100644 index 00000000000..25bcf840c33 --- /dev/null +++ b/homeassistant/components/imap/strings.json @@ -0,0 +1,40 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "server": "Server", + "port": "[%key:common::config_flow::data::port%]", + "charset": "Character set", + "folder": "Folder", + "search": "IMAP search" + } + }, + "reauth_confirm": { + "description": "The password for {username} is invalid.", + "title": "[%key:common::config_flow::title::reauth%]", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_charset": "The specified charset is not supported", + "invalid_search": "The selected search is invalid" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + }, + "issues": { + "deprecated_yaml": { + "title": "The IMAP YAML configuration is being removed", + "description": "Configuring IMAP using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the IMAP YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/imap/translations/bg.json b/homeassistant/components/imap/translations/bg.json new file mode 100644 index 00000000000..2f31f00ae03 --- /dev/null +++ b/homeassistant/components/imap/translations/bg.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + }, + "description": "\u041f\u0430\u0440\u043e\u043b\u0430\u0442\u0430 \u0437\u0430 {username} \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "user": { + "data": { + "folder": "\u041f\u0430\u043f\u043a\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "server": "\u0421\u044a\u0440\u0432\u044a\u0440", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 IMAP \u0447\u0440\u0435\u0437 YAML \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 IMAP \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 IMAP \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/ca.json b/homeassistant/components/imap/translations/ca.json new file mode 100644 index 00000000000..19c516bd984 --- /dev/null +++ b/homeassistant/components/imap/translations/ca.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_charset": "El conjunt de car\u00e0cters especificat no \u00e9s compatible", + "invalid_search": "La cerca seleccionada \u00e9s inv\u00e0lida" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Contrasenya" + }, + "description": "La contrasenya de {username} \u00e9s inv\u00e0lida.", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, + "user": { + "data": { + "charset": "Conjunt de car\u00e0cters", + "folder": "Carpeta", + "password": "Contrasenya", + "port": "Port", + "search": "Cerca IMAP", + "server": "Servidor", + "username": "Nom d'usuari" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 d'IMAP mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML d'IMAP del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML d'IMAP est\u00e0 sent eliminada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/de.json b/homeassistant/components/imap/translations/de.json new file mode 100644 index 00000000000..ca36d7f883b --- /dev/null +++ b/homeassistant/components/imap/translations/de.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_charset": "Der angegebene Zeichensatz wird nicht unterst\u00fctzt", + "invalid_search": "Die ausgew\u00e4hlte Suche ist ung\u00fcltig" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Passwort" + }, + "description": "Das Passwort f\u00fcr {username} ist ung\u00fcltig.", + "title": "Integration erneut authentifizieren" + }, + "user": { + "data": { + "charset": "Zeichensatz", + "folder": "Ordner", + "password": "Passwort", + "port": "Port", + "search": "IMAP-Suche", + "server": "Server", + "username": "Benutzername" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von IMAP \u00fcber YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert.\n\nEntferne die IMAP-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die IMAP-YAML-Konfiguration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/el.json b/homeassistant/components/imap/translations/el.json new file mode 100644 index 00000000000..c9cb87b9744 --- /dev/null +++ b/homeassistant/components/imap/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", + "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", + "invalid_charset": "\u03a4\u03bf \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03c3\u03cd\u03bd\u03bf\u03bb\u03bf \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9", + "invalid_search": "\u0397 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b1\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7" + }, + "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 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\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": { + "charset": "\u03a3\u03cd\u03bd\u03bf\u03bb\u03bf \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd", + "folder": "\u03a6\u03ac\u03ba\u03b5\u03bb\u03bf\u03c2", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1", + "search": "\u0391\u03bd\u03b1\u03b6\u03ae\u03c4\u03b7\u03c3\u03b7 IMAP", + "server": "\u0394\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 IMAP \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 IMAP YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 IMAP YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/en.json b/homeassistant/components/imap/translations/en.json new file mode 100644 index 00000000000..a1317b32f19 --- /dev/null +++ b/homeassistant/components/imap/translations/en.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_charset": "The specified charset is not supported", + "invalid_search": "The selected search is invalid" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "The password for {username} is invalid.", + "title": "Reauthenticate Integration" + }, + "user": { + "data": { + "charset": "Character set", + "folder": "Folder", + "password": "Password", + "port": "Port", + "search": "IMAP search", + "server": "Server", + "username": "Username" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuring IMAP using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the IMAP YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The IMAP YAML configuration is being removed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/es.json b/homeassistant/components/imap/translations/es.json new file mode 100644 index 00000000000..fc5631d27da --- /dev/null +++ b/homeassistant/components/imap/translations/es.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_charset": "El juego de caracteres especificado no es compatible", + "invalid_search": "La b\u00fasqueda seleccionada no es v\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "La contrase\u00f1a para {username} no es v\u00e1lida.", + "title": "Volver a autenticar la integraci\u00f3n" + }, + "user": { + "data": { + "charset": "Juego de caracteres", + "folder": "Carpeta", + "password": "Contrase\u00f1a", + "port": "Puerto", + "search": "B\u00fasqueda IMAP", + "server": "Servidor", + "username": "Nombre de usuario" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de IMAP mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n IMAP YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de IMAP" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/et.json b/homeassistant/components/imap/translations/et.json new file mode 100644 index 00000000000..4a752c171d8 --- /dev/null +++ b/homeassistant/components/imap/translations/et.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "invalid_charset": "M\u00e4\u00e4ratud m\u00e4rgistikku ei toetata", + "invalid_search": "Valitud otsing on sobimatu" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Kasutaja {username} salas\u00f5na on kehtetu", + "title": "Taastuvasta sidumine" + }, + "user": { + "data": { + "charset": "T\u00e4hem\u00e4rkide komplekt", + "folder": "Kaust", + "password": "Salas\u00f5na", + "port": "Port", + "search": "IMAP otsing", + "server": "Server", + "username": "Kasutajanimi" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "IMAP-i konfigureerimine YAML-i abil eemaldatakse. \n\n Teie olemasolev YAML-i konfiguratsioon imporditi kasutajaliidesesse automaatselt. \n\n Eemaldage IMAP YAML-i konfiguratsioon failist configuration.yaml ja taask\u00e4ivitage selle probleemi lahendamiseks Home Assistant.", + "title": "IMAP YAML-i konfiguratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/hu.json b/homeassistant/components/imap/translations/hu.json new file mode 100644 index 00000000000..9f06e711bb8 --- /dev/null +++ b/homeassistant/components/imap/translations/hu.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "invalid_charset": "A megadott karakterk\u00e9szlet nem t\u00e1mogatott", + "invalid_search": "A kiv\u00e1lasztott keres\u00e9s \u00e9rv\u00e9nytelen" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Jelsz\u00f3" + }, + "description": "The password for {username} is invalid.", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, + "user": { + "data": { + "charset": "Karakterk\u00e9szlet", + "folder": "Mappa", + "password": "Jelsz\u00f3", + "port": "Port", + "search": "IMAP keres\u00e9s", + "server": "Szerver", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Az IMAP konfigur\u00e1l\u00e1sa YAML haszn\u00e1lat\u00e1val elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el az IMAP YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "Az IMAP YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/id.json b/homeassistant/components/imap/translations/id.json new file mode 100644 index 00000000000..1f6178d9f68 --- /dev/null +++ b/homeassistant/components/imap/translations/id.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "invalid_charset": "Set karakter yang ditentukan tidak didukung", + "invalid_search": "Pencarian yang dipilih tidak valid" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Kata Sandi" + }, + "description": "Kata sandi untuk {username} tidak valid.", + "title": "Autentikasi Ulang Integrasi" + }, + "user": { + "data": { + "charset": "Set karakter", + "folder": "Folder", + "password": "Kata Sandi", + "port": "Port", + "search": "Pencarian IMAP", + "server": "Server", + "username": "Nama Pengguna" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Integrasi IMAP lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi IMAP dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Integrasi IMAP dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/it.json b/homeassistant/components/imap/translations/it.json new file mode 100644 index 00000000000..038c23f19c5 --- /dev/null +++ b/homeassistant/components/imap/translations/it.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "invalid_charset": "Il set di caratteri specificato non \u00e8 supportato", + "invalid_search": "La ricerca selezionata non \u00e8 valida" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "La password per {username} non \u00e8 valida.", + "title": "Autentica nuovamente l'integrazione" + }, + "user": { + "data": { + "charset": "Set di caratteri", + "folder": "Cartella", + "password": "Password", + "port": "Porta", + "search": "Ricerca IMAP", + "server": "Server", + "username": "Nome utente" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di IMAP tramite YAML \u00e8 stata rimossa. \n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente. \n\nRimuovi la configurazione YAML di IMAP dal tuo file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di IMAP \u00e8 in fase di rimozione" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/lv.json b/homeassistant/components/imap/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/imap/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/nl.json b/homeassistant/components/imap/translations/nl.json new file mode 100644 index 00000000000..ebd680dc66d --- /dev/null +++ b/homeassistant/components/imap/translations/nl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Wachtwoord" + }, + "description": "Het wachtwoord voor {username} is onjuist.", + "title": "Integratie herauthenticeren" + }, + "user": { + "data": { + "folder": "Map", + "password": "Wachtwoord", + "port": "Poort", + "server": "Server", + "username": "Gebruikersnaam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/no.json b/homeassistant/components/imap/translations/no.json new file mode 100644 index 00000000000..fa8677df062 --- /dev/null +++ b/homeassistant/components/imap/translations/no.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Re-autentisering var vellykket" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "invalid_charset": "Det angitte tegnsettet st\u00f8ttes ikke", + "invalid_search": "Det valgte s\u00f8ket er ugyldig" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Passord" + }, + "description": "Passordet for {username} er ugyldig.", + "title": "Godkjenne integrering p\u00e5 nytt" + }, + "user": { + "data": { + "charset": "Tegnsett", + "folder": "Mappe", + "password": "Passord", + "port": "Port", + "search": "IMAP-s\u00f8k", + "server": "Server", + "username": "Brukernavn" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av IMAP med YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern IMAP YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "IMAP YAML-konfigurasjonen fjernes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/pl.json b/homeassistant/components/imap/translations/pl.json new file mode 100644 index 00000000000..1a73cf79e97 --- /dev/null +++ b/homeassistant/components/imap/translations/pl.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "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", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_charset": "Podany zestaw znak\u00f3w nie jest obs\u0142ugiwany", + "invalid_search": "Wybrane wyszukiwanie jest nieprawid\u0142owe" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Has\u0142o" + }, + "description": "Has\u0142o u\u017cytkownika {username} jest nieprawid\u0142owe.", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, + "user": { + "data": { + "charset": "Zestaw znak\u00f3w", + "folder": "Folder", + "password": "Has\u0142o", + "port": "Port", + "search": "Wyszukiwanie IMAP", + "server": "Serwer", + "username": "Nazwa u\u017cytkownika" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja IMAP przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla IMAP zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/pt-BR.json b/homeassistant/components/imap/translations/pt-BR.json new file mode 100644 index 00000000000..a290aa7c968 --- /dev/null +++ b/homeassistant/components/imap/translations/pt-BR.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_charset": "O conjunto de caracteres especificado n\u00e3o \u00e9 suportado", + "invalid_search": "A pesquisa selecionada \u00e9 inv\u00e1lida" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "A senha para {username} \u00e9 inv\u00e1lida.", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "charset": "Conjunto de caracteres", + "folder": "Pasta", + "password": "Senha", + "port": "Porta", + "search": "Pesquisa IMAP", + "server": "Servidor", + "username": "Nome de usu\u00e1rio" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do IMAP usando YAML est\u00e1 sendo removida. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a IU automaticamente. \n\n Remova a configura\u00e7\u00e3o IMAP YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML de IMAP est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/pt.json b/homeassistant/components/imap/translations/pt.json new file mode 100644 index 00000000000..4dbbc0745af --- /dev/null +++ b/homeassistant/components/imap/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "Senha" + }, + "description": "A senha para {username} \u00e9 inv\u00e1lida.", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "port": "Porta", + "search": "Pesquisa IMAP", + "server": "Servidor", + "username": "Nome Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/ru.json b/homeassistant/components/imap/translations/ru.json new file mode 100644 index 00000000000..dbf0b78f1c3 --- /dev/null +++ b/homeassistant/components/imap/translations/ru.json @@ -0,0 +1,40 @@ +{ + "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.", + "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.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "invalid_charset": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u043d\u0430\u0431\u043e\u0440 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", + "invalid_search": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0439 \u043f\u043e\u0438\u0441\u043a \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d." + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f {username}.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "user": { + "data": { + "charset": "\u041d\u0430\u0431\u043e\u0440 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432", + "folder": "\u041f\u0430\u043f\u043a\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "search": "\u041f\u043e\u0438\u0441\u043a \u043f\u043e IMAP", + "server": "\u0421\u0435\u0440\u0432\u0435\u0440", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 IMAP \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 IMAP \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/sk.json b/homeassistant/components/imap/translations/sk.json new file mode 100644 index 00000000000..bba06f0a874 --- /dev/null +++ b/homeassistant/components/imap/translations/sk.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "invalid_charset": "Zadan\u00e1 znakov\u00e1 sada nie je podporovan\u00e1", + "invalid_search": "Vybran\u00e9 vyh\u013ead\u00e1vanie je neplatn\u00e9" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Heslo" + }, + "description": "Heslo pre {username} je neplatn\u00e9.", + "title": "Znova overi\u0165 integr\u00e1ciu" + }, + "user": { + "data": { + "charset": "Sada znakov", + "folder": "Prie\u010dinok", + "password": "Heslo", + "port": "Port", + "search": "Vyh\u013ead\u00e1vanie IMAP", + "server": "Server", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigur\u00e1cia IMAP pomocou YAML sa odstra\u0148uje. \n\n Va\u0161a existuj\u00faca konfigur\u00e1cia YAML bola importovan\u00e1 do pou\u017e\u00edvate\u013esk\u00e9ho rozhrania automaticky. \n\n Ak chcete tento probl\u00e9m vyrie\u0161i\u0165, odstr\u00e1\u0148te konfigur\u00e1ciu IMAP YAML zo s\u00faboru configuration.yaml a re\u0161tartujte aplik\u00e1ciu Home Assistant.", + "title": "Konfigur\u00e1cia IMAP YAML sa odstra\u0148uje" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/tr.json b/homeassistant/components/imap/translations/tr.json new file mode 100644 index 00000000000..f63c713cb98 --- /dev/null +++ b/homeassistant/components/imap/translations/tr.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "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", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_charset": "Belirtilen karakter desteklenmiyor", + "invalid_search": "Se\u00e7ilen arama ge\u00e7ersiz" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "{username} \u015fifresi ge\u00e7ersiz.", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, + "user": { + "data": { + "charset": "Karakter seti", + "folder": "Klas\u00f6r", + "password": "Parola", + "port": "Port", + "search": "IMAP aramas\u0131", + "server": "Sunucu", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "YAML kullanarak IMAP yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z, kullan\u0131c\u0131 aray\u00fcz\u00fcne otomatik olarak aktar\u0131ld\u0131. \n\n IMAP YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu \u00e7\u00f6zmek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "IMAP YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/uk.json b/homeassistant/components/imap/translations/uk.json new file mode 100644 index 00000000000..20ae288f8ae --- /dev/null +++ b/homeassistant/components/imap/translations/uk.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f", + "invalid_charset": "\u0417\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u043d\u0430\u0431\u0456\u0440 \u0441\u0438\u043c\u0432\u043e\u043b\u0456\u0432 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f", + "invalid_search": "\u0412\u0438\u0431\u0440\u0430\u043d\u0438\u0439 \u043f\u043e\u0448\u0443\u043a \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username} \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457" + }, + "user": { + "data": { + "charset": "\u041d\u0430\u0431\u0456\u0440 \u0441\u0438\u043c\u0432\u043e\u043b\u0456\u0432", + "folder": "\u041f\u0430\u043f\u043a\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "search": "IMAP \u043f\u043e\u0448\u0443\u043a", + "server": "\u0421\u0435\u0440\u0432\u0435\u0440", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f IMAP \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e YAML \u0431\u0443\u0434\u0435 \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043e.\n\n\u0412\u0430\u0448\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f YAML \u0431\u0443\u043b\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0456\u043c\u043f\u043e\u0440\u0442\u043e\u0432\u0430\u043d\u0430 \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430.\n\n\u0412\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e IMAP YAML \u0456\u0437 \u0444\u0430\u0439\u043b\u0443 configuration.yaml \u0456 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c Home Assistant, \u0449\u043e\u0431 \u0440\u043e\u0437\u0432'\u044f\u0437\u0430\u0442\u0438 \u0446\u044e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f IMAP YAML \u0432\u0438\u0434\u0430\u043b\u044f\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/imap/translations/zh-Hant.json b/homeassistant/components/imap/translations/zh-Hant.json new file mode 100644 index 00000000000..5f4e27281bf --- /dev/null +++ b/homeassistant/components/imap/translations/zh-Hant.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "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", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_charset": "\u6307\u5b9a\u5b57\u5143\u4e0d\u652f\u63f4", + "invalid_search": "\u9078\u64c7\u641c\u5c0b\u7121\u6548" + }, + "step": { + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "{username} \u5bc6\u78bc\u7121\u6548\u3002", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, + "user": { + "data": { + "charset": "\u5b57\u5143\u96c6", + "folder": "\u6a94\u6848\u593e", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "search": "IMAP \u641c\u5c0b", + "server": "\u4f3a\u670d\u5668", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 IMAP \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 IMAP YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "IMAP YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index 24625a66099..67aaae225a8 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -18,7 +18,6 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP, - STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady, TemplateError @@ -248,10 +247,10 @@ class InfluxSensor(SensorEntity): """Get the latest data from Influxdb and updates the states.""" self.data.update() if (value := self.data.value) is None: - value = STATE_UNKNOWN + value = None if self._value_template is not None: value = self._value_template.render_with_possible_json_value( - str(value), STATE_UNKNOWN + str(value), None ) self._state = value diff --git a/homeassistant/components/inkbird/manifest.json b/homeassistant/components/inkbird/manifest.json index 97234de9d6d..e9984284c72 100644 --- a/homeassistant/components/inkbird/manifest.json +++ b/homeassistant/components/inkbird/manifest.json @@ -11,7 +11,7 @@ { "local_name": "tps", "connectable": false } ], "requirements": ["inkbird-ble==0.5.5"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" } diff --git a/homeassistant/components/inkbird/sensor.py b/homeassistant/components/inkbird/sensor.py index 74716d465a8..f93f2024289 100644 --- a/homeassistant/components/inkbird/sensor.py +++ b/homeassistant/components/inkbird/sensor.py @@ -1,8 +1,6 @@ """Support for inkbird ble sensors.""" from __future__ import annotations -from typing import Optional, Union - from inkbird_ble import DeviceClass, DeviceKey, SensorUpdate, Units from homeassistant import config_entries @@ -115,9 +113,7 @@ async def async_setup_entry( class INKBIRDBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a inkbird ble sensor.""" diff --git a/homeassistant/components/inkbird/translations/lv.json b/homeassistant/components/inkbird/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/inkbird/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/inkbird/translations/tr.json b/homeassistant/components/inkbird/translations/tr.json index f63cee3493c..66d94aa9414 100644 --- a/homeassistant/components/inkbird/translations/tr.json +++ b/homeassistant/components/inkbird/translations/tr.json @@ -8,13 +8,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 82b910215c4..c8a0982b430 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -51,7 +51,7 @@ async def async_get_device_config(hass, config_entry): load_aldb = 2 if devices.modem.aldb.read_write_mode == ReadWriteMode.UNKNOWN else 1 await devices.async_load(id_devices=1, load_modem_aldb=load_aldb) - for addr in devices: + for addr in list(devices): device = devices[addr] flags = True for name in device.operating_flags: diff --git a/homeassistant/components/insteon/api/__init__.py b/homeassistant/components/insteon/api/__init__.py index e56d4dab07e..b12ae993d9b 100644 --- a/homeassistant/components/insteon/api/__init__.py +++ b/homeassistant/components/insteon/api/__init__.py @@ -55,12 +55,6 @@ def async_load_api(hass): websocket_api.async_register_command(hass, websocket_reset_properties) -def get_entrypoint(is_dev): - """Get the entry point for the frontend.""" - if is_dev: - return "entrypoint.js" - - async def async_register_insteon_frontend(hass: HomeAssistant): """Register the Insteon frontend configuration panel.""" # Add to sidepanel if needed diff --git a/homeassistant/components/insteon/config_flow.py b/homeassistant/components/insteon/config_flow.py index d9261a65c32..60da74fdf01 100644 --- a/homeassistant/components/insteon/config_flow.py +++ b/homeassistant/components/insteon/config_flow.py @@ -49,6 +49,7 @@ STEP_PLM = "plm" STEP_HUB_V1 = "hubv1" STEP_HUB_V2 = "hubv2" STEP_CHANGE_HUB_CONFIG = "change_hub_config" +STEP_CHANGE_PLM_CONFIG = "change_plm_config" STEP_ADD_X10 = "add_x10" STEP_ADD_OVERRIDE = "add_override" STEP_REMOVE_OVERRIDE = "remove_override" @@ -213,7 +214,7 @@ class InsteonFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(config_entries.DEFAULT_DISCOVERY_UNIQUE_ID) return await self.async_step_confirm_usb() - async def async_step_confirm_usb(self, user_input=None): + async def async_step_confirm_usb(self, user_input=None) -> FlowResult: """Confirm a USB discovery.""" if user_input is not None: return await self.async_step_plm({CONF_DEVICE: self._device_path}) @@ -240,17 +241,19 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): """Init the InsteonOptionsFlowHandler class.""" self.config_entry = config_entry - async def async_step_init(self, user_input=None): + async def async_step_init(self, user_input=None) -> FlowResult: """Init the options config flow.""" errors = {} if user_input is not None: change_hub_config = user_input.get(STEP_CHANGE_HUB_CONFIG, False) + change_plm_config = user_input.get(STEP_CHANGE_PLM_CONFIG, False) device_override = user_input.get(STEP_ADD_OVERRIDE, False) x10_device = user_input.get(STEP_ADD_X10, False) remove_override = user_input.get(STEP_REMOVE_OVERRIDE, False) remove_x10 = user_input.get(STEP_REMOVE_X10, False) if _only_one_selected( change_hub_config, + change_plm_config, device_override, x10_device, remove_override, @@ -258,6 +261,8 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): ): if change_hub_config: return await self.async_step_change_hub_config() + if change_plm_config: + return await self.async_step_change_plm_config() if device_override: return await self.async_step_add_override() if x10_device: @@ -274,6 +279,8 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): } if self.config_entry.data.get(CONF_HOST): data_schema[vol.Optional(STEP_CHANGE_HUB_CONFIG)] = bool + else: + data_schema[vol.Optional(STEP_CHANGE_PLM_CONFIG)] = bool options = {**self.config_entry.options} if options.get(CONF_OVERRIDE): @@ -285,7 +292,7 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): step_id="init", data_schema=vol.Schema(data_schema), errors=errors ) - async def async_step_change_hub_config(self, user_input=None): + async def async_step_change_hub_config(self, user_input=None) -> FlowResult: """Change the Hub configuration.""" if user_input is not None: data = { @@ -306,7 +313,24 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): step_id=STEP_CHANGE_HUB_CONFIG, data_schema=data_schema ) - async def async_step_add_override(self, user_input=None): + async def async_step_change_plm_config(self, user_input=None) -> FlowResult: + """Change the PLM configuration.""" + if user_input is not None: + data = { + **self.config_entry.data, + CONF_DEVICE: user_input[CONF_DEVICE], + } + self.hass.config_entries.async_update_entry(self.config_entry, data=data) + return self.async_create_entry( + title="", + data={**self.config_entry.options}, + ) + data_schema = build_plm_schema(**self.config_entry.data) + return self.async_show_form( + step_id=STEP_CHANGE_PLM_CONFIG, data_schema=data_schema + ) + + async def async_step_add_override(self, user_input=None) -> FlowResult: """Add a device override.""" errors = {} if user_input is not None: @@ -322,22 +346,22 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): step_id=STEP_ADD_OVERRIDE, data_schema=data_schema, errors=errors ) - async def async_step_add_x10(self, user_input=None): + async def async_step_add_x10(self, user_input=None) -> FlowResult: """Add an X10 device.""" - errors = {} + errors: dict[str, str] = {} if user_input is not None: options = add_x10_device({**self.config_entry.options}, user_input) async_dispatcher_send(self.hass, SIGNAL_ADD_X10_DEVICE, user_input) return self.async_create_entry(title="", data=options) - schema_defaults = user_input if user_input is not None else {} + schema_defaults: dict[str, str] = user_input if user_input is not None else {} data_schema = build_x10_schema(**schema_defaults) return self.async_show_form( step_id=STEP_ADD_X10, data_schema=data_schema, errors=errors ) - async def async_step_remove_override(self, user_input=None): + async def async_step_remove_override(self, user_input=None) -> FlowResult: """Remove a device override.""" - errors = {} + errors: dict[str, str] = {} options = self.config_entry.options if user_input is not None: options = _remove_override(user_input[CONF_ADDRESS], options) @@ -353,9 +377,9 @@ class InsteonOptionsFlowHandler(config_entries.OptionsFlow): step_id=STEP_REMOVE_OVERRIDE, data_schema=data_schema, errors=errors ) - async def async_step_remove_x10(self, user_input=None): + async def async_step_remove_x10(self, user_input=None) -> FlowResult: """Remove an X10 device.""" - errors = {} + errors: dict[str, str] = {} options = self.config_entry.options if user_input is not None: options, housecode, unitcode = _remove_x10(user_input[CONF_DEVICE], options) diff --git a/homeassistant/components/insteon/strings.json b/homeassistant/components/insteon/strings.json index ddeed18edcd..b302165ce6f 100644 --- a/homeassistant/components/insteon/strings.json +++ b/homeassistant/components/insteon/strings.json @@ -52,6 +52,7 @@ "init": { "data": { "change_hub_config": "Change the Hub configuration.", + "change_plm_config": "Change the PLM configuration.", "add_override": "Add a device override.", "add_x10": "Add an X10 device.", "remove_override": "Remove a device override.", @@ -67,6 +68,12 @@ "password": "[%key:common::config_flow::data::password%]" } }, + "change_plm_config": { + "description": "Change the Insteon PLM connection information. You must restart Home Assistant after making this change. This does not change the configuration of the PLM itself.", + "data": { + "device": "[%key:common::config_flow::data::usb_path%]" + } + }, "add_override": { "description": "Add a device override.", "data": { diff --git a/homeassistant/components/insteon/translations/bg.json b/homeassistant/components/insteon/translations/bg.json index f3fb9df607c..eb1a0da32fe 100644 --- a/homeassistant/components/insteon/translations/bg.json +++ b/homeassistant/components/insteon/translations/bg.json @@ -46,6 +46,11 @@ "port": "\u041f\u043e\u0440\u0442", "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" } + }, + "init": { + "data": { + "change_plm_config": "\u041f\u0440\u043e\u043c\u044f\u043d\u0430 \u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 PLM." + } } } } diff --git a/homeassistant/components/insteon/translations/ca.json b/homeassistant/components/insteon/translations/ca.json index f8e0e1c853e..435c8500f93 100644 --- a/homeassistant/components/insteon/translations/ca.json +++ b/homeassistant/components/insteon/translations/ca.json @@ -80,11 +80,18 @@ }, "description": "Canvia la informaci\u00f3 de connexi\u00f3 de l'Insteon Hub. Has de reiniciar Home Assistant si fas canvis. Aix\u00f2 no canvia la configuraci\u00f3 del Hub en si. Per canviar la configuraci\u00f3 del Hub, utilitza la seva aplicaci\u00f3." }, + "change_plm_config": { + "data": { + "device": "Ruta del dispositiu USB" + }, + "description": "Canvia la informaci\u00f3 de connexi\u00f3 d'Insteon PLM. Has de reiniciar Home Assistant si fas canvis. Aix\u00f2 no canvia la configuraci\u00f3 del PLM en si." + }, "init": { "data": { "add_override": "Afegeix una substituci\u00f3 de dispositiu.", "add_x10": "Afegeix un dispositiu X10.", "change_hub_config": "Canvia la configuraci\u00f3 del Hub.", + "change_plm_config": "Canvia la configuraci\u00f3 de PLM.", "remove_override": "Elimina una substituci\u00f3 de dispositiu.", "remove_x10": "Elimina un dispositiu X10." } diff --git a/homeassistant/components/insteon/translations/de.json b/homeassistant/components/insteon/translations/de.json index 70a8ded1a36..e2481451474 100644 --- a/homeassistant/components/insteon/translations/de.json +++ b/homeassistant/components/insteon/translations/de.json @@ -80,11 +80,18 @@ }, "description": "\u00c4ndere die Verbindungsinformationen des Insteon-Hubs. Du musst Home Assistant neu starten, nachdem du diese \u00c4nderung vorgenommen hast. Dies \u00e4ndert nicht die Konfiguration des Hubs selbst. Um die Konfiguration im Hub zu \u00e4ndern, verwende die Hub-App." }, + "change_plm_config": { + "data": { + "device": "USB-Ger\u00e4te-Pfad" + }, + "description": "\u00c4ndere die Insteon PLM-Verbindungsinformationen. Du musst Home Assistant neu starten, nachdem du diese \u00c4nderung vorgenommen hast. An der Konfiguration des PLM selbst \u00e4ndert sich dadurch nichts." + }, "init": { "data": { "add_override": "F\u00fcge eine Ger\u00e4te\u00fcberschreibung hinzu.", "add_x10": "F\u00fcge ein X10-Ger\u00e4t hinzu.", "change_hub_config": "\u00c4ndere die Konfiguration des Hubs.", + "change_plm_config": "\u00c4ndern der PLM-Konfiguration.", "remove_override": "Entferne eine Ger\u00e4te\u00fcbersteuerung.", "remove_x10": "Entferne ein X10-Ger\u00e4t." } diff --git a/homeassistant/components/insteon/translations/el.json b/homeassistant/components/insteon/translations/el.json index 9ff9e025f8f..8452d655fca 100644 --- a/homeassistant/components/insteon/translations/el.json +++ b/homeassistant/components/insteon/translations/el.json @@ -80,11 +80,15 @@ }, "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." }, + "change_plm_config": { + "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 Insteon PLM. \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 \u03b1\u03c6\u03bf\u03cd \u03ba\u03ac\u03bd\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae. \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 PLM." + }, "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.", + "change_plm_config": "\u0391\u03bb\u03bb\u03ac\u03be\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 PLM.", "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." } diff --git a/homeassistant/components/insteon/translations/en.json b/homeassistant/components/insteon/translations/en.json index b1c4a8d792a..df1eafc8757 100644 --- a/homeassistant/components/insteon/translations/en.json +++ b/homeassistant/components/insteon/translations/en.json @@ -80,11 +80,18 @@ }, "description": "Change the Insteon Hub connection information. You must restart Home Assistant after making this change. This does not change the configuration of the Hub itself. To change the configuration in the Hub use the Hub app." }, + "change_plm_config": { + "data": { + "device": "USB Device Path" + }, + "description": "Change the Insteon PLM connection information. You must restart Home Assistant after making this change. This does not change the configuration of the PLM itself." + }, "init": { "data": { "add_override": "Add a device override.", "add_x10": "Add an X10 device.", "change_hub_config": "Change the Hub configuration.", + "change_plm_config": "Change the PLM configuration.", "remove_override": "Remove a device override.", "remove_x10": "Remove an X10 device." } diff --git a/homeassistant/components/insteon/translations/es.json b/homeassistant/components/insteon/translations/es.json index 855349ab9cf..64e95837942 100644 --- a/homeassistant/components/insteon/translations/es.json +++ b/homeassistant/components/insteon/translations/es.json @@ -80,11 +80,18 @@ }, "description": "Cambia la informaci\u00f3n de conexi\u00f3n del Insteon Hub. Debes reiniciar Home Assistant despu\u00e9s de realizar este cambio. Esto no cambia la configuraci\u00f3n del Hub en s\u00ed. Para cambiar la configuraci\u00f3n en el Hub, usa la aplicaci\u00f3n Hub." }, + "change_plm_config": { + "data": { + "device": "Ruta del dispositivo USB" + }, + "description": "Cambia la informaci\u00f3n de conexi\u00f3n de Insteon PLM. Debes reiniciar Home Assistant despu\u00e9s de realizar este cambio. Esto no cambia la configuraci\u00f3n del propio PLM." + }, "init": { "data": { "add_override": "A\u00f1ade una anulaci\u00f3n de dispositivo.", "add_x10": "A\u00f1ade un dispositivo X10.", "change_hub_config": "Cambia la configuraci\u00f3n del Hub.", + "change_plm_config": "Cambiar la configuraci\u00f3n de PLM.", "remove_override": "Eliminar una anulaci\u00f3n de dispositivo.", "remove_x10": "Eliminar un dispositivo X10." } diff --git a/homeassistant/components/insteon/translations/et.json b/homeassistant/components/insteon/translations/et.json index 72f2a9ac5c4..34202dedcfa 100644 --- a/homeassistant/components/insteon/translations/et.json +++ b/homeassistant/components/insteon/translations/et.json @@ -80,11 +80,18 @@ }, "description": "Muutda Insteon Hubi \u00fchenduse teavet. P\u00e4rast selle muudatuse tegemist pead Home Assistanti taask\u00e4ivitama. See ei muuda jaoturi enda konfiguratsiooni. Hubis muudatuste tegemiseks kasuta rakendust Hub." }, + "change_plm_config": { + "data": { + "device": "USB-seadme asukoha rada" + }, + "description": "Muuda Insteon PLMi \u00fchendusandmeid. P\u00e4rast selle muudatuse tegemist pead Home Assistant'i uuesti k\u00e4ivitama. See ei muuda PLM-i enda konfiguratsiooni." + }, "init": { "data": { "add_override": "Lisa seadme alistamine.", "add_x10": "Lisa X10 seade.", "change_hub_config": "Muuda jaoturi konfiguratsiooni.", + "change_plm_config": "Muuda PLMi konfiguratsiooni.", "remove_override": "Seadme alistamise eemaldamine.", "remove_x10": "Eemalda X10 seade." } diff --git a/homeassistant/components/insteon/translations/fa.json b/homeassistant/components/insteon/translations/fa.json index 2456fbcba00..758677b40ea 100644 --- a/homeassistant/components/insteon/translations/fa.json +++ b/homeassistant/components/insteon/translations/fa.json @@ -3,5 +3,14 @@ "abort": { "single_instance_allowed": "\u0628\u0647 \u062f\u0631\u0633\u062a\u06cc \u062a\u0646\u0638\u06cc\u0645 \u0634\u062f\u0647 \u0627\u0633\u062a. \u062a\u0646\u0647\u0627 \u06cc\u06a9 \u062a\u0646\u0638\u06cc\u0645 \u0627\u0645\u06a9\u0627\u0646 \u067e\u0630\u06cc\u0631 \u0627\u0633\u062a." } + }, + "options": { + "step": { + "change_plm_config": { + "data": { + "device": "\u0622\u062f\u0631\u0633 \u067e\u0648\u0631\u062a usb" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/id.json b/homeassistant/components/insteon/translations/id.json index 7520c6cd03d..b11a7c12540 100644 --- a/homeassistant/components/insteon/translations/id.json +++ b/homeassistant/components/insteon/translations/id.json @@ -80,11 +80,18 @@ }, "description": "Ubah informasi koneksi Insteon Hub. Anda harus memulai ulang Home Assistant setelah melakukan perubahan ini. Ini tidak mengubah konfigurasi Hub itu sendiri. Untuk mengubah konfigurasi di Hub, gunakan aplikasi Hub." }, + "change_plm_config": { + "data": { + "device": "Jalur Perangkat USB" + }, + "description": "Ubah informasi koneksi Insteon PLM. Anda harus memulai ulang Home Assistant setelah melakukan perubahan ini. Ini tidak mengubah konfigurasi PLM itu sendiri." + }, "init": { "data": { "add_override": "Tambahkan penimpaan nilai perangkat.", "add_x10": "Tambahkan perangkat X10.", "change_hub_config": "Ubah konfigurasi Hub.", + "change_plm_config": "Ubah konfigurasi PLM.", "remove_override": "Hapus penimpaan nilai perangkat.", "remove_x10": "Hapus perangkat X10." } diff --git a/homeassistant/components/insteon/translations/it.json b/homeassistant/components/insteon/translations/it.json index 38a04fa1fe0..5fea063e7b1 100644 --- a/homeassistant/components/insteon/translations/it.json +++ b/homeassistant/components/insteon/translations/it.json @@ -66,7 +66,7 @@ "data": { "housecode": "Codice casa (a - p)", "platform": "Piattaforma", - "steps": "Livelli del dimmer (solo per dispositivi luminosi, predefiniti 22)", + "steps": "Livelli di attenuazione (solo per dispositivi luminosi, predefiniti 22)", "unitcode": "Codice unit\u00e0 (1 - 16)" }, "description": "Cambia la password di Insteon Hub." diff --git a/homeassistant/components/insteon/translations/lt.json b/homeassistant/components/insteon/translations/lt.json new file mode 100644 index 00000000000..954f27dbf51 --- /dev/null +++ b/homeassistant/components/insteon/translations/lt.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "change_hub_config": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/no.json b/homeassistant/components/insteon/translations/no.json index 48ed9d61998..6062ff7b6a3 100644 --- a/homeassistant/components/insteon/translations/no.json +++ b/homeassistant/components/insteon/translations/no.json @@ -80,11 +80,18 @@ }, "description": "Endre Insteon Hub-tilkoblingsinformasjonen. Du m\u00e5 starte Home Assistant p\u00e5 nytt n\u00e5r du har gjort denne endringen. Dette endrer ikke konfigurasjonen av selve huben. For \u00e5 endre konfigurasjonen i huben bruker du hub-appen." }, + "change_plm_config": { + "data": { + "device": "USB enhetsbane" + }, + "description": "Endre Insteon PLM-tilkoblingsinformasjonen. Du m\u00e5 starte Home Assistant p\u00e5 nytt etter \u00e5 ha gjort denne endringen. Dette endrer ikke konfigurasjonen av selve PLM." + }, "init": { "data": { "add_override": "Legg til en enhetsoverstyring.", "add_x10": "Legg til en X10-enhet.", "change_hub_config": "Endre hub-konfigurasjonen.", + "change_plm_config": "Endre PLM-konfigurasjonen.", "remove_override": "Fjern en enhet overstyring.", "remove_x10": "Fjern en X10-enhet." } diff --git a/homeassistant/components/insteon/translations/pt-BR.json b/homeassistant/components/insteon/translations/pt-BR.json index e03bd8a55c0..ec059c2ed6b 100644 --- a/homeassistant/components/insteon/translations/pt-BR.json +++ b/homeassistant/components/insteon/translations/pt-BR.json @@ -80,11 +80,18 @@ }, "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." }, + "change_plm_config": { + "data": { + "device": "Caminho do Dispositivo USB" + }, + "description": "Altere as informa\u00e7\u00f5es de conex\u00e3o do Insteon PLM. 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 PLM." + }, "init": { "data": { "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.", + "change_plm_config": "Altere a configura\u00e7\u00e3o do PLM.", "remove_override": "Remova uma substitui\u00e7\u00e3o de dispositivo.", "remove_x10": "Remova um dispositivo X10." } diff --git a/homeassistant/components/insteon/translations/ru.json b/homeassistant/components/insteon/translations/ru.json index a95d3a51afc..9c4c6d4c73c 100644 --- a/homeassistant/components/insteon/translations/ru.json +++ b/homeassistant/components/insteon/translations/ru.json @@ -80,11 +80,18 @@ }, "description": "\u0418\u0437\u043c\u0435\u043d\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 Insteon Hub. \u041f\u043e\u0441\u043b\u0435 \u0432\u043d\u0435\u0441\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c Home Assistant. \u042d\u0442\u043e \u043d\u0435 \u043c\u0435\u043d\u044f\u0435\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0441\u0430\u043c\u043e\u0433\u043e \u0445\u0430\u0431\u0430. \u0427\u0442\u043e\u0431\u044b \u0438\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0445\u0430\u0431\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Hub." }, + "change_plm_config": { + "data": { + "device": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "description": "\u0418\u0437\u043c\u0435\u043d\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 Insteon PLM. \u041f\u043e\u0441\u043b\u0435 \u0432\u043d\u0435\u0441\u0435\u043d\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c Home Assistant. \u042d\u0442\u043e \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u0438\u0442 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0441\u0430\u043c\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." + }, "init": { "data": { "add_override": "\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", "add_x10": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e X10", "change_hub_config": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0445\u0430\u0431\u0430", + "change_plm_config": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e PLM", "remove_override": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", "remove_x10": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e X10" } diff --git a/homeassistant/components/insteon/translations/sk.json b/homeassistant/components/insteon/translations/sk.json index d3c2a12b202..cc2f7c67f07 100644 --- a/homeassistant/components/insteon/translations/sk.json +++ b/homeassistant/components/insteon/translations/sk.json @@ -80,11 +80,18 @@ }, "description": "Zme\u0148te inform\u00e1cie o pripojen\u00ed Insteon Hub. Po vykonan\u00ed tejto zmeny mus\u00edte Home Assistant re\u0161tartova\u0165. Toto nemen\u00ed konfigur\u00e1ciu samotn\u00e9ho Hubu. Ak chcete zmeni\u0165 konfigur\u00e1ciu v Hub, pou\u017eite aplik\u00e1ciu Hub." }, + "change_plm_config": { + "data": { + "device": "Cesta k zariadeniu USB" + }, + "description": "Zme\u0148te inform\u00e1cie o pripojen\u00ed Insteon PLM. Po vykonan\u00ed tejto zmeny mus\u00edte Home Assistant re\u0161tartova\u0165. Toto nemen\u00ed konfigur\u00e1ciu samotn\u00e9ho PLM." + }, "init": { "data": { "add_override": "Pridanie prep\u00edsania zariadenia.", "add_x10": "Pridajte zariadenie X10.", "change_hub_config": "Zme\u0148te konfigur\u00e1ciu hubu.", + "change_plm_config": "Zmena konfigur\u00e1cie PLM.", "remove_override": "Odstr\u00e1nenie prep\u00edsania zariadenia.", "remove_x10": "Odstr\u00e1\u0148te zariadenie X10." } diff --git a/homeassistant/components/insteon/translations/tr.json b/homeassistant/components/insteon/translations/tr.json index 2bf143699f4..54ae0a4b7d8 100644 --- a/homeassistant/components/insteon/translations/tr.json +++ b/homeassistant/components/insteon/translations/tr.json @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "confirm_usb": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "hubv1": { "data": { diff --git a/homeassistant/components/insteon/translations/uk.json b/homeassistant/components/insteon/translations/uk.json index 3d450d8d973..e56f9d1a955 100644 --- a/homeassistant/components/insteon/translations/uk.json +++ b/homeassistant/components/insteon/translations/uk.json @@ -75,11 +75,17 @@ }, "description": "\u0417\u043c\u0456\u043d\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f Insteon Hub. \u041f\u0456\u0441\u043b\u044f \u0432\u043d\u0435\u0441\u0435\u043d\u043d\u044f \u0446\u0438\u0445 \u0437\u043c\u0456\u043d \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 Home Assistant. \u0426\u0435 \u043d\u0435 \u0437\u043c\u0456\u043d\u044e\u0454 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0441\u0430\u043c\u043e\u0433\u043e \u0445\u0430\u0431\u0430. \u0429\u043e\u0431 \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0445\u0430\u0431\u0430, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Hub." }, + "change_plm_config": { + "data": { + "device": "\u0428\u043b\u044f\u0445 USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + }, "init": { "data": { "add_override": "\u041f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", "add_x10": "\u0414\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 X10", "change_hub_config": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0445\u0430\u0431\u0430", + "change_plm_config": "\u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e PLM.", "remove_override": "\u0412\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u043f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", "remove_x10": "\u0412\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 X10" } diff --git a/homeassistant/components/insteon/translations/zh-Hant.json b/homeassistant/components/insteon/translations/zh-Hant.json index 608b6929e52..621c24548de 100644 --- a/homeassistant/components/insteon/translations/zh-Hant.json +++ b/homeassistant/components/insteon/translations/zh-Hant.json @@ -80,11 +80,18 @@ }, "description": "\u8b8a\u66f4 Insteon Hub \u9023\u7dda\u8cc7\u8a0a\u3002\u65bc\u8b8a\u66f4\u4e4b\u5f8c\u3001\u5fc5\u9808\u91cd\u555f Home Assistant\u3002\u6b64\u4e9b\u8a2d\u5b9a\u4e0d\u6703\u8b8a\u66f4 Hub \u88dd\u7f6e\u672c\u8eab\u7684\u8a2d\u5b9a\uff0c\u5982\u6b32\u8b8a\u66f4 Hub \u8a2d\u5b9a\u3001\u5247\u8acb\u4f7f\u7528 Hub app\u3002" }, + "change_plm_config": { + "data": { + "device": "USB \u88dd\u7f6e\u8def\u5f91" + }, + "description": "\u8b8a\u66f4 PLM \u9023\u7dda\u8cc7\u8a0a\u3002\u65bc\u8b8a\u66f4\u4e4b\u5f8c\u3001\u5fc5\u9808\u91cd\u555f Home Assistant\u3002\u6b64\u4e9b\u8a2d\u5b9a\u4e0d\u6703\u8b8a\u66f4 PLM \u88dd\u7f6e\u672c\u8eab\u7684\u8a2d\u5b9a\u3002" + }, "init": { "data": { "add_override": "\u65b0\u589e\u88dd\u7f6e\u8986\u5beb\u3002", "add_x10": "\u65b0\u589e X10 \u88dd\u7f6e\u3002", "change_hub_config": "\u8b8a\u66f4 Hub \u8a2d\u5b9a\u3002", + "change_plm_config": "\u8b8a\u66f4 PLM \u8a2d\u5b9a\u3002", "remove_override": "\u79fb\u9664\u88dd\u7f6e\u8986\u5beb", "remove_x10": "\u79fb\u9664 X10 \u88dd\u7f6e\u3002" } diff --git a/homeassistant/components/integration/config_flow.py b/homeassistant/components/integration/config_flow.py index 147c4262319..65840383926 100644 --- a/homeassistant/components/integration/config_flow.py +++ b/homeassistant/components/integration/config_flow.py @@ -33,15 +33,15 @@ UNIT_PREFIXES = [ selector.SelectOptionDict(value="T", label="T (tera)"), ] TIME_UNITS = [ - selector.SelectOptionDict(value=UnitOfTime.SECONDS, label="s (seconds)"), - selector.SelectOptionDict(value=UnitOfTime.MINUTES, label="min (minutes)"), - selector.SelectOptionDict(value=UnitOfTime.HOURS, label="h (hours)"), - selector.SelectOptionDict(value=UnitOfTime.DAYS, label="d (days)"), + UnitOfTime.SECONDS, + UnitOfTime.MINUTES, + UnitOfTime.HOURS, + UnitOfTime.DAYS, ] INTEGRATION_METHODS = [ - selector.SelectOptionDict(value=METHOD_TRAPEZOIDAL, label="Trapezoidal rule"), - selector.SelectOptionDict(value=METHOD_LEFT, label="Left Riemann sum"), - selector.SelectOptionDict(value=METHOD_RIGHT, label="Right Riemann sum"), + METHOD_TRAPEZOIDAL, + METHOD_LEFT, + METHOD_RIGHT, ] OPTIONS_SCHEMA = vol.Schema( @@ -61,7 +61,9 @@ CONFIG_SCHEMA = vol.Schema( selector.EntitySelectorConfig(domain=SENSOR_DOMAIN) ), vol.Required(CONF_METHOD, default=METHOD_TRAPEZOIDAL): selector.SelectSelector( - selector.SelectSelectorConfig(options=INTEGRATION_METHODS), + selector.SelectSelectorConfig( + options=INTEGRATION_METHODS, translation_key=CONF_METHOD + ), ), vol.Required(CONF_ROUND_DIGITS, default=2): selector.NumberSelector( selector.NumberSelectorConfig( @@ -76,7 +78,9 @@ CONFIG_SCHEMA = vol.Schema( ), vol.Required(CONF_UNIT_TIME, default=UnitOfTime.HOURS): selector.SelectSelector( selector.SelectSelectorConfig( - options=TIME_UNITS, mode=selector.SelectSelectorMode.DROPDOWN + options=TIME_UNITS, + mode=selector.SelectSelectorMode.DROPDOWN, + translation_key=CONF_UNIT_TIME, ), ), } diff --git a/homeassistant/components/integration/strings.json b/homeassistant/components/integration/strings.json index 4eb3b952a78..3a3940ffc2c 100644 --- a/homeassistant/components/integration/strings.json +++ b/homeassistant/components/integration/strings.json @@ -32,5 +32,22 @@ } } } + }, + "selector": { + "method": { + "options": { + "trapezoidal": "Trapezoidal rule", + "left": "Left Riemann sum", + "right": "Right Riemann sum" + } + }, + "unit_time": { + "options": { + "s": "Seconds", + "min": "Minutes", + "h": "Hours", + "d": "Days" + } + } } } diff --git a/homeassistant/components/integration/translations/bg.json b/homeassistant/components/integration/translations/bg.json index ac35010224f..83e0946606d 100644 --- a/homeassistant/components/integration/translations/bg.json +++ b/homeassistant/components/integration/translations/bg.json @@ -8,5 +8,15 @@ } } } + }, + "selector": { + "unit_time": { + "options": { + "d": "\u0414\u043d\u0438", + "h": "\u0427\u0430\u0441\u0430", + "min": "\u041c\u0438\u043d\u0443\u0442\u0438", + "s": "\u0421\u0435\u043a\u0443\u043d\u0434\u0438" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/integration/translations/ca.json b/homeassistant/components/integration/translations/ca.json index ad9553ff346..efa3404ba17 100644 --- a/homeassistant/components/integration/translations/ca.json +++ b/homeassistant/components/integration/translations/ca.json @@ -32,5 +32,22 @@ } } }, + "selector": { + "method": { + "options": { + "left": "Suma de Riemann esquerra", + "right": "Suma de Riemann dreta", + "trapezoidal": "Regla trapezoidal" + } + }, + "unit_time": { + "options": { + "d": "Dies", + "h": "Hores", + "min": "Minuts", + "s": "Segons" + } + } + }, "title": "Integraci\u00f3 - Sensor integral de suma de Riemann" } \ No newline at end of file diff --git a/homeassistant/components/integration/translations/de.json b/homeassistant/components/integration/translations/de.json index c01af414971..cfb1395af1e 100644 --- a/homeassistant/components/integration/translations/de.json +++ b/homeassistant/components/integration/translations/de.json @@ -32,5 +32,22 @@ } } }, + "selector": { + "method": { + "options": { + "left": "Linke Riemannsche Summe", + "right": "Rechte Riemannsche Summe", + "trapezoidal": "Trapezregel" + } + }, + "unit_time": { + "options": { + "d": "Tage", + "h": "Stunden", + "min": "Minuten", + "s": "Sekunden" + } + } + }, "title": "Integration - Riemann Summenintegralsensor" } \ No newline at end of file diff --git a/homeassistant/components/integration/translations/el.json b/homeassistant/components/integration/translations/el.json index 6893190d9bf..ed866e34ad9 100644 --- a/homeassistant/components/integration/translations/el.json +++ b/homeassistant/components/integration/translations/el.json @@ -8,15 +8,15 @@ "round": "\u0391\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1", "source": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5", "unit_prefix": "\u039c\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1", - "unit_time": "\u03a7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + "unit_time": "\u039c\u03bf\u03bd\u03ac\u03b4\u03b1 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5" }, "data_description": { "round": "\u0395\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf.", "unit_prefix": "\u0397 \u03ad\u03be\u03bf\u03b4\u03bf\u03c2 \u03b8\u03b1 \u03ba\u03bb\u03b9\u03bc\u03b1\u03ba\u03ce\u03bd\u03b5\u03c4\u03b1\u03b9 \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1.", "unit_time": "\u0397 \u03ad\u03be\u03bf\u03b4\u03bf\u03c2 \u03b8\u03b1 \u03ba\u03bb\u03b9\u03bc\u03b1\u03ba\u03c9\u03b8\u03b5\u03af \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5." }, - "description": "\u0397 \u03b1\u03ba\u03c1\u03af\u03b2\u03b5\u03b9\u03b1 \u03b5\u03bb\u03ad\u03b3\u03c7\u03b5\u03b9 \u03c4\u03bf\u03bd \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc \u03c4\u03c9\u03bd \u03b4\u03b5\u03ba\u03b1\u03b4\u03b9\u03ba\u03ce\u03bd \u03c8\u03b7\u03c6\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03ad\u03be\u03bf\u03b4\u03bf.\n\u03a4\u03bf \u03ac\u03b8\u03c1\u03bf\u03b9\u03c3\u03bc\u03b1 \u03b8\u03b1 \u03ba\u03bb\u03b9\u03bc\u03b1\u03ba\u03ce\u03bd\u03b5\u03c4\u03b1\u03b9 \u03c3\u03cd\u03bc\u03c6\u03c9\u03bd\u03b1 \u03bc\u03b5 \u03c4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03b5\u03c4\u03c1\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03ba\u03b1\u03b9 \u03c4\u03bf \u03c7\u03c1\u03cc\u03bd\u03bf \u03bf\u03bb\u03bf\u03ba\u03bb\u03ae\u03c1\u03c9\u03c3\u03b7\u03c2.", - "title": "\u039d\u03ad\u03bf\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + "description": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03af\u03b6\u03b5\u03b9 \u03ad\u03bd\u03b1 \u03ac\u03b8\u03c1\u03bf\u03b9\u03c3\u03bc\u03b1 Riemann \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03ba\u03c4\u03b9\u03bc\u03ae\u03c3\u03b5\u03b9 \u03c4\u03bf \u03bf\u03bb\u03bf\u03ba\u03bb\u03ae\u03c1\u03c9\u03bc\u03b1 \u03b5\u03bd\u03cc\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1.", + "title": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03b1\u03c4\u03c9\u03bc\u03ad\u03bd\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b1\u03b8\u03c1\u03bf\u03af\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2 Riemann" } } }, diff --git a/homeassistant/components/integration/translations/en.json b/homeassistant/components/integration/translations/en.json index 1ee047b447f..ea9368692ab 100644 --- a/homeassistant/components/integration/translations/en.json +++ b/homeassistant/components/integration/translations/en.json @@ -32,5 +32,22 @@ } } }, + "selector": { + "method": { + "options": { + "left": "Left Riemann sum", + "right": "Right Riemann sum", + "trapezoidal": "Trapezoidal rule" + } + }, + "unit_time": { + "options": { + "d": "Days", + "h": "Hours", + "min": "Minutes", + "s": "Seconds" + } + } + }, "title": "Integration - Riemann sum integral sensor" } \ No newline at end of file diff --git a/homeassistant/components/integration/translations/et.json b/homeassistant/components/integration/translations/et.json index 4b0143d0662..866f4ed13a1 100644 --- a/homeassistant/components/integration/translations/et.json +++ b/homeassistant/components/integration/translations/et.json @@ -32,5 +32,22 @@ } } }, + "selector": { + "method": { + "options": { + "left": "Vasak Riemanni summa", + "right": "Parem Riemanni summa", + "trapezoidal": "Trapetsreegel" + } + }, + "unit_time": { + "options": { + "d": "p\u00e4eva", + "h": "tundi", + "min": "minutit", + "s": "sekundit" + } + } + }, "title": "Sidumine - Riemanni integraalsumma andur" } \ No newline at end of file diff --git a/homeassistant/components/integration/translations/pl.json b/homeassistant/components/integration/translations/pl.json index 5dfe00cd31b..75407a3b89a 100644 --- a/homeassistant/components/integration/translations/pl.json +++ b/homeassistant/components/integration/translations/pl.json @@ -32,5 +32,22 @@ } } }, + "selector": { + "method": { + "options": { + "left": "lewa suma Riemanna", + "right": "prawa suma Riemanna", + "trapezoidal": "metoda trapez\u00f3w" + } + }, + "unit_time": { + "options": { + "d": "dni", + "h": "godziny", + "min": "minuty", + "s": "sekundy" + } + } + }, "title": "Integracja - czujnik ca\u0142kuj\u0105cy sum\u0119 Riemanna" } \ No newline at end of file diff --git a/homeassistant/components/integration/translations/ru.json b/homeassistant/components/integration/translations/ru.json index 8e7ad96b803..878448d8194 100644 --- a/homeassistant/components/integration/translations/ru.json +++ b/homeassistant/components/integration/translations/ru.json @@ -32,5 +32,22 @@ } } }, + "selector": { + "method": { + "options": { + "left": "\u041b\u0435\u0432\u0430\u044f \u0441\u0443\u043c\u043c\u0430 \u0420\u0438\u043c\u0430\u043d\u0430", + "right": "\u041f\u0440\u0430\u0432\u0430\u044f \u0441\u0443\u043c\u043c\u0430 \u0420\u0438\u043c\u0430\u043d\u0430", + "trapezoidal": "\u041c\u0435\u0442\u043e\u0434 \u0442\u0440\u0430\u043f\u0435\u0446\u0438\u0439" + } + }, + "unit_time": { + "options": { + "d": "\u0414\u043d\u0438", + "h": "\u0427\u0430\u0441\u044b", + "min": "\u041c\u0438\u043d\u0443\u0442\u044b", + "s": "\u0421\u0435\u043a\u0443\u043d\u0434\u044b" + } + } + }, "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u043b \u0420\u0438\u043c\u0430\u043d\u0430" } \ No newline at end of file diff --git a/homeassistant/components/integration/translations/uk.json b/homeassistant/components/integration/translations/uk.json new file mode 100644 index 00000000000..fe3fc997183 --- /dev/null +++ b/homeassistant/components/integration/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/integration/translations/zh-Hant.json b/homeassistant/components/integration/translations/zh-Hant.json index d7142365b05..073840568a4 100644 --- a/homeassistant/components/integration/translations/zh-Hant.json +++ b/homeassistant/components/integration/translations/zh-Hant.json @@ -32,5 +32,22 @@ } } }, + "selector": { + "method": { + "options": { + "left": "\u5de6\u9ece\u66fc\u548c", + "right": "\u53f3\u9ece\u66fc\u548c", + "trapezoidal": "\u68af\u5f62\u516c\u5f0f" + } + }, + "unit_time": { + "options": { + "d": "\u5929", + "h": "\u5c0f\u6642", + "min": "\u5206\u9418", + "s": "\u79d2\u9418" + } + } + }, "title": "\u6574\u5408 - \u9ece\u66fc\u548c\u7a4d\u5206\u611f\u6e2c\u5668" } \ No newline at end of file diff --git a/homeassistant/components/intellifire/__init__.py b/homeassistant/components/intellifire/__init__.py index e4e4f1a66c9..81ef383dfab 100644 --- a/homeassistant/components/intellifire/__init__.py +++ b/homeassistant/components/intellifire/__init__.py @@ -24,6 +24,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.FAN, + Platform.LIGHT, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, diff --git a/homeassistant/components/intellifire/light.py b/homeassistant/components/intellifire/light.py new file mode 100644 index 00000000000..f1fd81ab452 --- /dev/null +++ b/homeassistant/components/intellifire/light.py @@ -0,0 +1,97 @@ +"""The IntelliFire Light.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any + +from intellifire4py import IntellifireControlAsync, IntellifirePollData + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ColorMode, + LightEntity, + LightEntityDescription, +) +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 IntellifireDataUpdateCoordinator +from .entity import IntellifireEntity + + +@dataclass +class IntellifireLightRequiredKeysMixin: + """Required keys for fan entity.""" + + set_fn: Callable[[IntellifireControlAsync, int], Awaitable] + value_fn: Callable[[IntellifirePollData], bool] + + +@dataclass +class IntellifireLightEntityDescription( + LightEntityDescription, IntellifireLightRequiredKeysMixin +): + """Describes a light entity.""" + + +INTELLIFIRE_LIGHTS: tuple[IntellifireLightEntityDescription, ...] = ( + IntellifireLightEntityDescription( + key="lights", + name="Lights", + has_entity_name=True, + set_fn=lambda control_api, level: control_api.set_lights(level=level), + value_fn=lambda data: data.light_level, + ), +) + + +class IntellifireLight(IntellifireEntity, LightEntity): + """This is a Light entity for the fireplace.""" + + entity_description: IntellifireLightEntityDescription + _attr_color_mode = ColorMode.BRIGHTNESS + _attr_supported_color_modes = {ColorMode.BRIGHTNESS} + + @property + def brightness(self): + """Return the current brightness 0-255.""" + return 85 * self.entity_description.value_fn(self.coordinator.read_api.data) + + @property + def is_on(self): + """Return true if light is on.""" + return self.entity_description.value_fn(self.coordinator.read_api.data) >= 1 + + async def async_turn_on(self, **kwargs: Any) -> None: + """Instruct the light to turn on.""" + if ATTR_BRIGHTNESS in kwargs: + light_level = int(kwargs[ATTR_BRIGHTNESS] / 85) + else: + light_level = 2 + + await self.entity_description.set_fn(self.coordinator.control_api, light_level) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Instruct the light to turn off.""" + await self.entity_description.set_fn(self.coordinator.control_api, 0) + await self.coordinator.async_request_refresh() + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the fans.""" + coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + if coordinator.data.has_light: + async_add_entities( + IntellifireLight(coordinator=coordinator, description=description) + for description in INTELLIFIRE_LIGHTS + ) + return diff --git a/homeassistant/components/intellifire/translations/el.json b/homeassistant/components/intellifire/translations/el.json index ff286c04952..ca880527e30 100644 --- a/homeassistant/components/intellifire/translations/el.json +++ b/homeassistant/components/intellifire/translations/el.json @@ -19,11 +19,11 @@ } }, "dhcp_confirm": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {host}\nSerial: {serial}?" + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 {host}\nSerial: {serial};" }, "manual_device_entry": { "data": { - "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2" + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2 (\u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP)" }, "description": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7" }, diff --git a/homeassistant/components/intellifire/translations/lv.json b/homeassistant/components/intellifire/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/intellifire/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/intellifire/translations/tr.json b/homeassistant/components/intellifire/translations/tr.json index 1eba2a43e72..60fec51c98a 100644 --- a/homeassistant/components/intellifire/translations/tr.json +++ b/homeassistant/components/intellifire/translations/tr.json @@ -19,7 +19,7 @@ } }, "dhcp_confirm": { - "description": "Kurulumu yapmak {host} ister misiniz?\nSeri No: {serial}?" + "description": "{host} kurmak istiyor musunuz?\n Seri: {serial} ?" }, "manual_device_entry": { "data": { diff --git a/homeassistant/components/intellifire/translations/uk.json b/homeassistant/components/intellifire/translations/uk.json new file mode 100644 index 00000000000..9f12e1d2924 --- /dev/null +++ b/homeassistant/components/intellifire/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "api_config": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0415\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430 \u043f\u043e\u0448\u0442\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index c6ca9212c74..874a9f1120c 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -2,9 +2,19 @@ import voluptuous as vol from homeassistant.components import http +from homeassistant.components.cover import ( + DOMAIN as COVER_DOMAIN, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, +) from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.const import SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON -from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import DOMAIN as HA_DOMAIN, HomeAssistant, State from homeassistant.helpers import config_validation as cv, integration_platform, intent from homeassistant.helpers.typing import ConfigType @@ -21,26 +31,43 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: intent.async_register( hass, - intent.ServiceIntentHandler( - intent.INTENT_TURN_ON, HA_DOMAIN, SERVICE_TURN_ON, "Turned {} on" - ), + OnOffIntentHandler(intent.INTENT_TURN_ON, HA_DOMAIN, SERVICE_TURN_ON), ) intent.async_register( hass, - intent.ServiceIntentHandler( - intent.INTENT_TURN_OFF, HA_DOMAIN, SERVICE_TURN_OFF, "Turned {} off" - ), + OnOffIntentHandler(intent.INTENT_TURN_OFF, HA_DOMAIN, SERVICE_TURN_OFF), ) intent.async_register( hass, - intent.ServiceIntentHandler( - intent.INTENT_TOGGLE, HA_DOMAIN, SERVICE_TOGGLE, "Toggled {}" - ), + intent.ServiceIntentHandler(intent.INTENT_TOGGLE, HA_DOMAIN, SERVICE_TOGGLE), ) return True +class OnOffIntentHandler(intent.ServiceIntentHandler): + """Intent handler for on/off that handles covers too.""" + + async def async_call_service(self, intent_obj: intent.Intent, state: State) -> None: + """Call service on entity with special case for covers.""" + hass = intent_obj.hass + + if state.domain == COVER_DOMAIN: + # on = open + # off = close + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER + if self.service == SERVICE_TURN_ON + else SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: state.entity_id}, + context=intent_obj.context, + ) + else: + # Fall back to homeassistant.turn_on/off + await super().async_call_service(intent_obj, state) + + async def _async_process_intent(hass: HomeAssistant, domain: str, platform): """Process the intents of an integration.""" await platform.async_setup_intents(hass) diff --git a/homeassistant/components/ios/translations/tr.json b/homeassistant/components/ios/translations/tr.json index 8de4663957e..50e6a992c9b 100644 --- a/homeassistant/components/ios/translations/tr.json +++ b/homeassistant/components/ios/translations/tr.json @@ -5,7 +5,7 @@ }, "step": { "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } } diff --git a/homeassistant/components/iotawatt/const.py b/homeassistant/components/iotawatt/const.py index 0b80e108238..db847f3dfe8 100644 --- a/homeassistant/components/iotawatt/const.py +++ b/homeassistant/components/iotawatt/const.py @@ -9,6 +9,4 @@ DOMAIN = "iotawatt" VOLT_AMPERE_REACTIVE = "VAR" VOLT_AMPERE_REACTIVE_HOURS = "VARh" -ATTR_LAST_UPDATE = "last_update" - CONNECTION_ERRORS = (KeyError, json.JSONDecodeError, httpx.HTTPError) diff --git a/homeassistant/components/iotawatt/sensor.py b/homeassistant/components/iotawatt/sensor.py index 4839b8af8a7..0870e2234dc 100644 --- a/homeassistant/components/iotawatt/sensor.py +++ b/homeassistant/components/iotawatt/sensor.py @@ -27,17 +27,11 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity, entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt -from .const import ( - ATTR_LAST_UPDATE, - DOMAIN, - VOLT_AMPERE_REACTIVE, - VOLT_AMPERE_REACTIVE_HOURS, -) +from .const import DOMAIN, VOLT_AMPERE_REACTIVE, VOLT_AMPERE_REACTIVE_HOURS from .coordinator import IotawattUpdater _LOGGER = logging.getLogger(__name__) @@ -134,10 +128,6 @@ async def async_setup_entry( description = ENTITY_DESCRIPTION_KEY_MAP.get( data.getUnit(), IotaWattSensorEntityDescription("base_sensor") ) - if data.getUnit() == "WattHours" and not data.getFromStart(): - return IotaWattAccumulatingSensor( - coordinator=coordinator, key=key, entity_description=description - ) return IotaWattSensor( coordinator=coordinator, @@ -235,77 +225,3 @@ class IotaWattSensor(CoordinatorEntity[IotawattUpdater], SensorEntity): return func(self._sensor_data.getValue()) return self._sensor_data.getValue() - - -class IotaWattAccumulatingSensor(IotaWattSensor, RestoreEntity): - """Defines a IoTaWatt Accumulative Energy (High Accuracy) Sensor.""" - - def __init__( - self, - coordinator: IotawattUpdater, - key: str, - entity_description: IotaWattSensorEntityDescription, - ) -> None: - """Initialize the sensor.""" - - super().__init__(coordinator, key, entity_description) - - if self._attr_unique_id is not None: - self._attr_unique_id += ".accumulated" - - self._accumulated_value: float | None = None - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - assert ( - self._accumulated_value is not None - ), "async_added_to_hass must have been called first" - self._accumulated_value += float(self._sensor_data.getValue()) - - super()._handle_coordinator_update() - - @property - def native_value(self) -> StateType: - """Return the state of the sensor.""" - if self._accumulated_value is None: - return None - return round(self._accumulated_value, 1) - - async def async_added_to_hass(self) -> None: - """Load the last known state value of the entity if the accumulated type.""" - await super().async_added_to_hass() - state = await self.async_get_last_state() - self._accumulated_value = 0.0 - if state: - try: - # Previous value could be `unknown` if the connection didn't originally - # complete. - self._accumulated_value = float(state.state) - except (ValueError) as err: - _LOGGER.warning("Could not restore last state: %s", err) - else: - if ATTR_LAST_UPDATE in state.attributes: - last_run = dt.parse_datetime(state.attributes[ATTR_LAST_UPDATE]) - if last_run is not None: - self.coordinator.update_last_run(last_run) - # Force a second update from the iotawatt to ensure that sensors are up to date. - await self.coordinator.async_request_refresh() - - @property - def name(self) -> str | None: - """Return name of the entity.""" - return f"{self._sensor_data.getSourceName()} Accumulated" - - @property - def extra_state_attributes(self) -> dict[str, str]: - """Return the extra state attributes of the entity.""" - attrs = super().extra_state_attributes - - assert ( - self.coordinator.api is not None - and self.coordinator.api.getLastUpdateTime() is not None - ) - attrs[ATTR_LAST_UPDATE] = self.coordinator.api.getLastUpdateTime().isoformat() - - return attrs diff --git a/homeassistant/components/iotawatt/translations/uk.json b/homeassistant/components/iotawatt/translations/uk.json new file mode 100644 index 00000000000..5238fd9e822 --- /dev/null +++ b/homeassistant/components/iotawatt/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "auth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0414\u043b\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e IoTawatt \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c \u0456 \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443 \u041d\u0430\u0434\u0456\u0441\u043b\u0430\u0442\u0438." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iperf3/__init__.py b/homeassistant/components/iperf3/__init__.py index 951397e7e61..0448c3e48b2 100644 --- a/homeassistant/components/iperf3/__init__.py +++ b/homeassistant/components/iperf3/__init__.py @@ -11,6 +11,7 @@ from homeassistant.components.sensor import ( DOMAIN as SENSOR_DOMAIN, SensorDeviceClass, SensorEntityDescription, + SensorStateClass, ) from homeassistant.const import ( CONF_HOST, @@ -52,12 +53,16 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=ATTR_DOWNLOAD, name=ATTR_DOWNLOAD.capitalize(), + icon="mdi:download", + state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, ), SensorEntityDescription( key=ATTR_UPLOAD, name=ATTR_UPLOAD.capitalize(), + icon="mdi:upload", + state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DATA_RATE, native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, ), diff --git a/homeassistant/components/iperf3/sensor.py b/homeassistant/components/iperf3/sensor.py index 3d13c302606..1b68709b934 100644 --- a/homeassistant/components/iperf3/sensor.py +++ b/homeassistant/components/iperf3/sensor.py @@ -11,8 +11,6 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import ATTR_VERSION, DATA_UPDATED, DOMAIN as IPERF3_DOMAIN, SENSOR_TYPES -ICON = "mdi:speedometer" - ATTR_PROTOCOL = "Protocol" ATTR_REMOTE_HOST = "Remote Server" ATTR_REMOTE_PORT = "Remote Port" @@ -41,7 +39,6 @@ class Iperf3Sensor(RestoreEntity, SensorEntity): """A Iperf3 sensor implementation.""" _attr_attribution = "Data retrieved using Iperf3" - _attr_icon = ICON _attr_should_poll = False def __init__(self, iperf3_data, description: SensorEntityDescription): diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py index 293a6378b78..5058f6d10a8 100644 --- a/homeassistant/components/ipp/sensor.py +++ b/homeassistant/components/ipp/sensor.py @@ -66,11 +66,13 @@ class IPPSensor(IPPEntity, SensorEntity): key: str, name: str, unit_of_measurement: str | None = None, + translation_key: str | None = None, ) -> None: """Initialize IPP sensor.""" self._key = key self._attr_unique_id = f"{unique_id}_{key}" self._attr_native_unit_of_measurement = unit_of_measurement + self._attr_translation_key = translation_key super().__init__( entry_id=entry_id, @@ -136,6 +138,9 @@ class IPPMarkerSensor(IPPSensor): class IPPPrinterSensor(IPPSensor): """Defines an IPP printer sensor.""" + _attr_device_class = SensorDeviceClass.ENUM + _attr_options = ["idle", "printing", "stopped"] + def __init__( self, entry_id: str, unique_id: str, coordinator: IPPDataUpdateCoordinator ) -> None: @@ -148,6 +153,7 @@ class IPPPrinterSensor(IPPSensor): key="printer", name=coordinator.data.info.name, unit_of_measurement=None, + translation_key="printer", ) @property diff --git a/homeassistant/components/ipp/strings.json b/homeassistant/components/ipp/strings.json index c43b9a25463..fa7dd9b6bf8 100644 --- a/homeassistant/components/ipp/strings.json +++ b/homeassistant/components/ipp/strings.json @@ -31,5 +31,16 @@ "parse_error": "Failed to parse response from printer.", "unique_id_required": "Device missing unique identification required for discovery." } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "printing": "Printing", + "idle": "Idle", + "stopped": "Stopped" + } + } + } } } diff --git a/homeassistant/components/ipp/translations/ca.json b/homeassistant/components/ipp/translations/ca.json index 6d91535942a..cf68b459168 100644 --- a/homeassistant/components/ipp/translations/ca.json +++ b/homeassistant/components/ipp/translations/ca.json @@ -31,5 +31,16 @@ "title": "Impressora descoberta" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Inactiva", + "printing": "Imprimint", + "stopped": "Aturada" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/de.json b/homeassistant/components/ipp/translations/de.json index 9886bf9c0ef..15fbe450ce6 100644 --- a/homeassistant/components/ipp/translations/de.json +++ b/homeassistant/components/ipp/translations/de.json @@ -31,5 +31,16 @@ "title": "Entdeckter Drucker" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Unt\u00e4tig", + "printing": "Druckt", + "stopped": "Angehalten" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/el.json b/homeassistant/components/ipp/translations/el.json index ec29a112b61..dfe4cc7d408 100644 --- a/homeassistant/components/ipp/translations/el.json +++ b/homeassistant/components/ipp/translations/el.json @@ -31,5 +31,16 @@ "title": "\u0391\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b5\u03ba\u03c4\u03c5\u03c0\u03c9\u03c4\u03ae\u03c2" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "\u03a3\u03b5 \u03b1\u03b4\u03c1\u03ac\u03bd\u03b5\u03b9\u03b1", + "printing": "\u0395\u03ba\u03c4\u03c5\u03c0\u03ce\u03bd\u03b5\u03b9", + "stopped": "\u03a3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/en-GB.json b/homeassistant/components/ipp/translations/en-GB.json new file mode 100644 index 00000000000..57e7b1f9422 --- /dev/null +++ b/homeassistant/components/ipp/translations/en-GB.json @@ -0,0 +1,13 @@ +{ + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Idle", + "printing": "Printing", + "stopped": "Stopped" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/en.json b/homeassistant/components/ipp/translations/en.json index a8bfba5ac32..91d5fceb5dc 100644 --- a/homeassistant/components/ipp/translations/en.json +++ b/homeassistant/components/ipp/translations/en.json @@ -31,5 +31,16 @@ "title": "Discovered printer" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Idle", + "printing": "Printing", + "stopped": "Stopped" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/es.json b/homeassistant/components/ipp/translations/es.json index 740462de0f4..7fdc8ba65ce 100644 --- a/homeassistant/components/ipp/translations/es.json +++ b/homeassistant/components/ipp/translations/es.json @@ -31,5 +31,16 @@ "title": "Impresora descubierta" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Inactivo", + "printing": "Imprimiendo", + "stopped": "Detenido" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/et.json b/homeassistant/components/ipp/translations/et.json index b7e6c8b746f..580b04ebbe1 100644 --- a/homeassistant/components/ipp/translations/et.json +++ b/homeassistant/components/ipp/translations/et.json @@ -31,5 +31,16 @@ "title": "Avastatud printer" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Ootel", + "printing": "Tr\u00fckkimine", + "stopped": "Peatatud" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/fr.json b/homeassistant/components/ipp/translations/fr.json index 4ae1cc602f4..182035e5cb2 100644 --- a/homeassistant/components/ipp/translations/fr.json +++ b/homeassistant/components/ipp/translations/fr.json @@ -31,5 +31,16 @@ "title": "Imprimante trouv\u00e9e" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Inactive", + "printing": "Impression", + "stopped": "Arr\u00eat\u00e9e" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/hu.json b/homeassistant/components/ipp/translations/hu.json index 471c73c8abc..507b057d0e2 100644 --- a/homeassistant/components/ipp/translations/hu.json +++ b/homeassistant/components/ipp/translations/hu.json @@ -31,5 +31,16 @@ "title": "Felfedezett nyomtat\u00f3" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "K\u00e9szenl\u00e9t", + "printing": "Nyomtat\u00e1s", + "stopped": "Meg\u00e1llt" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/id.json b/homeassistant/components/ipp/translations/id.json index f65b853d671..7bb29dbc039 100644 --- a/homeassistant/components/ipp/translations/id.json +++ b/homeassistant/components/ipp/translations/id.json @@ -31,5 +31,16 @@ "title": "Printer yang ditemukan" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Siaga", + "printing": "Mencetak", + "stopped": "Terhenti" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/it.json b/homeassistant/components/ipp/translations/it.json index 3c2df9a6ee0..6f3473906da 100644 --- a/homeassistant/components/ipp/translations/it.json +++ b/homeassistant/components/ipp/translations/it.json @@ -31,5 +31,16 @@ "title": "Rilevata stampante" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Inattiva", + "printing": "In stampa", + "stopped": "Fermata" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/lv.json b/homeassistant/components/ipp/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/ipp/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/nl.json b/homeassistant/components/ipp/translations/nl.json index 2a97724a596..c502e056a0b 100644 --- a/homeassistant/components/ipp/translations/nl.json +++ b/homeassistant/components/ipp/translations/nl.json @@ -31,5 +31,14 @@ "title": "Gedetecteerde printer" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Inactief" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/no.json b/homeassistant/components/ipp/translations/no.json index 69c0cd86346..fa9b0633062 100644 --- a/homeassistant/components/ipp/translations/no.json +++ b/homeassistant/components/ipp/translations/no.json @@ -31,5 +31,16 @@ "title": "Oppdaget skriver" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Tomgang", + "printing": "Printing", + "stopped": "Stoppet" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/pl.json b/homeassistant/components/ipp/translations/pl.json index b44904095de..179fbba8b2b 100644 --- a/homeassistant/components/ipp/translations/pl.json +++ b/homeassistant/components/ipp/translations/pl.json @@ -31,5 +31,16 @@ "title": "Wykryto drukark\u0119" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "bezczynny", + "printing": "drukowanie", + "stopped": "zatrzymane" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/pt-BR.json b/homeassistant/components/ipp/translations/pt-BR.json index 7da66ff568b..abb7a2a6560 100644 --- a/homeassistant/components/ipp/translations/pt-BR.json +++ b/homeassistant/components/ipp/translations/pt-BR.json @@ -31,5 +31,16 @@ "title": "Impressora descoberta" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Ocioso", + "printing": "Impress\u00e3o", + "stopped": "Parado" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/ru.json b/homeassistant/components/ipp/translations/ru.json index e2a4413c211..d688d20ba2e 100644 --- a/homeassistant/components/ipp/translations/ru.json +++ b/homeassistant/components/ipp/translations/ru.json @@ -31,5 +31,16 @@ "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0439 \u043f\u0440\u0438\u043d\u0442\u0435\u0440" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "\u0411\u0435\u0437\u0434\u0435\u0439\u0441\u0442\u0432\u0443\u0435\u0442", + "printing": "\u041f\u0435\u0447\u0430\u0442\u0430\u0435\u0442", + "stopped": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/sk.json b/homeassistant/components/ipp/translations/sk.json index 8583079b1a2..4c7cbbb8f10 100644 --- a/homeassistant/components/ipp/translations/sk.json +++ b/homeassistant/components/ipp/translations/sk.json @@ -31,5 +31,16 @@ "title": "Objaven\u00e1 tla\u010diare\u0148" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Ne\u010dinn\u00fd", + "printing": "Tla\u010d", + "stopped": "Zastaven\u00e9" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/tr.json b/homeassistant/components/ipp/translations/tr.json index e3d0251a1b0..4d8f3bebb40 100644 --- a/homeassistant/components/ipp/translations/tr.json +++ b/homeassistant/components/ipp/translations/tr.json @@ -31,5 +31,16 @@ "title": "Ke\u015ffedilen yaz\u0131c\u0131" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "Bo\u015fta", + "printing": "Yazd\u0131r\u0131l\u0131yor", + "stopped": "Durduruldu" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/uk.json b/homeassistant/components/ipp/translations/uk.json index bb6df07f1e4..caf51a52871 100644 --- a/homeassistant/components/ipp/translations/uk.json +++ b/homeassistant/components/ipp/translations/uk.json @@ -31,5 +31,14 @@ "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u043d\u0442\u0435\u0440" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "\u0411\u0435\u0437\u0434\u0456\u044f\u043b\u044c\u043d\u0456\u0441\u0442\u044c" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/zh-Hant.json b/homeassistant/components/ipp/translations/zh-Hant.json index efe960a6a2e..913b76d2447 100644 --- a/homeassistant/components/ipp/translations/zh-Hant.json +++ b/homeassistant/components/ipp/translations/zh-Hant.json @@ -31,5 +31,16 @@ "title": "\u81ea\u52d5\u641c\u7d22\u5230\u7684\u5370\u8868\u6a5f" } } + }, + "entity": { + "sensor": { + "printer": { + "state": { + "idle": "\u9592\u7f6e", + "printing": "\u5217\u5370\u4e2d", + "stopped": "\u5df2\u505c\u6b62" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index aad505e23c4..def58d60201 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -2,10 +2,10 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Coroutine from datetime import timedelta from functools import partial -from typing import Any, cast +from typing import Any from pyiqvia import Client from pyiqvia.errors import IQVIAError @@ -57,16 +57,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client.disable_request_retries() async def async_get_data_from_api( - api_coro: Callable[..., Awaitable] + api_coro: Callable[..., Coroutine[Any, Any, dict[str, Any]]] ) -> dict[str, Any]: """Get data from a particular API coroutine.""" try: - data = await api_coro() + return await api_coro() except IQVIAError as err: raise UpdateFailed from err - return cast(dict[str, Any], data) - coordinators = {} init_data_update_tasks = [] @@ -115,14 +113,14 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class IQVIAEntity(CoordinatorEntity): +class IQVIAEntity(CoordinatorEntity[DataUpdateCoordinator[dict[str, Any]]]): """Define a base IQVIA entity.""" _attr_has_entity_name = True def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[dict[str, Any]], entry: ConfigEntry, description: EntityDescription, ) -> None: diff --git a/homeassistant/components/iqvia/diagnostics.py b/homeassistant/components/iqvia/diagnostics.py index 664467b0702..6f2df6bd7d3 100644 --- a/homeassistant/components/iqvia/diagnostics.py +++ b/homeassistant/components/iqvia/diagnostics.py @@ -35,7 +35,9 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinators: dict[str, DataUpdateCoordinator] = hass.data[DOMAIN][entry.entry_id] + coordinators: dict[str, DataUpdateCoordinator[dict[str, Any]]] = hass.data[DOMAIN][ + entry.entry_id + ] return { "entry": async_redact_data(entry.as_dict(), TO_REDACT), diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 455711c0ead..0052e90880b 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from statistics import mean -from typing import NamedTuple +from typing import Any, NamedTuple, cast import numpy as np @@ -247,10 +247,11 @@ class IndexSensor(IQVIAEntity, SensorEntity): key = self.entity_description.key.split("_")[-1].title() try: - [period] = [p for p in data["periods"] if p["Type"] == key] - except ValueError: + [period] = [p for p in data["periods"] if p["Type"] == key] # type: ignore[index] + except TypeError: return + data = cast(dict[str, Any], data) [rating] = [ i.label for i in RATING_MAPPING if i.minimum <= period["Index"] <= i.maximum ] diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index 55c27c4cc59..7fd5ed4129f 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -26,10 +26,9 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up the Islamic Prayer Component.""" client = IslamicPrayerClient(hass, config_entry) - + hass.data[DOMAIN] = client await client.async_setup() - hass.data.setdefault(DOMAIN, client) return True @@ -155,7 +154,9 @@ class IslamicPrayerClient: await self.async_update() self.config_entry.add_update_listener(self.async_options_updated) - self.hass.config_entries.async_setup_platforms(self.config_entry, PLATFORMS) + await self.hass.config_entries.async_forward_entry_setups( + self.config_entry, PLATFORMS + ) return True diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 4fabb120102..c9e4f6ed16e 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -1,4 +1,4 @@ -"""Support the ISY-994 controllers.""" +"""Support the Universal Devices ISY/IoX controllers.""" from __future__ import annotations import asyncio @@ -14,17 +14,23 @@ from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, CONF_USERNAME, + CONF_VARIABLES, EVENT_HOMEASSISTANT_STOP, + Platform, ) from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv import homeassistant.helpers.device_registry as dr +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from .const import ( _LOGGER, CONF_IGNORE_STRING, + CONF_NETWORK, CONF_RESTORE_LIGHT_STATE, CONF_SENSOR_STRING, CONF_TLS_VER, @@ -34,42 +40,46 @@ from .const import ( DEFAULT_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING, DOMAIN, - ISY994_ISY, - ISY994_NODES, - ISY994_PROGRAMS, - ISY994_VARIABLES, + ISY_CONF_FIRMWARE, + ISY_CONF_MODEL, + ISY_CONF_NAME, + ISY_CONF_NETWORKING, MANUFACTURER, PLATFORMS, - PROGRAM_PLATFORMS, - SENSOR_AUX, + SCHEME_HTTP, + SCHEME_HTTPS, ) from .helpers import _categorize_nodes, _categorize_programs, _categorize_variables +from .models import IsyData from .services import async_setup_services, async_unload_services -from .util import unique_ids_for_config_entry_id +from .util import _async_cleanup_registry_entries CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_HOST): cv.url, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_TLS_VER): vol.Coerce(float), - vol.Optional( - CONF_IGNORE_STRING, default=DEFAULT_IGNORE_STRING - ): cv.string, - vol.Optional( - CONF_SENSOR_STRING, default=DEFAULT_SENSOR_STRING - ): cv.string, - vol.Optional( - CONF_VAR_SENSOR_STRING, default=DEFAULT_VAR_SENSOR_STRING - ): cv.string, - vol.Required( - CONF_RESTORE_LIGHT_STATE, default=DEFAULT_RESTORE_LIGHT_STATE - ): bool, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.url, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_TLS_VER): vol.Coerce(float), + vol.Optional( + CONF_IGNORE_STRING, default=DEFAULT_IGNORE_STRING + ): cv.string, + vol.Optional( + CONF_SENSOR_STRING, default=DEFAULT_SENSOR_STRING + ): cv.string, + vol.Optional( + CONF_VAR_SENSOR_STRING, default=DEFAULT_VAR_SENSOR_STRING + ): cv.string, + vol.Required( + CONF_RESTORE_LIGHT_STATE, default=DEFAULT_RESTORE_LIGHT_STATE + ): bool, + }, + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -82,6 +92,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if not isy_config: return True + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.5.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + # Only import if we haven't before. config_entry = _async_find_matching_config_entry(hass) if not config_entry: @@ -119,18 +139,7 @@ async def async_setup_entry( # they are missing from the options _async_import_options_from_data_if_missing(hass, entry) - hass.data[DOMAIN][entry.entry_id] = {} - hass_isy_data = hass.data[DOMAIN][entry.entry_id] - - hass_isy_data[ISY994_NODES] = {SENSOR_AUX: []} - for platform in PLATFORMS: - hass_isy_data[ISY994_NODES][platform] = [] - - hass_isy_data[ISY994_PROGRAMS] = {} - for platform in PROGRAM_PLATFORMS: - hass_isy_data[ISY994_PROGRAMS][platform] = [] - - hass_isy_data[ISY994_VARIABLES] = [] + isy_data = hass.data[DOMAIN][entry.entry_id] = IsyData() isy_config = entry.data isy_options = entry.options @@ -148,18 +157,18 @@ async def async_setup_entry( CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING ) - if host.scheme == "http": + if host.scheme == SCHEME_HTTP: https = False port = host.port or 80 session = aiohttp_client.async_create_clientsession( hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True) ) - elif host.scheme == "https": + elif host.scheme == SCHEME_HTTPS: https = True port = host.port or 443 session = aiohttp_client.async_get_clientsession(hass) else: - _LOGGER.error("The isy994 host value in configuration is invalid") + _LOGGER.error("The ISY/IoX host value in configuration is invalid") return False # Connect to ISY controller. @@ -199,19 +208,37 @@ async def async_setup_entry( f"Invalid response ISY, device is likely still starting: {err}" ) from err - _categorize_nodes(hass_isy_data, isy.nodes, ignore_identifier, sensor_identifier) - _categorize_programs(hass_isy_data, isy.programs) - _categorize_variables(hass_isy_data, isy.variables, variable_identifier) + _categorize_nodes(isy_data, isy.nodes, ignore_identifier, sensor_identifier) + _categorize_programs(isy_data, isy.programs) + # Categorize variables call to be removed with variable sensors in 2023.5.0 + _categorize_variables(isy_data, isy.variables, variable_identifier) + # Gather ISY Variables to be added. Identifier used to enable by default. + if isy.variables.children: + isy_data.devices[CONF_VARIABLES] = _create_service_device_info( + isy, name=CONF_VARIABLES.title(), unique_id=CONF_VARIABLES + ) + numbers = isy_data.variables[Platform.NUMBER] + for vtype, _, vid in isy.variables.children: + numbers.append(isy.variables[vtype][vid]) + if isy.conf[ISY_CONF_NETWORKING]: + isy_data.devices[CONF_NETWORK] = _create_service_device_info( + isy, name=ISY_CONF_NETWORKING, unique_id=CONF_NETWORK + ) + for resource in isy.networking.nobjs: + isy_data.net_resources.append(resource) # Dump ISY Clock Information. Future: Add ISY as sensor to Hass with attrs _LOGGER.info(repr(isy.clock)) - hass_isy_data[ISY994_ISY] = isy + isy_data.root = isy _async_get_or_create_isy_device_in_registry(hass, entry, isy) # Load platforms for the devices in the ISY controller that we support. await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + # Clean-up any old entities that we no longer provide. + _async_cleanup_registry_entries(hass, entry.entry_id) + @callback def _async_stop_auto_update(event: Event) -> None: """Stop the isy auto update on Home Assistant Shutdown.""" @@ -258,29 +285,39 @@ def _async_import_options_from_data_if_missing( hass.config_entries.async_update_entry(entry, options=options) -@callback -def _async_isy_to_configuration_url(isy: ISY) -> str: - """Extract the configuration url from the isy.""" - connection_info = isy.conn.connection_info - proto = "https" if "tls" in connection_info else "http" - return f"{proto}://{connection_info['addr']}:{connection_info['port']}" - - @callback def _async_get_or_create_isy_device_in_registry( hass: HomeAssistant, entry: config_entries.ConfigEntry, isy: ISY ) -> None: device_registry = dr.async_get(hass) - url = _async_isy_to_configuration_url(isy) device_registry.async_get_or_create( config_entry_id=entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, isy.configuration["uuid"])}, - identifiers={(DOMAIN, isy.configuration["uuid"])}, + connections={(dr.CONNECTION_NETWORK_MAC, isy.uuid)}, + identifiers={(DOMAIN, isy.uuid)}, manufacturer=MANUFACTURER, - name=isy.configuration["name"], - model=isy.configuration["model"], - sw_version=isy.configuration["firmware"], - configuration_url=url, + name=isy.conf[ISY_CONF_NAME], + model=isy.conf[ISY_CONF_MODEL], + sw_version=isy.conf[ISY_CONF_FIRMWARE], + configuration_url=isy.conn.url, + ) + + +def _create_service_device_info(isy: ISY, name: str, unique_id: str) -> DeviceInfo: + """Create device info for ISY service devices.""" + return DeviceInfo( + identifiers={ + ( + DOMAIN, + f"{isy.uuid}_{unique_id}", + ) + }, + manufacturer=MANUFACTURER, + name=f"{isy.conf[ISY_CONF_NAME]} {name}", + model=isy.conf[ISY_CONF_MODEL], + sw_version=isy.conf[ISY_CONF_FIRMWARE], + configuration_url=isy.conn.url, + via_device=(DOMAIN, isy.uuid), + entry_type=DeviceEntryType.SERVICE, ) @@ -290,9 +327,9 @@ async def async_unload_entry( """Unload a config entry.""" unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - hass_isy_data = hass.data[DOMAIN][entry.entry_id] + isy_data = hass.data[DOMAIN][entry.entry_id] - isy: ISY = hass_isy_data[ISY994_ISY] + isy: ISY = isy_data.root _LOGGER.debug("ISY Stopping Event Stream and automatic updates") isy.websocket.stop() @@ -310,8 +347,8 @@ async def async_remove_config_entry_device( config_entry: config_entries.ConfigEntry, device_entry: dr.DeviceEntry, ) -> bool: - """Remove isy994 config entry from a device.""" + """Remove ISY config entry from a device.""" + isy_data = hass.data[DOMAIN][config_entry.entry_id] return not device_entry.identifiers.intersection( - (DOMAIN, unique_id) - for unique_id in unique_ids_for_config_entry_id(hass, config_entry.entry_id) + (DOMAIN, unique_id) for unique_id in isy_data.devices ) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 399d9953170..521bfb41a80 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -1,4 +1,4 @@ -"""Support for ISY994 binary sensors.""" +"""Support for ISY binary sensors.""" from __future__ import annotations from datetime import datetime, timedelta @@ -15,23 +15,23 @@ from pyisy.helpers import NodeProperty from pyisy.nodes import Group, Node from homeassistant.components.binary_sensor import ( - DOMAIN as BINARY_SENSOR, BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_ON, Platform from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.util import dt as dt_util from .const import ( _LOGGER, BINARY_SENSOR_DEVICE_TYPES_ISY, BINARY_SENSOR_DEVICE_TYPES_ZWAVE, - DOMAIN as ISY994_DOMAIN, - ISY994_NODES, - ISY994_PROGRAMS, + DOMAIN, SUBNODE_CLIMATE_COOL, SUBNODE_CLIMATE_HEAT, SUBNODE_DUSK_DAWN, @@ -44,7 +44,6 @@ from .const import ( TYPE_INSTEON_MOTION, ) from .entity import ISYNodeEntity, ISYProgramEntity -from .helpers import migrate_old_unique_ids DEVICE_PARENT_REQUIRED = [ BinarySensorDeviceClass.OPENING, @@ -56,7 +55,7 @@ DEVICE_PARENT_REQUIRED = [ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the ISY994 binary sensor platform.""" + """Set up the ISY binary sensor platform.""" entities: list[ ISYInsteonBinarySensorEntity | ISYBinarySensorEntity @@ -70,27 +69,33 @@ async def async_setup_entry( | ISYBinarySensorHeartbeat | ISYBinarySensorProgramEntity, ] = {} - child_nodes: list[tuple[Node, BinarySensorDeviceClass | None, str | None]] = [] + child_nodes: list[ + tuple[Node, BinarySensorDeviceClass | None, str | None, DeviceInfo | 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]: + isy_data = hass.data[DOMAIN][entry.entry_id] + devices: dict[str, DeviceInfo] = isy_data.devices + for node in isy_data.nodes[Platform.BINARY_SENSOR]: assert isinstance(node, Node) + device_info = devices.get(node.primary_node) device_class, device_type = _detect_device_type_and_class(node) if node.protocol == PROTO_INSTEON: if node.parent_node is not None: # We'll process the Insteon child nodes last, to ensure all parent # nodes have been processed - child_nodes.append((node, device_class, device_type)) + child_nodes.append((node, device_class, device_type, device_info)) continue - entity = ISYInsteonBinarySensorEntity(node, device_class) + entity = ISYInsteonBinarySensorEntity( + node, device_class, device_info=device_info + ) else: - entity = ISYBinarySensorEntity(node, device_class) + entity = ISYBinarySensorEntity(node, device_class, device_info=device_info) 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: + for (node, device_class, device_type, device_info) in child_nodes: subnode_id = int(node.address.split(" ")[-1], 16) # Handle Insteon Thermostats if device_type is not None and device_type.startswith(TYPE_CATEGORY_CLIMATE): @@ -101,13 +106,13 @@ async def async_setup_entry( # As soon as the ISY Event Stream connects if it has a # valid state, it will be set. entity = ISYInsteonBinarySensorEntity( - node, BinarySensorDeviceClass.COLD, False + node, BinarySensorDeviceClass.COLD, False, device_info=device_info ) entities.append(entity) elif subnode_id == SUBNODE_CLIMATE_HEAT: # Subnode 3 is the "Heat Control" sensor entity = ISYInsteonBinarySensorEntity( - node, BinarySensorDeviceClass.HEAT, False + node, BinarySensorDeviceClass.HEAT, False, device_info=device_info ) entities.append(entity) continue @@ -138,7 +143,9 @@ async def async_setup_entry( assert isinstance(parent_entity, ISYInsteonBinarySensorEntity) # Subnode 4 is the heartbeat node, which we will # represent as a separate binary_sensor - entity = ISYBinarySensorHeartbeat(node, parent_entity) + entity = ISYBinarySensorHeartbeat( + node, parent_entity, device_info=device_info + ) parent_entity.add_heartbeat_device(entity) entities.append(entity) continue @@ -157,14 +164,17 @@ async def async_setup_entry( if subnode_id == SUBNODE_DUSK_DAWN: # Subnode 2 is the Dusk/Dawn sensor entity = ISYInsteonBinarySensorEntity( - node, BinarySensorDeviceClass.LIGHT + node, BinarySensorDeviceClass.LIGHT, device_info=device_info ) entities.append(entity) continue if subnode_id == SUBNODE_LOW_BATTERY: # Subnode 3 is the low battery node entity = ISYInsteonBinarySensorEntity( - node, BinarySensorDeviceClass.BATTERY, initial_state + node, + BinarySensorDeviceClass.BATTERY, + initial_state, + device_info=device_info, ) entities.append(entity) continue @@ -172,25 +182,29 @@ async def async_setup_entry( # Tamper Sub-node for MS II. Sometimes reported as "A" sometimes # reported as "10", which translate from Hex to 10 and 16 resp. entity = ISYInsteonBinarySensorEntity( - node, BinarySensorDeviceClass.PROBLEM, initial_state + node, + BinarySensorDeviceClass.PROBLEM, + initial_state, + device_info=device_info, ) entities.append(entity) continue if subnode_id in SUBNODE_MOTION_DISABLED: # Motion Disabled Sub-node for MS II ("D" or "13") - entity = ISYInsteonBinarySensorEntity(node) + entity = ISYInsteonBinarySensorEntity(node, device_info=device_info) entities.append(entity) continue # We don't yet have any special logic for other sensor # types, so add the nodes as individual devices - entity = ISYBinarySensorEntity(node, device_class) + entity = ISYBinarySensorEntity( + node, force_device_class=device_class, device_info=device_info + ) entities.append(entity) - for name, status, _ in hass_isy_data[ISY994_PROGRAMS][BINARY_SENSOR]: + for name, status, _ in isy_data.programs[Platform.BINARY_SENSOR]: entities.append(ISYBinarySensorProgramEntity(name, status)) - await migrate_old_unique_ids(hass, BINARY_SENSOR, entities) async_add_entities(entities) @@ -219,21 +233,22 @@ def _detect_device_type_and_class( class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity): - """Representation of a basic ISY994 binary sensor device.""" + """Representation of a basic ISY binary sensor device.""" def __init__( self, node: Node, force_device_class: BinarySensorDeviceClass | None = None, unknown_state: bool | None = None, + device_info: DeviceInfo | None = None, ) -> None: - """Initialize the ISY994 binary sensor device.""" - super().__init__(node) + """Initialize the ISY binary sensor device.""" + super().__init__(node, device_info=device_info) self._device_class = force_device_class @property def is_on(self) -> bool | None: - """Get whether the ISY994 binary sensor device is on.""" + """Get whether the ISY binary sensor device is on.""" if self._node.status == ISY_VALUE_UNKNOWN: return None return bool(self._node.status) @@ -248,7 +263,7 @@ class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity): class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): - """Representation of an ISY994 Insteon binary sensor device. + """Representation of an ISY Insteon binary sensor device. Often times, a single device is represented by multiple nodes in the ISY, allowing for different nuances in how those devices report their on and @@ -261,9 +276,10 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): node: Node, force_device_class: BinarySensorDeviceClass | None = None, unknown_state: bool | None = None, + device_info: DeviceInfo | None = None, ) -> None: - """Initialize the ISY994 binary sensor device.""" - super().__init__(node, force_device_class) + """Initialize the ISY binary sensor device.""" + super().__init__(node, force_device_class, device_info=device_info) self._negative_node: Node | None = None self._heartbeat_device: ISYBinarySensorHeartbeat | None = None if self._node.status == ISY_VALUE_UNKNOWN: @@ -374,7 +390,7 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): @property def is_on(self) -> bool | None: - """Get whether the ISY994 binary sensor device is on. + """Get whether the ISY binary sensor device is on. Insteon leak sensors set their primary node to On when the state is DRY, not WET, so we invert the binary state if the user indicates @@ -390,8 +406,8 @@ class ISYInsteonBinarySensorEntity(ISYBinarySensorEntity): return self._computed_state -class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): - """Representation of the battery state of an ISY994 sensor.""" +class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity, RestoreEntity): + """Representation of the battery state of an ISY sensor.""" def __init__( self, @@ -400,16 +416,18 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): | ISYBinarySensorEntity | ISYBinarySensorHeartbeat | ISYBinarySensorProgramEntity, + device_info: DeviceInfo | None = None, ) -> None: - """Initialize the ISY994 binary sensor device. + """Initialize the ISY binary sensor device. Computed state is set to UNKNOWN unless the ISY provided a valid state. See notes above regarding ISY Sensor status on ISY restart. If a valid state is provided (either on or off), the computed state in - HA is set to OFF (Normal). If the heartbeat is not received in 25 hours - then the computed state is set to ON (Low Battery). + HA is restored to the previous value or defaulted to OFF (Normal). + If the heartbeat is not received in 25 hours then the computed state is + set to ON (Low Battery). """ - super().__init__(node) + super().__init__(node, device_info=device_info) self._parent_device = parent_device self._heartbeat_timer: CALLBACK_TYPE | None = None self._computed_state: bool | None = None @@ -425,6 +443,11 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): # Start the timer on bootup, so we can change from UNKNOWN to OFF self._restart_timer() + if (last_state := await self.async_get_last_state()) is not None: + # Only restore the state if it was previously ON (Low Battery) + if last_state.state == STATE_ON: + self._computed_state = True + def _heartbeat_node_control_handler(self, event: NodeProperty) -> None: """Update the heartbeat timestamp when any ON/OFF event is sent. @@ -479,7 +502,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): @property def is_on(self) -> bool: - """Get whether the ISY994 binary sensor device is on. + """Get whether the ISY binary sensor device is on. Note: This method will return false if the current state is UNKNOWN which occurs after a restart until the first heartbeat or control @@ -501,7 +524,7 @@ class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): class ISYBinarySensorProgramEntity(ISYProgramEntity, BinarySensorEntity): - """Representation of an ISY994 binary sensor program. + """Representation of an ISY binary sensor program. This does not need all of the subnode logic in the device version of binary sensors. @@ -509,5 +532,5 @@ class ISYBinarySensorProgramEntity(ISYProgramEntity, BinarySensorEntity): @property def is_on(self) -> bool: - """Get whether the ISY994 binary sensor device is on.""" + """Get whether the ISY binary sensor device is on.""" return bool(self._node.status) diff --git a/homeassistant/components/isy994/button.py b/homeassistant/components/isy994/button.py new file mode 100644 index 00000000000..e4d7fb807eb --- /dev/null +++ b/homeassistant/components/isy994/button.py @@ -0,0 +1,161 @@ +"""Representation of ISY/IoX buttons.""" +from __future__ import annotations + +from pyisy import ISY +from pyisy.constants import ( + ATTR_ACTION, + NC_NODE_ENABLED, + PROTO_INSTEON, + TAG_ADDRESS, + TAG_ENABLED, +) +from pyisy.helpers import EventListener, NodeProperty +from pyisy.networking import NetworkCommand +from pyisy.nodes import Node + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import CONF_NETWORK, DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ISY/IoX button from config entry.""" + isy_data = hass.data[DOMAIN][config_entry.entry_id] + isy: ISY = isy_data.root + device_info = isy_data.devices + entities: list[ + ISYNodeQueryButtonEntity + | ISYNodeBeepButtonEntity + | ISYNetworkResourceButtonEntity + ] = [] + + for node in isy_data.root_nodes[Platform.BUTTON]: + entities.append( + ISYNodeQueryButtonEntity( + node=node, + name="Query", + unique_id=f"{isy_data.uid_base(node)}_query", + entity_category=EntityCategory.DIAGNOSTIC, + device_info=device_info[node.address], + ) + ) + if node.protocol == PROTO_INSTEON: + entities.append( + ISYNodeBeepButtonEntity( + node=node, + name="Beep", + unique_id=f"{isy_data.uid_base(node)}_beep", + entity_category=EntityCategory.DIAGNOSTIC, + device_info=device_info[node.address], + ) + ) + + for node in isy_data.net_resources: + entities.append( + ISYNetworkResourceButtonEntity( + node=node, + name=node.name, + unique_id=isy_data.uid_base(node), + device_info=device_info[CONF_NETWORK], + ) + ) + + # Add entity to query full system + entities.append( + ISYNodeQueryButtonEntity( + node=isy, + name="Query", + unique_id=f"{isy.uuid}_query", + device_info=DeviceInfo(identifiers={(DOMAIN, isy.uuid)}), + entity_category=EntityCategory.DIAGNOSTIC, + ) + ) + + async_add_entities(entities) + + +class ISYNodeButtonEntity(ButtonEntity): + """Representation of an ISY/IoX device button entity.""" + + _attr_should_poll = False + _attr_has_entity_name = True + + def __init__( + self, + node: Node | ISY | NetworkCommand, + name: str, + unique_id: str, + device_info: DeviceInfo, + entity_category: EntityCategory | None = None, + ) -> None: + """Initialize a query ISY device button entity.""" + self._node = node + + # Entity class attributes + self._attr_name = name + self._attr_entity_category = entity_category + self._attr_unique_id = unique_id + self._attr_device_info = device_info + self._node_enabled = getattr(node, TAG_ENABLED, True) + self._availability_handler: EventListener | None = None + + @property + def available(self) -> bool: + """Return entity availability.""" + return self._node_enabled + + async def async_added_to_hass(self) -> None: + """Subscribe to the node change events.""" + # No status for NetworkResources or ISY Query buttons + if not hasattr(self._node, "status_events") or not hasattr(self._node, "isy"): + return + self._availability_handler = self._node.isy.nodes.status_events.subscribe( + self.async_on_update, + event_filter={ + TAG_ADDRESS: self._node.address, + ATTR_ACTION: NC_NODE_ENABLED, + }, + key=self.unique_id, + ) + + @callback + def async_on_update(self, event: NodeProperty, key: str) -> None: + """Handle the update event from the ISY Node.""" + # Watch for node availability/enabled changes only + self._node_enabled = getattr(self._node, TAG_ENABLED, True) + self.async_write_ha_state() + + +class ISYNodeQueryButtonEntity(ISYNodeButtonEntity): + """Representation of a device query button entity.""" + + async def async_press(self) -> None: + """Press the button.""" + await self._node.query() + + +class ISYNodeBeepButtonEntity(ISYNodeButtonEntity): + """Representation of a device beep button entity.""" + + async def async_press(self) -> None: + """Press the button.""" + await self._node.beep() + + +class ISYNetworkResourceButtonEntity(ISYNodeButtonEntity): + """Representation of an ISY/IoX Network Resource button entity.""" + + _attr_has_entity_name = False + + async def async_press(self) -> None: + """Press the button.""" + await self._node.run() diff --git a/homeassistant/components/isy994/climate.py b/homeassistant/components/isy994/climate.py index 5267dbff48d..83fea57a9fa 100644 --- a/homeassistant/components/isy994/climate.py +++ b/homeassistant/components/isy994/climate.py @@ -1,4 +1,4 @@ -"""Support for Insteon Thermostats via ISY994 Platform.""" +"""Support for Insteon Thermostats via ISY Platform.""" from __future__ import annotations from typing import Any @@ -19,7 +19,6 @@ from pyisy.nodes import Node from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - DOMAIN as CLIMATE, FAN_AUTO, FAN_OFF, FAN_ON, @@ -29,16 +28,21 @@ from homeassistant.components.climate import ( HVACMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, UnitOfTemperature +from homeassistant.const import ( + ATTR_TEMPERATURE, + PRECISION_TENTHS, + Platform, + UnitOfTemperature, +) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( _LOGGER, - DOMAIN as ISY994_DOMAIN, + DOMAIN, HA_FAN_TO_ISY, HA_HVAC_TO_ISY, - ISY994_NODES, ISY_HVAC_MODES, UOM_FAN_MODES, UOM_HVAC_ACTIONS, @@ -50,25 +54,25 @@ from .const import ( UOM_TO_STATES, ) from .entity import ISYNodeEntity -from .helpers import convert_isy_value_to_hass, migrate_old_unique_ids +from .helpers import convert_isy_value_to_hass async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the ISY994 thermostat platform.""" + """Set up the ISY thermostat platform.""" entities = [] - hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] - for node in hass_isy_data[ISY994_NODES][CLIMATE]: - entities.append(ISYThermostatEntity(node)) + isy_data = hass.data[DOMAIN][entry.entry_id] + devices: dict[str, DeviceInfo] = isy_data.devices + for node in isy_data.nodes[Platform.CLIMATE]: + entities.append(ISYThermostatEntity(node, devices.get(node.primary_node))) - await migrate_old_unique_ids(hass, CLIMATE, entities) async_add_entities(entities) class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): - """Representation of an ISY994 thermostat entity.""" + """Representation of an ISY thermostat entity.""" _attr_hvac_modes = ISY_HVAC_MODES _attr_precision = PRECISION_TENTHS @@ -78,9 +82,9 @@ class ISYThermostatEntity(ISYNodeEntity, ClimateEntity): | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE ) - def __init__(self, node: Node) -> None: + def __init__(self, node: Node, device_info: DeviceInfo | None = None) -> None: """Initialize the ISY Thermostat entity.""" - super().__init__(node) + super().__init__(node, device_info=device_info) self._uom = self._node.uom if isinstance(self._uom, list): self._uom = self._node.uom[0] diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index 0dab84878b0..0b61b14d9b1 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for Universal Devices ISY994 integration.""" +"""Config flow for Universal Devices ISY/IoX integration.""" from __future__ import annotations from collections.abc import Mapping @@ -34,6 +34,8 @@ from .const import ( DOMAIN, HTTP_PORT, HTTPS_PORT, + ISY_CONF_NAME, + ISY_CONF_UUID, ISY_URL_POSTFIX, SCHEME_HTTP, SCHEME_HTTPS, @@ -79,7 +81,7 @@ async def validate_input( port = host.port or HTTPS_PORT session = aiohttp_client.async_get_clientsession(hass) else: - _LOGGER.error("The isy994 host value in configuration is invalid") + _LOGGER.error("The ISY/IoX host value in configuration is invalid") raise InvalidHost # Connect to ISY controller. @@ -106,20 +108,23 @@ async def validate_input( isy_conf = Configuration(xml=isy_conf_xml) except ISYResponseParseError as error: raise CannotConnect from error - if not isy_conf or "name" not in isy_conf or not isy_conf["name"]: + if not isy_conf or ISY_CONF_NAME not in isy_conf or not isy_conf[ISY_CONF_NAME]: raise CannotConnect # Return info that you want to store in the config entry. - return {"title": f"{isy_conf['name']} ({host.hostname})", "uuid": isy_conf["uuid"]} + return { + "title": f"{isy_conf[ISY_CONF_NAME]} ({host.hostname})", + ISY_CONF_UUID: isy_conf[ISY_CONF_UUID], + } class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for Universal Devices ISY994.""" + """Handle a config flow for Universal Devices ISY/IoX.""" VERSION = 1 def __init__(self) -> None: - """Initialize the isy994 config flow.""" + """Initialize the ISY/IoX config flow.""" self.discovered_conf: dict[str, str] = {} self._existing_entry: config_entries.ConfigEntry | None = None @@ -151,7 +156,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" if not errors: - await self.async_set_unique_id(info["uuid"], raise_on_progress=False) + await self.async_set_unique_id( + info[ISY_CONF_UUID], raise_on_progress=False + ) self._abort_if_unique_id_configured() return self.async_create_entry(title=info["title"], data=user_input) @@ -200,9 +207,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): raise AbortFlow("already_configured") async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: - """Handle a discovered isy994 via dhcp.""" + """Handle a discovered ISY/IoX device via dhcp.""" friendly_name = discovery_info.hostname - if friendly_name.startswith("polisy"): + if friendly_name.startswith("polisy") or friendly_name.startswith("eisy"): url = f"http://{discovery_info.ip}:8080" else: url = f"http://{discovery_info.ip}" @@ -221,16 +228,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: - """Handle a discovered isy994.""" + """Handle a discovered ISY/IoX Device.""" 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): - mac = mac[len(UDN_UUID_PREFIX) :] - if url.endswith(ISY_URL_POSTFIX): - url = url[: -len(ISY_URL_POSTFIX)] + mac = mac.removeprefix(UDN_UUID_PREFIX) + url = url.removesuffix(ISY_URL_POSTFIX) port = HTTP_PORT if parsed_url.port: @@ -300,7 +305,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class OptionsFlowHandler(config_entries.OptionsFlow): - """Handle a option flow for isy994.""" + """Handle a option flow for ISY/IoX.""" def __init__(self, config_entry: config_entries.ConfigEntry) -> None: """Initialize options flow.""" diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 8f6c9b0f888..211939f8eb6 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -1,6 +1,8 @@ -"""Constants for the ISY994 Platform.""" +"""Constants for the ISY Platform.""" import logging +from pyisy.constants import PROP_ON_LEVEL, PROP_RAMP_RATE + from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.climate import ( FAN_AUTO, @@ -58,6 +60,7 @@ DOMAIN = "isy994" MANUFACTURER = "Universal Devices, Inc" +CONF_NETWORK = "network" CONF_IGNORE_STRING = "ignore_string" CONF_SENSOR_STRING = "sensor_string" CONF_VAR_SENSOR_STRING = "variable_sensor_string" @@ -74,7 +77,7 @@ DEFAULT_VAR_SENSOR_STRING = "HA." KEY_ACTIONS = "actions" KEY_STATUS = "status" -PLATFORMS = [ +NODE_PLATFORMS = [ Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.COVER, @@ -84,6 +87,12 @@ PLATFORMS = [ Platform.SENSOR, Platform.SWITCH, ] +NODE_AUX_PROP_PLATFORMS = [ + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, +] PROGRAM_PLATFORMS = [ Platform.BINARY_SENSOR, Platform.COVER, @@ -91,6 +100,17 @@ PROGRAM_PLATFORMS = [ Platform.LOCK, Platform.SWITCH, ] +ROOT_NODE_PLATFORMS = [Platform.BUTTON] +VARIABLE_PLATFORMS = [Platform.NUMBER, Platform.SENSOR] + +# Set of all platforms used by integration +PLATFORMS = { + *NODE_PLATFORMS, + *NODE_AUX_PROP_PLATFORMS, + *PROGRAM_PLATFORMS, + *ROOT_NODE_PLATFORMS, + *VARIABLE_PLATFORMS, +} SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"] @@ -98,10 +118,15 @@ SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"] # (they can turn off, and report their state) ISY_GROUP_PLATFORM = Platform.SWITCH -ISY994_ISY = "isy" -ISY994_NODES = "isy994_nodes" -ISY994_PROGRAMS = "isy994_programs" -ISY994_VARIABLES = "isy994_variables" +ISY_CONF_NETWORKING = "Networking Module" +ISY_CONF_UUID = "uuid" +ISY_CONF_NAME = "name" +ISY_CONF_MODEL = "model" +ISY_CONF_FIRMWARE = "firmware" + +ISY_CONN_PORT = "port" +ISY_CONN_ADDRESS = "addr" +ISY_CONN_TLS = "tls" FILTER_UOM = "uom" FILTER_STATES = "states" @@ -162,8 +187,6 @@ UOM_INDEX = "25" UOM_ON_OFF = "2" UOM_PERCENTAGE = "51" -SENSOR_AUX = "sensor_aux" - # Do not use the Home Assistant consts for the states here - we're matching exact API # responses, not using them for Home Assistant states # Insteon Types: https://www.universal-devices.com/developers/wsdk/5.0.4/1_fam.xml @@ -289,11 +312,15 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = { FILTER_ZWAVE_CAT: ["140"], }, } +NODE_AUX_FILTERS: dict[str, Platform] = { + PROP_ON_LEVEL: Platform.NUMBER, + PROP_RAMP_RATE: Platform.SELECT, +} UOM_FRIENDLY_NAME = { - "1": "A", + "1": UnitOfElectricCurrent.AMPERE, UOM_ON_OFF: "", # Binary, no unit - "3": f"btu/{UnitOfTime.HOURS}", + "3": UnitOfPower.BTU_PER_HOUR, "4": UnitOfTemperature.CELSIUS, "5": UnitOfLength.CENTIMETERS, "6": UnitOfVolume.CUBIC_FEET, @@ -319,7 +346,7 @@ UOM_FRIENDLY_NAME = { "28": UnitOfMass.KILOGRAMS, "29": "kV", "30": UnitOfPower.KILO_WATT, - "31": "kPa", + "31": UnitOfPressure.KPA, "32": UnitOfSpeed.KILOMETERS_PER_HOUR, "33": UnitOfEnergy.KILO_WATT_HOUR, "34": "liedu", diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index 60027a31c89..97f3c669772 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -1,4 +1,4 @@ -"""Support for ISY994 covers.""" +"""Support for ISY covers.""" from __future__ import annotations from typing import Any, cast @@ -7,44 +7,37 @@ from pyisy.constants import ISY_VALUE_UNKNOWN from homeassistant.components.cover import ( ATTR_POSITION, - DOMAIN as COVER, CoverEntity, CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ( - _LOGGER, - DOMAIN as ISY994_DOMAIN, - ISY994_NODES, - ISY994_PROGRAMS, - UOM_8_BIT_RANGE, - UOM_BARRIER, -) +from .const import _LOGGER, DOMAIN, UOM_8_BIT_RANGE, UOM_BARRIER from .entity import ISYNodeEntity, ISYProgramEntity -from .helpers import migrate_old_unique_ids async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the ISY994 cover platform.""" - hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] + """Set up the ISY cover platform.""" + isy_data = hass.data[DOMAIN][entry.entry_id] entities: list[ISYCoverEntity | ISYCoverProgramEntity] = [] - for node in hass_isy_data[ISY994_NODES][COVER]: - entities.append(ISYCoverEntity(node)) + devices: dict[str, DeviceInfo] = isy_data.devices + for node in isy_data.nodes[Platform.COVER]: + entities.append(ISYCoverEntity(node, devices.get(node.primary_node))) - for name, status, actions in hass_isy_data[ISY994_PROGRAMS][COVER]: + for name, status, actions in isy_data.programs[Platform.COVER]: entities.append(ISYCoverProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, COVER, entities) async_add_entities(entities) class ISYCoverEntity(ISYNodeEntity, CoverEntity): - """Representation of an ISY994 cover device.""" + """Representation of an ISY cover device.""" _attr_supported_features = ( CoverEntityFeature.OPEN @@ -63,19 +56,19 @@ class ISYCoverEntity(ISYNodeEntity, CoverEntity): @property def is_closed(self) -> bool | None: - """Get whether the ISY994 cover device is closed.""" + """Get whether the ISY cover device is closed.""" if self._node.status == ISY_VALUE_UNKNOWN: return None return bool(self._node.status == 0) async def async_open_cover(self, **kwargs: Any) -> None: - """Send the open cover command to the ISY994 cover device.""" + """Send the open cover command to the ISY 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: Any) -> None: - """Send the close cover command to the ISY994 cover device.""" + """Send the close cover command to the ISY cover device.""" if not await self._node.turn_off(): _LOGGER.error("Unable to close the cover") @@ -89,19 +82,19 @@ class ISYCoverEntity(ISYNodeEntity, CoverEntity): class ISYCoverProgramEntity(ISYProgramEntity, CoverEntity): - """Representation of an ISY994 cover program.""" + """Representation of an ISY cover program.""" @property def is_closed(self) -> bool: - """Get whether the ISY994 cover program is closed.""" + """Get whether the ISY cover program is closed.""" return bool(self._node.status) async def async_open_cover(self, **kwargs: Any) -> None: - """Send the open cover command to the ISY994 cover program.""" + """Send the open cover command to the ISY cover program.""" if not await self._actions.run_then(): _LOGGER.error("Unable to open the cover") async def async_close_cover(self, **kwargs: Any) -> None: - """Send the close cover command to the ISY994 cover program.""" + """Send the close cover command to the ISY 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 a90701f3323..425f1fe5b87 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -4,43 +4,49 @@ from __future__ import annotations from typing import Any, cast from pyisy.constants import ( + ATTR_ACTION, + ATTR_CONTROL, COMMAND_FRIENDLY_NAME, EMPTY_TIME, EVENT_PROPS_IGNORED, - PROTO_GROUP, + NC_NODE_ENABLED, PROTO_INSTEON, PROTO_ZWAVE, + TAG_ADDRESS, + TAG_ENABLED, ) from pyisy.helpers import EventListener, NodeProperty -from pyisy.nodes import Node +from pyisy.nodes import Group, Node, NodeChangedEvent from pyisy.programs import Program +from pyisy.variables import Variable -from homeassistant.const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NAME, - ATTR_SUGGESTED_AREA, - STATE_OFF, - STATE_ON, -) +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription -from . import _async_isy_to_configuration_url from .const import DOMAIN class ISYEntity(Entity): - """Representation of an ISY994 device.""" + """Representation of an ISY device.""" - _name: str | None = None + _attr_has_entity_name = False _attr_should_poll = False + _node: Node | Program | Variable - def __init__(self, node: Node) -> None: - """Initialize the insteon device.""" + def __init__( + self, + node: Node | Group | Variable | Program, + device_info: DeviceInfo | None = None, + ) -> None: + """Initialize the ISY/IoX entity.""" self._node = node + self._attr_name = node.name + if device_info is None: + device_info = DeviceInfo(identifiers={(DOMAIN, node.isy.uuid)}) + self._attr_device_info = device_info + self._attr_unique_id = f"{node.isy.uuid}_{node.address}" self._attrs: dict[str, Any] = {} self._change_handler: EventListener | None = None self._control_handler: EventListener | None = None @@ -56,12 +62,12 @@ class ISYEntity(Entity): @callback def async_on_update(self, event: NodeProperty) -> None: - """Handle the update event from the ISY994 Node.""" + """Handle the update event from the ISY Node.""" self.async_write_ha_state() @callback def async_on_control(self, event: NodeProperty) -> None: - """Handle a control event from the ISY994 Node.""" + """Handle a control event from the ISY Node.""" event_data = { "entity_id": self.entity_id, "control": event.control, @@ -77,86 +83,26 @@ class ISYEntity(Entity): self.hass.bus.async_fire("isy994_control", event_data) - @property - 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 - return None - isy = self._node.isy - uuid = isy.configuration["uuid"] - node = self._node - url = _async_isy_to_configuration_url(isy) - - basename = self._name or str(self._node.name) - - if hasattr(self._node, "parent_node") and self._node.parent_node is not None: - # This is not the parent node, get the parent node. - node = self._node.parent_node - basename = node.name - - device_info = DeviceInfo( - manufacturer="Unknown", - model="Unknown", - name=basename, - via_device=(DOMAIN, uuid), - configuration_url=url, - ) - - if hasattr(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: - model: str = str(node.node_def_id) - # Numerical Device Type - if hasattr(node, "type") and node.type is not None: - model += f" {node.type}" - device_info[ATTR_MODEL] = model - if hasattr(node, "protocol"): - model = str(device_info[ATTR_MODEL]) - manufacturer = str(node.protocol) - if node.protocol == PROTO_ZWAVE: - # Get extra information for Z-Wave Devices - 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. - - return device_info - - @property - 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 | None: - """Get the old unique identifier of the device.""" - if hasattr(self._node, "address"): - return cast(str, self._node.address) - return None - - @property - def name(self) -> str: - """Get the name of the device.""" - return self._name or str(self._node.name) - class ISYNodeEntity(ISYEntity): """Representation of a ISY Nodebase (Node/Group) entity.""" + def __init__( + self, + node: Node | Group | Variable | Program, + device_info: DeviceInfo | None = None, + ) -> None: + """Initialize the ISY/IoX node entity.""" + super().__init__(node, device_info=device_info) + if hasattr(node, "parent_node") and node.parent_node is None: + self._attr_has_entity_name = True + self._attr_name = None + + @property + def available(self) -> bool: + """Return entity availability.""" + return getattr(self._node, TAG_ENABLED, True) + @property def extra_state_attributes(self) -> dict: """Get the state attributes for the device. @@ -168,10 +114,7 @@ class ISYNodeEntity(ISYEntity): attr = {} node = self._node # Insteon aux_properties are now their own sensors - if ( - hasattr(self._node, "aux_properties") - and getattr(node, "protocol", None) != PROTO_INSTEON - ): + if hasattr(self._node, "aux_properties") and node.protocol != PROTO_INSTEON: for name, value in self._node.aux_properties.items(): attr_name = COMMAND_FRIENDLY_NAME.get(name, name) attr[attr_name] = str(value.formatted).lower() @@ -207,7 +150,7 @@ class ISYNodeEntity(ISYEntity): 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: + if self._node.protocol != PROTO_ZWAVE: raise HomeAssistantError( "Invalid service call: cannot request Z-Wave Parameter for non-Z-Wave" f" device {self.entity_id}" @@ -218,7 +161,7 @@ class ISYNodeEntity(ISYEntity): 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: + if self._node.protocol != PROTO_ZWAVE: raise HomeAssistantError( "Invalid service call: cannot set Z-Wave Parameter for non-Z-Wave" f" device {self.entity_id}" @@ -232,12 +175,15 @@ class ISYNodeEntity(ISYEntity): class ISYProgramEntity(ISYEntity): - """Representation of an ISY994 program base.""" + """Representation of an ISY program base.""" - def __init__(self, name: str, status: Any | None, actions: Program = None) -> None: - """Initialize the ISY994 program-based entity.""" + _actions: Program + _status: Program + + def __init__(self, name: str, status: Program, actions: Program = None) -> None: + """Initialize the ISY program-based entity.""" super().__init__(status) - self._name = name + self._attr_name = name self._actions = actions @property @@ -264,3 +210,57 @@ class ISYProgramEntity(ISYEntity): if self._node.last_update != EMPTY_TIME: attr["status_last_update"] = self._node.last_update return attr + + +class ISYAuxControlEntity(Entity): + """Representation of a ISY/IoX Aux Control base entity.""" + + _attr_should_poll = False + + def __init__( + self, + node: Node, + control: str, + unique_id: str, + description: EntityDescription, + device_info: DeviceInfo | None, + ) -> None: + """Initialize the ISY Aux Control Number entity.""" + self._node = node + self._control = control + name = COMMAND_FRIENDLY_NAME.get(control, control).replace("_", " ").title() + if node.address != node.primary_node: + name = f"{node.name} {name}" + self._attr_name = name + self.entity_description = description + self._attr_has_entity_name = node.address == node.primary_node + self._attr_unique_id = unique_id + self._attr_device_info = device_info + self._change_handler: EventListener = None + self._availability_handler: EventListener = None + + async def async_added_to_hass(self) -> None: + """Subscribe to the node control change events.""" + self._change_handler = self._node.control_events.subscribe( + self.async_on_update, + event_filter={ATTR_CONTROL: self._control}, + key=self.unique_id, + ) + self._availability_handler = self._node.isy.nodes.status_events.subscribe( + self.async_on_update, + event_filter={ + TAG_ADDRESS: self._node.address, + ATTR_ACTION: NC_NODE_ENABLED, + }, + key=self.unique_id, + ) + + @callback + def async_on_update(self, event: NodeProperty | NodeChangedEvent, key: str) -> None: + """Handle a control event from the ISY Node.""" + self.async_write_ha_state() + + @property + def available(self) -> bool: + """Return entity availability.""" + return cast(bool, self._node.enabled) diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index 9e264076d88..75c033bd9ea 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -1,4 +1,4 @@ -"""Support for ISY994 fans.""" +"""Support for ISY fans.""" from __future__ import annotations import math @@ -6,9 +6,11 @@ from typing import Any from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_INSTEON -from homeassistant.components.fan import DOMAIN as FAN, FanEntity, FanEntityFeature +from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( int_states_in_range, @@ -16,9 +18,8 @@ from homeassistant.util.percentage import ( ranged_value_to_percentage, ) -from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS +from .const import _LOGGER, DOMAIN from .entity import ISYNodeEntity, ISYProgramEntity -from .helpers import migrate_old_unique_ids SPEED_RANGE = (1, 255) # off is not included @@ -26,22 +27,22 @@ SPEED_RANGE = (1, 255) # off is not included async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the ISY994 fan platform.""" - hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] + """Set up the ISY fan platform.""" + isy_data = hass.data[DOMAIN][entry.entry_id] + devices: dict[str, DeviceInfo] = isy_data.devices entities: list[ISYFanEntity | ISYFanProgramEntity] = [] - for node in hass_isy_data[ISY994_NODES][FAN]: - entities.append(ISYFanEntity(node)) + for node in isy_data.nodes[Platform.FAN]: + entities.append(ISYFanEntity(node, devices.get(node.primary_node))) - for name, status, actions in hass_isy_data[ISY994_PROGRAMS][FAN]: + for name, status, actions in isy_data.programs[Platform.FAN]: entities.append(ISYFanProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, FAN, entities) async_add_entities(entities) class ISYFanEntity(ISYNodeEntity, FanEntity): - """Representation of an ISY994 fan device.""" + """Representation of an ISY fan device.""" _attr_supported_features = FanEntityFeature.SET_SPEED @@ -67,7 +68,7 @@ class ISYFanEntity(ISYNodeEntity, FanEntity): 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.""" + """Set node to speed percentage for the ISY fan device.""" if percentage == 0: await self._node.turn_off() return @@ -82,16 +83,16 @@ class ISYFanEntity(ISYNodeEntity, FanEntity): preset_mode: str | None = None, **kwargs: Any, ) -> None: - """Send the turn on command to the ISY994 fan device.""" + """Send the turn on command to the ISY fan device.""" await self.async_set_percentage(percentage or 67) async def async_turn_off(self, **kwargs: Any) -> None: - """Send the turn off command to the ISY994 fan device.""" + """Send the turn off command to the ISY fan device.""" await self._node.turn_off() class ISYFanProgramEntity(ISYProgramEntity, FanEntity): - """Representation of an ISY994 fan program.""" + """Representation of an ISY fan program.""" @property def percentage(self) -> int | None: @@ -111,7 +112,7 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity): return bool(self._node.status != 0) async def async_turn_off(self, **kwargs: Any) -> None: - """Send the turn on command to ISY994 fan program.""" + """Send the turn on command to ISY fan program.""" if not await self._actions.run_then(): _LOGGER.error("Unable to turn off the fan") @@ -121,6 +122,6 @@ class ISYFanProgramEntity(ISYProgramEntity, FanEntity): preset_mode: str | None = None, **kwargs: Any, ) -> None: - """Send the turn off command to ISY994 fan program.""" + """Send the turn off command to ISY fan program.""" if not await self._actions.run_else(): _LOGGER.error("Unable to turn on the fan") diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index c4f0c2ea595..b190638fb35 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -1,30 +1,31 @@ -"""Sorting helpers for ISY994 device classifications.""" +"""Sorting helpers for ISY device classifications.""" from __future__ import annotations -from collections.abc import Sequence -from typing import TYPE_CHECKING, cast +from typing import cast from pyisy.constants import ( + BACKLIGHT_SUPPORT, + CMD_BACKLIGHT, ISY_VALUE_UNKNOWN, + PROP_BUSY, + PROP_COMMS_ERROR, + PROP_ON_LEVEL, + PROP_RAMP_RATE, + PROP_STATUS, PROTO_GROUP, PROTO_INSTEON, PROTO_PROGRAM, PROTO_ZWAVE, + TAG_ENABLED, TAG_FOLDER, + UOM_INDEX, ) from pyisy.nodes import Group, Node, Nodes from pyisy.programs import Programs from pyisy.variables import Variables -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR -from homeassistant.components.climate import DOMAIN as CLIMATE -from homeassistant.components.fan import DOMAIN as FAN -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 import entity_registry as er +from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, Platform +from homeassistant.helpers.entity import DeviceInfo from .const import ( _LOGGER, @@ -35,16 +36,13 @@ from .const import ( FILTER_STATES, FILTER_UOM, FILTER_ZWAVE_CAT, - ISY994_NODES, - ISY994_PROGRAMS, - ISY994_VARIABLES, ISY_GROUP_PLATFORM, KEY_ACTIONS, KEY_STATUS, + NODE_AUX_FILTERS, NODE_FILTERS, - PLATFORMS, + NODE_PLATFORMS, PROGRAM_PLATFORMS, - SENSOR_AUX, SUBNODE_CLIMATE_COOL, SUBNODE_CLIMATE_HEAT, SUBNODE_EZIO2X4_SENSORS, @@ -55,16 +53,19 @@ from .const import ( UOM_DOUBLE_TEMP, UOM_ISYV4_DEGREES, ) - -if TYPE_CHECKING: - from .entity import ISYEntity +from .models import IsyData BINARY_SENSOR_UOMS = ["2", "78"] BINARY_SENSOR_ISY_STATES = ["on", "off"] +ROOT_AUX_CONTROLS = { + PROP_ON_LEVEL, + PROP_RAMP_RATE, +} +SKIP_AUX_PROPS = {PROP_BUSY, PROP_COMMS_ERROR, PROP_STATUS, *ROOT_AUX_CONTROLS} def _check_for_node_def( - hass_isy_data: dict, node: Group | Node, single_platform: Platform | None = None + isy_data: IsyData, node: Group | Node, single_platform: Platform | None = None ) -> bool: """Check if the node matches the node_def_id for any platforms. @@ -77,17 +78,17 @@ def _check_for_node_def( node_def_id = node.node_def_id - platforms = PLATFORMS if not single_platform else [single_platform] + platforms = NODE_PLATFORMS if not single_platform else [single_platform] for platform in platforms: if node_def_id in NODE_FILTERS[platform][FILTER_NODE_DEF_ID]: - hass_isy_data[ISY994_NODES][platform].append(node) + isy_data.nodes[platform].append(node) return True return False def _check_for_insteon_type( - hass_isy_data: dict, node: Group | Node, single_platform: Platform | None = None + isy_data: IsyData, node: Group | Node, single_platform: Platform | None = None ) -> bool: """Check if the node matches the Insteon type for any platforms. @@ -95,14 +96,14 @@ def _check_for_insteon_type( works for Insteon device. "Node Server" (v5+) and Z-Wave and others will not have a type. """ - if not hasattr(node, "protocol") or node.protocol != PROTO_INSTEON: + if node.protocol != PROTO_INSTEON: return False if not hasattr(node, "type") or node.type is None: # Node doesn't have a type (non-Insteon device most likely) return False device_type = node.type - platforms = PLATFORMS if not single_platform else [single_platform] + platforms = NODE_PLATFORMS if not single_platform else [single_platform] for platform in platforms: if any( device_type.startswith(t) @@ -115,51 +116,51 @@ def _check_for_insteon_type( subnode_id = int(node.address.split(" ")[-1], 16) # FanLinc, which has a light module as one of its nodes. - if platform == FAN and subnode_id == SUBNODE_FANLINC_LIGHT: - hass_isy_data[ISY994_NODES][LIGHT].append(node) + if platform == Platform.FAN and subnode_id == SUBNODE_FANLINC_LIGHT: + isy_data.nodes[Platform.LIGHT].append(node) return True # Thermostats, which has a "Heat" and "Cool" sub-node on address 2 and 3 - if platform == CLIMATE and subnode_id in ( + if platform == Platform.CLIMATE and subnode_id in ( SUBNODE_CLIMATE_COOL, SUBNODE_CLIMATE_HEAT, ): - hass_isy_data[ISY994_NODES][BINARY_SENSOR].append(node) + isy_data.nodes[Platform.BINARY_SENSOR].append(node) return True # IOLincs which have a sensor and relay on 2 different nodes if ( - platform == BINARY_SENSOR + platform == Platform.BINARY_SENSOR and device_type.startswith(TYPE_CATEGORY_SENSOR_ACTUATORS) and subnode_id == SUBNODE_IOLINC_RELAY ): - hass_isy_data[ISY994_NODES][SWITCH].append(node) + isy_data.nodes[Platform.SWITCH].append(node) return True # Smartenit EZIO2X4 if ( - platform == SWITCH + platform == Platform.SWITCH and device_type.startswith(TYPE_EZIO2X4) and subnode_id in SUBNODE_EZIO2X4_SENSORS ): - hass_isy_data[ISY994_NODES][BINARY_SENSOR].append(node) + isy_data.nodes[Platform.BINARY_SENSOR].append(node) return True - hass_isy_data[ISY994_NODES][platform].append(node) + isy_data.nodes[platform].append(node) return True return False def _check_for_zwave_cat( - hass_isy_data: dict, node: Group | Node, single_platform: Platform | None = None + isy_data: IsyData, node: Group | Node, single_platform: Platform | None = None ) -> bool: """Check if the node matches the ISY Z-Wave Category for any platforms. This is for (presumably) every version of the ISY firmware, but only works for Z-Wave Devices with the devtype.cat property. """ - if not hasattr(node, "protocol") or node.protocol != PROTO_ZWAVE: + if node.protocol != PROTO_ZWAVE: return False if not hasattr(node, "zwave_props") or node.zwave_props is None: @@ -167,20 +168,20 @@ def _check_for_zwave_cat( return False device_type = node.zwave_props.category - platforms = PLATFORMS if not single_platform else [single_platform] + platforms = NODE_PLATFORMS if not single_platform else [single_platform] for platform in platforms: if any( device_type.startswith(t) for t in set(NODE_FILTERS[platform][FILTER_ZWAVE_CAT]) ): - hass_isy_data[ISY994_NODES][platform].append(node) + isy_data.nodes[platform].append(node) return True return False def _check_for_uom_id( - hass_isy_data: dict, + isy_data: IsyData, node: Group | Node, single_platform: Platform | None = None, uom_list: list[str] | None = None, @@ -199,23 +200,23 @@ def _check_for_uom_id( if isinstance(node.uom, list): node_uom = node.uom[0] - if uom_list: + if uom_list and single_platform: if node_uom in uom_list: - hass_isy_data[ISY994_NODES][single_platform].append(node) + isy_data.nodes[single_platform].append(node) return True return False - platforms = PLATFORMS if not single_platform else [single_platform] + platforms = NODE_PLATFORMS if not single_platform else [single_platform] for platform in platforms: if node_uom in NODE_FILTERS[platform][FILTER_UOM]: - hass_isy_data[ISY994_NODES][platform].append(node) + isy_data.nodes[platform].append(node) return True return False def _check_for_states_in_uom( - hass_isy_data: dict, + isy_data: IsyData, node: Group | Node, single_platform: Platform | None = None, states_list: list[str] | None = None, @@ -236,28 +237,26 @@ def _check_for_states_in_uom( node_uom = set(map(str.lower, node.uom)) - if states_list: + if states_list and single_platform: if node_uom == set(states_list): - hass_isy_data[ISY994_NODES][single_platform].append(node) + isy_data.nodes[single_platform].append(node) return True return False - platforms = PLATFORMS if not single_platform else [single_platform] + platforms = NODE_PLATFORMS if not single_platform else [single_platform] for platform in platforms: if node_uom == set(NODE_FILTERS[platform][FILTER_STATES]): - hass_isy_data[ISY994_NODES][platform].append(node) + isy_data.nodes[platform].append(node) return True return False -def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Group | Node) -> bool: +def _is_sensor_a_binary_sensor(isy_data: IsyData, 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=Platform.BINARY_SENSOR): + if _check_for_node_def(isy_data, node, single_platform=Platform.BINARY_SENSOR): return True - if _check_for_insteon_type( - hass_isy_data, node, single_platform=Platform.BINARY_SENSOR - ): + if _check_for_insteon_type(isy_data, node, single_platform=Platform.BINARY_SENSOR): return True # For the next two checks, we're providing our own set of uoms that @@ -265,14 +264,14 @@ 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, + isy_data, node, single_platform=Platform.BINARY_SENSOR, uom_list=BINARY_SENSOR_UOMS, ): return True if _check_for_states_in_uom( - hass_isy_data, + isy_data, node, single_platform=Platform.BINARY_SENSOR, states_list=BINARY_SENSOR_ISY_STATES, @@ -282,8 +281,57 @@ def _is_sensor_a_binary_sensor(hass_isy_data: dict, node: Group | Node) -> bool: return False +def _add_backlight_if_supported(isy_data: IsyData, node: Node) -> None: + """Check if a node supports setting a backlight and add entity.""" + if not getattr(node, "is_backlight_supported", False): + return + if BACKLIGHT_SUPPORT[node.node_def_id] == UOM_INDEX: + isy_data.aux_properties[Platform.SELECT].append((node, CMD_BACKLIGHT)) + return + isy_data.aux_properties[Platform.NUMBER].append((node, CMD_BACKLIGHT)) + + +def _generate_device_info(node: Node) -> DeviceInfo: + """Generate the device info for a root node device.""" + isy = node.isy + device_info = DeviceInfo( + identifiers={(DOMAIN, f"{isy.uuid}_{node.address}")}, + manufacturer=node.protocol.title(), + name=node.name, + via_device=(DOMAIN, isy.uuid), + configuration_url=isy.conn.url, + suggested_area=node.folder, + ) + + # ISYv5 Device Types can provide model and manufacturer + model: str = str(node.address).rpartition(" ")[0] or node.address + if node.node_def_id is not None: + model += f": {node.node_def_id}" + + # Numerical Device Type + if node.type is not None: + model += f" ({node.type})" + + # Get extra information for Z-Wave Devices + if ( + node.protocol == PROTO_ZWAVE + and node.zwave_props + and node.zwave_props.mfr_id != "0" + ): + device_info[ + ATTR_MANUFACTURER + ] = f"Z-Wave MfrID:{int(node.zwave_props.mfr_id):#0{6}x}" + model += ( + f"Type:{int(node.zwave_props.prod_type_id):#0{6}x} " + f"Product:{int(node.zwave_props.product_id):#0{6}x}" + ) + device_info[ATTR_MODEL] = model + + return device_info + + def _categorize_nodes( - hass_isy_data: dict, nodes: Nodes, ignore_identifier: str, sensor_identifier: str + isy_data: IsyData, nodes: Nodes, ignore_identifier: str, sensor_identifier: str ) -> None: """Sort the nodes to their proper platforms.""" for path, node in nodes: @@ -292,42 +340,62 @@ def _categorize_nodes( # Don't import this node as a device at all continue - if hasattr(node, "protocol") and node.protocol == PROTO_GROUP: - hass_isy_data[ISY994_NODES][ISY_GROUP_PLATFORM].append(node) + if hasattr(node, "parent_node") and node.parent_node is None: + # This is a physical device / parent node + isy_data.devices[node.address] = _generate_device_info(node) + isy_data.root_nodes[Platform.BUTTON].append(node) + # Any parent node can have communication errors: + isy_data.aux_properties[Platform.SENSOR].append((node, PROP_COMMS_ERROR)) + # Add Ramp Rate and On Levels for Dimmable Load devices + if getattr(node, "is_dimmable", False): + aux_controls = ROOT_AUX_CONTROLS.intersection(node.aux_properties) + for control in aux_controls: + # Deprecated all aux properties as sensors. Update in 2023.5.0 to remove extras. + isy_data.aux_properties[Platform.SENSOR].append((node, control)) + platform = NODE_AUX_FILTERS[control] + isy_data.aux_properties[platform].append((node, control)) + if hasattr(node, TAG_ENABLED): + isy_data.aux_properties[Platform.SWITCH].append((node, TAG_ENABLED)) + _add_backlight_if_supported(isy_data, node) + + if node.protocol == PROTO_GROUP: + isy_data.nodes[ISY_GROUP_PLATFORM].append(node) continue - if getattr(node, "protocol", None) == PROTO_INSTEON: + if node.protocol == PROTO_INSTEON: for control in node.aux_properties: - hass_isy_data[ISY994_NODES][SENSOR_AUX].append((node, control)) + if control in SKIP_AUX_PROPS: + continue + isy_data.aux_properties[Platform.SENSOR].append((node, control)) if sensor_identifier in path or sensor_identifier in node.name: # User has specified to treat this as a sensor. First we need to # determine if it should be a binary_sensor. - if _is_sensor_a_binary_sensor(hass_isy_data, node): + if _is_sensor_a_binary_sensor(isy_data, node): continue - hass_isy_data[ISY994_NODES][SENSOR].append(node) + isy_data.nodes[Platform.SENSOR].append(node) continue # We have a bunch of different methods for determining the device type, # each of which works with different ISY firmware versions or device # family. The order here is important, from most reliable to least. - if _check_for_node_def(hass_isy_data, node): + if _check_for_node_def(isy_data, node): continue - if _check_for_insteon_type(hass_isy_data, node): + if _check_for_insteon_type(isy_data, node): continue - if _check_for_zwave_cat(hass_isy_data, node): + if _check_for_zwave_cat(isy_data, node): continue - if _check_for_uom_id(hass_isy_data, node): + if _check_for_uom_id(isy_data, node): continue - if _check_for_states_in_uom(hass_isy_data, node): + if _check_for_states_in_uom(isy_data, node): continue # Fallback as as sensor, e.g. for un-sortable items like NodeServer nodes. - hass_isy_data[ISY994_NODES][SENSOR].append(node) + isy_data.nodes[Platform.SENSOR].append(node) -def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: - """Categorize the ISY994 programs.""" +def _categorize_programs(isy_data: IsyData, programs: Programs) -> None: + """Categorize the ISY programs.""" for platform in PROGRAM_PLATFORMS: folder = programs.get_by_name(f"{DEFAULT_PROGRAM_STRING}{platform}") if not folder: @@ -348,7 +416,7 @@ def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: ) continue - if platform != BINARY_SENSOR: + if platform != Platform.BINARY_SENSOR: actions = entity_folder.get_by_name(KEY_ACTIONS) if not actions or actions.protocol != PROTO_PROGRAM: _LOGGER.warning( @@ -362,58 +430,21 @@ def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: continue entity = (entity_folder.name, status, actions) - hass_isy_data[ISY994_PROGRAMS][platform].append(entity) + isy_data.programs[platform].append(entity) def _categorize_variables( - hass_isy_data: dict, variables: Variables, identifier: str + isy_data: IsyData, variables: Variables, identifier: str ) -> None: - """Gather the ISY994 Variables to be added as sensors.""" + """Gather the ISY Variables to be added as sensors.""" try: - var_to_add = [ - (vtype, vname, vid) + isy_data.variables[Platform.SENSOR] = [ + variables[vtype][vid] for (vtype, vname, vid) in variables.children if identifier in vname ] except KeyError as err: _LOGGER.error("Error adding ISY Variables: %s", err) - return - for vtype, vname, vid in var_to_add: - hass_isy_data[ISY994_VARIABLES].append((vname, variables[vtype][vid])) - - -async def migrate_old_unique_ids( - hass: HomeAssistant, platform: str, entities: Sequence[ISYEntity] -) -> None: - """Migrate to new controller-specific unique ids.""" - registry = er.async_get(hass) - - 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, entity.old_unique_id - ) - if old_entity_id is not None: - _LOGGER.debug( - "Migrating unique_id from [%s] to [%s]", - entity.old_unique_id, - entity.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, entity.unique_id.replace(":", "") - ) - if old_entity_id_2 is not None: - _LOGGER.debug( - "Migrating unique_id from [%s] to [%s]", - entity.unique_id.replace(":", ""), - entity.unique_id, - ) - registry.async_update_entity( - old_entity_id_2, new_unique_id=entity.unique_id - ) def convert_isy_value_to_hass( diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 6e67ed32938..0b62f2bd144 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -1,4 +1,4 @@ -"""Support for ISY994 lights.""" +"""Support for ISY lights.""" from __future__ import annotations from typing import Any, cast @@ -7,22 +7,22 @@ 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, ColorMode, LightEntity +from homeassistant.components.light import ColorMode, LightEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.helpers.entity_registry as er from homeassistant.helpers.restore_state import RestoreEntity -from .const import ( - _LOGGER, - CONF_RESTORE_LIGHT_STATE, - DOMAIN as ISY994_DOMAIN, - ISY994_NODES, - UOM_PERCENTAGE, -) +from .const import _LOGGER, CONF_RESTORE_LIGHT_STATE, DOMAIN, UOM_PERCENTAGE from .entity import ISYNodeEntity -from .helpers import migrate_old_unique_ids -from .services import async_setup_light_services +from .services import ( + SERVICE_SET_ON_LEVEL, + async_log_deprecated_service_call, + async_setup_light_services, +) ATTR_LAST_BRIGHTNESS = "last_brightness" @@ -30,42 +30,49 @@ ATTR_LAST_BRIGHTNESS = "last_brightness" async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the ISY994 light platform.""" - hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] + """Set up the ISY light platform.""" + isy_data = hass.data[DOMAIN][entry.entry_id] + devices: dict[str, DeviceInfo] = isy_data.devices isy_options = entry.options restore_light_state = isy_options.get(CONF_RESTORE_LIGHT_STATE, False) entities = [] - for node in hass_isy_data[ISY994_NODES][LIGHT]: - entities.append(ISYLightEntity(node, restore_light_state)) + for node in isy_data.nodes[Platform.LIGHT]: + entities.append( + ISYLightEntity(node, restore_light_state, devices.get(node.primary_node)) + ) - 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.""" + """Representation of an ISY light device.""" _attr_color_mode = ColorMode.BRIGHTNESS _attr_supported_color_modes = {ColorMode.BRIGHTNESS} - def __init__(self, node: Node, restore_light_state: bool) -> None: - """Initialize the ISY994 light device.""" - super().__init__(node) + def __init__( + self, + node: Node, + restore_light_state: bool, + device_info: DeviceInfo | None = None, + ) -> None: + """Initialize the ISY light device.""" + super().__init__(node, device_info=device_info) self._last_brightness: int | None = None self._restore_light_state = restore_light_state @property def is_on(self) -> bool: - """Get whether the ISY994 light is on.""" + """Get whether the ISY light is on.""" if self._node.status == ISY_VALUE_UNKNOWN: return False return int(self._node.status) != 0 @property def brightness(self) -> int | None: - """Get the brightness of the ISY994 light.""" + """Get the brightness of the ISY light.""" if self._node.status == ISY_VALUE_UNKNOWN: return None # Special Case for ISY Z-Wave Devices using % instead of 0-255: @@ -74,14 +81,14 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): return int(self._node.status) async def async_turn_off(self, **kwargs: Any) -> None: - """Send the turn off command to the ISY994 light device.""" + """Send the turn off command to the ISY 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: NodeProperty) -> None: - """Save brightness in the update event from the ISY994 Node.""" + """Save brightness in the update event from the ISY Node.""" if self._node.status not in (0, ISY_VALUE_UNKNOWN): self._last_brightness = self._node.status if self._node.uom == UOM_PERCENTAGE: @@ -91,7 +98,7 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): super().async_on_update(event) async def async_turn_on(self, brightness: int | None = None, **kwargs: Any) -> None: - """Send the turn on command to the ISY994 light device.""" + """Send the turn on command to the ISY light device.""" if self._restore_light_state and brightness is None and self._last_brightness: brightness = self._last_brightness # Special Case for ISY Z-Wave Devices using % instead of 0-255: @@ -123,8 +130,32 @@ class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): async def async_set_on_level(self, value: int) -> None: """Set the ON Level for a device.""" + entity_registry = er.async_get(self.hass) + async_log_deprecated_service_call( + self.hass, + call=ServiceCall(domain=DOMAIN, service=SERVICE_SET_ON_LEVEL), + alternate_service="number.set_value", + alternate_target=entity_registry.async_get_entity_id( + Platform.NUMBER, + DOMAIN, + f"{self._node.isy.uuid}_{self._node.address}_OL", + ), + breaks_in_ha_version="2023.5.0", + ) await self._node.set_on_level(value) async def async_set_ramp_rate(self, value: int) -> None: """Set the Ramp Rate for a device.""" + entity_registry = er.async_get(self.hass) + async_log_deprecated_service_call( + self.hass, + call=ServiceCall(domain=DOMAIN, service=SERVICE_SET_ON_LEVEL), + alternate_service="select.select_option", + alternate_target=entity_registry.async_get_entity_id( + Platform.NUMBER, + DOMAIN, + f"{self._node.isy.uuid}_{self._node.address}_RR", + ), + breaks_in_ha_version="2023.5.0", + ) await self._node.set_ramp_rate(value) diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index 4de5cdaa05b..c5372135bbb 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -1,18 +1,19 @@ -"""Support for ISY994 locks.""" +"""Support for ISY locks.""" from __future__ import annotations from typing import Any from pyisy.constants import ISY_VALUE_UNKNOWN -from homeassistant.components.lock import DOMAIN as LOCK, LockEntity +from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS +from .const import _LOGGER, DOMAIN from .entity import ISYNodeEntity, ISYProgramEntity -from .helpers import migrate_old_unique_ids VALUE_TO_STATE = {0: False, 100: True} @@ -20,21 +21,21 @@ VALUE_TO_STATE = {0: False, 100: True} async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the ISY994 lock platform.""" - hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] + """Set up the ISY lock platform.""" + isy_data = hass.data[DOMAIN][entry.entry_id] + devices: dict[str, DeviceInfo] = isy_data.devices entities: list[ISYLockEntity | ISYLockProgramEntity] = [] - for node in hass_isy_data[ISY994_NODES][LOCK]: - entities.append(ISYLockEntity(node)) + for node in isy_data.nodes[Platform.LOCK]: + entities.append(ISYLockEntity(node, devices.get(node.primary_node))) - for name, status, actions in hass_isy_data[ISY994_PROGRAMS][LOCK]: + for name, status, actions in isy_data.programs[Platform.LOCK]: entities.append(ISYLockProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, LOCK, entities) async_add_entities(entities) class ISYLockEntity(ISYNodeEntity, LockEntity): - """Representation of an ISY994 lock device.""" + """Representation of an ISY lock device.""" @property def is_locked(self) -> bool | None: @@ -44,12 +45,12 @@ class ISYLockEntity(ISYNodeEntity, LockEntity): return VALUE_TO_STATE.get(self._node.status) async def async_lock(self, **kwargs: Any) -> None: - """Send the lock command to the ISY994 device.""" + """Send the lock command to the ISY device.""" if not await self._node.secure_lock(): _LOGGER.error("Unable to lock device") async def async_unlock(self, **kwargs: Any) -> None: - """Send the unlock command to the ISY994 device.""" + """Send the unlock command to the ISY device.""" if not await self._node.secure_unlock(): _LOGGER.error("Unable to lock device") diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index cfc7f5d0e22..8fa77cd126c 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -1,9 +1,9 @@ { "domain": "isy994", - "name": "Universal Devices ISY994", + "name": "Universal Devices ISY/IoX", "integration_type": "hub", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": ["pyisy==3.0.10"], + "requirements": ["pyisy==3.1.11"], "codeowners": ["@bdraco", "@shbatm"], "config_flow": true, "ssdp": [ @@ -13,9 +13,21 @@ } ], "dhcp": [ - { "registered_devices": true }, - { "hostname": "isy*", "macaddress": "0021B9*" }, - { "hostname": "polisy*", "macaddress": "000DB9*" } + { + "registered_devices": true + }, + { + "hostname": "isy*", + "macaddress": "0021B9*" + }, + { + "hostname": "eisy*", + "macaddress": "0021B9*" + }, + { + "hostname": "polisy*", + "macaddress": "000DB9*" + } ], "iot_class": "local_push", "loggers": ["pyisy"] diff --git a/homeassistant/components/isy994/models.py b/homeassistant/components/isy994/models.py new file mode 100644 index 00000000000..202bebb32f8 --- /dev/null +++ b/homeassistant/components/isy994/models.py @@ -0,0 +1,96 @@ +"""The ISY/IoX integration data models.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import cast + +from pyisy import ISY +from pyisy.constants import PROTO_INSTEON +from pyisy.networking import NetworkCommand +from pyisy.nodes import Group, Node +from pyisy.programs import Program +from pyisy.variables import Variable + +from homeassistant.const import Platform +from homeassistant.helpers.entity import DeviceInfo + +from .const import ( + CONF_NETWORK, + NODE_AUX_PROP_PLATFORMS, + NODE_PLATFORMS, + PROGRAM_PLATFORMS, + ROOT_NODE_PLATFORMS, + VARIABLE_PLATFORMS, +) + + +@dataclass +class IsyData: + """Data for the ISY/IoX integration.""" + + root: ISY + nodes: dict[Platform, list[Node | Group]] + root_nodes: dict[Platform, list[Node]] + variables: dict[Platform, list[Variable]] + programs: dict[Platform, list[tuple[str, Program, Program]]] + net_resources: list[NetworkCommand] + devices: dict[str, DeviceInfo] + aux_properties: dict[Platform, list[tuple[Node, str]]] + + def __init__(self) -> None: + """Initialize an empty ISY data class.""" + self.nodes = {p: [] for p in NODE_PLATFORMS} + self.root_nodes = {p: [] for p in ROOT_NODE_PLATFORMS} + self.aux_properties = {p: [] for p in NODE_AUX_PROP_PLATFORMS} + self.programs = {p: [] for p in PROGRAM_PLATFORMS} + self.variables = {p: [] for p in VARIABLE_PLATFORMS} + self.net_resources = [] + self.devices = {} + + @property + def uuid(self) -> str: + """Return the ISY UUID identification.""" + return cast(str, self.root.uuid) + + def uid_base(self, node: Node | Group | Variable | Program | NetworkCommand) -> str: + """Return the unique id base string for a given node.""" + if isinstance(node, NetworkCommand): + return f"{self.uuid}_{CONF_NETWORK}_{node.address}" + return f"{self.uuid}_{node.address}" + + @property + def unique_ids(self) -> set[tuple[Platform, str]]: + """Return all the unique ids for a config entry id.""" + current_unique_ids: set[tuple[Platform, str]] = { + (Platform.BUTTON, f"{self.uuid}_query") + } + + # Structure and prefixes here must match what's added in __init__ and helpers + for platform in NODE_PLATFORMS: + for node in self.nodes[platform]: + current_unique_ids.add((platform, self.uid_base(node))) + + for platform in NODE_AUX_PROP_PLATFORMS: + for node, control in self.aux_properties[platform]: + current_unique_ids.add((platform, f"{self.uid_base(node)}_{control}")) + + for platform in PROGRAM_PLATFORMS: + for _, node, _ in self.programs[platform]: + current_unique_ids.add((platform, self.uid_base(node))) + + for platform in VARIABLE_PLATFORMS: + for node in self.variables[platform]: + current_unique_ids.add((platform, self.uid_base(node))) + if platform == Platform.NUMBER: + current_unique_ids.add((platform, f"{self.uid_base(node)}_init")) + + for platform in ROOT_NODE_PLATFORMS: + for node in self.root_nodes[platform]: + current_unique_ids.add((platform, f"{self.uid_base(node)}_query")) + if platform == Platform.BUTTON and node.protocol == PROTO_INSTEON: + current_unique_ids.add((platform, f"{self.uid_base(node)}_beep")) + + for node in self.net_resources: + current_unique_ids.add((Platform.BUTTON, self.uid_base(node))) + + return current_unique_ids diff --git a/homeassistant/components/isy994/number.py b/homeassistant/components/isy994/number.py new file mode 100644 index 00000000000..ada40bb9186 --- /dev/null +++ b/homeassistant/components/isy994/number.py @@ -0,0 +1,300 @@ +"""Support for ISY number entities.""" +from __future__ import annotations + +from dataclasses import replace +from typing import Any + +from pyisy.constants import ( + ATTR_ACTION, + CMD_BACKLIGHT, + DEV_BL_ADDR, + DEV_CMD_MEMORY_WRITE, + DEV_MEMORY, + ISY_VALUE_UNKNOWN, + PROP_ON_LEVEL, + TAG_ADDRESS, + UOM_PERCENTAGE, +) +from pyisy.helpers import EventListener, NodeProperty +from pyisy.nodes import Node, NodeChangedEvent +from pyisy.variables import Variable + +from homeassistant.components.number import ( + NumberEntity, + NumberEntityDescription, + NumberMode, + RestoreNumber, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_VARIABLES, + PERCENTAGE, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.percentage import ( + percentage_to_ranged_value, + ranged_value_to_percentage, +) + +from .const import ( + CONF_VAR_SENSOR_STRING, + DEFAULT_VAR_SENSOR_STRING, + DOMAIN, + UOM_8_BIT_RANGE, +) +from .entity import ISYAuxControlEntity +from .helpers import convert_isy_value_to_hass + +ISY_MAX_SIZE = (2**32) / 2 +ON_RANGE = (1, 255) # Off is not included +CONTROL_DESC = { + PROP_ON_LEVEL: NumberEntityDescription( + key=PROP_ON_LEVEL, + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.CONFIG, + native_min_value=1.0, + native_max_value=100.0, + native_step=1.0, + ), + CMD_BACKLIGHT: NumberEntityDescription( + key=CMD_BACKLIGHT, + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.CONFIG, + native_min_value=0.0, + native_max_value=100.0, + native_step=1.0, + ), +} +BACKLIGHT_MEMORY_FILTER = {"memory": DEV_BL_ADDR, "cmd1": DEV_CMD_MEMORY_WRITE} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ISY/IoX number entities from config entry.""" + isy_data = hass.data[DOMAIN][config_entry.entry_id] + device_info = isy_data.devices + entities: list[ + ISYVariableNumberEntity | ISYAuxControlNumberEntity | ISYBacklightNumberEntity + ] = [] + var_id = config_entry.options.get(CONF_VAR_SENSOR_STRING, DEFAULT_VAR_SENSOR_STRING) + + for node in isy_data.variables[Platform.NUMBER]: + step = 10 ** (-1 * int(node.prec)) + min_max = ISY_MAX_SIZE / (10 ** int(node.prec)) + description = NumberEntityDescription( + key=node.address, + name=node.name, + entity_registry_enabled_default=var_id in node.name, + native_unit_of_measurement=None, + native_step=step, + native_min_value=-min_max, + native_max_value=min_max, + ) + description_init = replace( + description, + key=f"{node.address}_init", + name=f"{node.name} Initial Value", + entity_category=EntityCategory.CONFIG, + ) + + entities.append( + ISYVariableNumberEntity( + node, + unique_id=isy_data.uid_base(node), + description=description, + device_info=device_info[CONF_VARIABLES], + ) + ) + entities.append( + ISYVariableNumberEntity( + node=node, + unique_id=f"{isy_data.uid_base(node)}_init", + description=description_init, + device_info=device_info[CONF_VARIABLES], + init_entity=True, + ) + ) + + for node, control in isy_data.aux_properties[Platform.NUMBER]: + entity_init_info = { + "node": node, + "control": control, + "unique_id": f"{isy_data.uid_base(node)}_{control}", + "description": CONTROL_DESC[control], + "device_info": device_info.get(node.primary_node), + } + if control == CMD_BACKLIGHT: + entities.append(ISYBacklightNumberEntity(**entity_init_info)) + continue + entities.append(ISYAuxControlNumberEntity(**entity_init_info)) + async_add_entities(entities) + + +class ISYAuxControlNumberEntity(ISYAuxControlEntity, NumberEntity): + """Representation of a ISY/IoX Aux Control Number entity.""" + + _attr_mode = NumberMode.SLIDER + + @property + def native_value(self) -> float | int | None: + """Return the state of the variable.""" + node_prop: NodeProperty = self._node.aux_properties[self._control] + if node_prop.value == ISY_VALUE_UNKNOWN: + return None + + if ( + self.entity_description.native_unit_of_measurement == PERCENTAGE + and node_prop.uom == UOM_8_BIT_RANGE # Insteon 0-255 + ): + return ranged_value_to_percentage(ON_RANGE, node_prop.value) + return int(node_prop.value) + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + node_prop: NodeProperty = self._node.aux_properties[self._control] + + if self.entity_description.native_unit_of_measurement == PERCENTAGE: + value = ( + percentage_to_ranged_value(ON_RANGE, round(value)) + if node_prop.uom == UOM_8_BIT_RANGE + else value + ) + if self._control == PROP_ON_LEVEL: + await self._node.set_on_level(value) + return + + if not await self._node.send_cmd(self._control, val=value, uom=node_prop.uom): + raise HomeAssistantError( + f"Could not set {self.name} to {value} for {self._node.address}" + ) + + +class ISYVariableNumberEntity(NumberEntity): + """Representation of an ISY variable as a number entity device.""" + + _attr_has_entity_name = False + _attr_should_poll = False + _init_entity: bool + _node: Variable + entity_description: NumberEntityDescription + + def __init__( + self, + node: Variable, + unique_id: str, + description: NumberEntityDescription, + device_info: DeviceInfo, + init_entity: bool = False, + ) -> None: + """Initialize the ISY variable number.""" + self._node = node + self.entity_description = description + self._change_handler: EventListener | None = None + + # Two entities are created for each variable, one for current value and one for initial. + # Initial value entities are disabled by default + self._init_entity = init_entity + self._attr_unique_id = unique_id + self._attr_device_info = device_info + + async def async_added_to_hass(self) -> None: + """Subscribe to the node change events.""" + self._change_handler = self._node.status_events.subscribe(self.async_on_update) + + @callback + def async_on_update(self, event: NodeProperty) -> None: + """Handle the update event from the ISY Node.""" + self.async_write_ha_state() + + @property + def native_value(self) -> float | int | None: + """Return the state of the variable.""" + return convert_isy_value_to_hass( + self._node.init if self._init_entity else self._node.status, + "", + self._node.prec, + ) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Get the state attributes for the device.""" + return { + "last_edited": self._node.last_edited, + } + + async def async_set_native_value(self, value: float) -> None: + """Set new value.""" + if not await self._node.set_value(value, init=self._init_entity): + raise HomeAssistantError( + f"Could not set {self.name} to {value} for {self._node.address}" + ) + + +class ISYBacklightNumberEntity(ISYAuxControlEntity, RestoreNumber): + """Representation of a ISY/IoX Backlight Number entity.""" + + _assumed_state = True # Backlight values aren't read from device + + def __init__( + self, + node: Node, + control: str, + unique_id: str, + description: NumberEntityDescription, + device_info: DeviceInfo | None, + ) -> None: + """Initialize the ISY Backlight number entity.""" + super().__init__(node, control, unique_id, description, device_info) + self._memory_change_handler: EventListener | None = None + self._attr_native_value = 0 + + async def async_added_to_hass(self) -> None: + """Load the last known state when added to hass.""" + await super().async_added_to_hass() + if (last_state := await self.async_get_last_state()) and ( + last_number_data := await self.async_get_last_number_data() + ): + if last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): + self._attr_native_value = last_number_data.native_value + + # Listen to memory writing events to update state if changed in ISY + self._memory_change_handler = self._node.isy.nodes.status_events.subscribe( + self.async_on_memory_write, + event_filter={ + TAG_ADDRESS: self._node.address, + ATTR_ACTION: DEV_MEMORY, + }, + key=self.unique_id, + ) + + @callback + def async_on_memory_write(self, event: NodeChangedEvent, key: str) -> None: + """Handle a memory write event from the ISY Node.""" + if not (BACKLIGHT_MEMORY_FILTER.items() <= event.event_info.items()): + return # This was not a backlight event + value = ranged_value_to_percentage((0, 127), event.event_info["value"]) + if value == self._attr_native_value: + return # Change was from this entity, don't update twice + self._attr_native_value = value + self.async_write_ha_state() + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + + if not await self._node.send_cmd( + CMD_BACKLIGHT, val=int(value), uom=UOM_PERCENTAGE + ): + raise HomeAssistantError( + f"Could not set backlight to {value}% for {self._node.address}" + ) + self._attr_native_value = value + self.async_write_ha_state() diff --git a/homeassistant/components/isy994/select.py b/homeassistant/components/isy994/select.py new file mode 100644 index 00000000000..49044d43724 --- /dev/null +++ b/homeassistant/components/isy994/select.py @@ -0,0 +1,206 @@ +"""Support for ISY select entities.""" +from __future__ import annotations + +from typing import cast + +from pyisy.constants import ( + ATTR_ACTION, + BACKLIGHT_INDEX, + CMD_BACKLIGHT, + COMMAND_FRIENDLY_NAME, + DEV_BL_ADDR, + DEV_CMD_MEMORY_WRITE, + DEV_MEMORY, + INSTEON_RAMP_RATES, + ISY_VALUE_UNKNOWN, + PROP_RAMP_RATE, + TAG_ADDRESS, + UOM_INDEX as ISY_UOM_INDEX, + UOM_TO_STATES, +) +from pyisy.helpers import EventListener, NodeProperty +from pyisy.nodes import Node, NodeChangedEvent + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform, UnitOfTime +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.restore_state import RestoreEntity + +from .const import _LOGGER, DOMAIN, UOM_INDEX +from .entity import ISYAuxControlEntity +from .models import IsyData + + +def time_string(i: int) -> str: + """Return a formatted ramp rate time string.""" + if i >= 60: + return f"{(float(i)/60):.1f} {UnitOfTime.MINUTES}" + return f"{i} {UnitOfTime.SECONDS}" + + +RAMP_RATE_OPTIONS = [time_string(rate) for rate in INSTEON_RAMP_RATES.values()] +BACKLIGHT_MEMORY_FILTER = {"memory": DEV_BL_ADDR, "cmd1": DEV_CMD_MEMORY_WRITE} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up ISY/IoX select entities from config entry.""" + isy_data: IsyData = hass.data[DOMAIN][config_entry.entry_id] + device_info = isy_data.devices + entities: list[ + ISYAuxControlIndexSelectEntity + | ISYRampRateSelectEntity + | ISYBacklightSelectEntity + ] = [] + + for node, control in isy_data.aux_properties[Platform.SELECT]: + name = COMMAND_FRIENDLY_NAME.get(control, control).replace("_", " ").title() + if node.address != node.primary_node: + name = f"{node.name} {name}" + + options = [] + if control == PROP_RAMP_RATE: + options = RAMP_RATE_OPTIONS + elif control == CMD_BACKLIGHT: + options = BACKLIGHT_INDEX + else: + if uom := node.aux_properties[control].uom == UOM_INDEX: + if options_dict := UOM_TO_STATES.get(uom): + options = list(options_dict.values()) + + description = SelectEntityDescription( + key=f"{node.address}_{control}", + name=name, + entity_category=EntityCategory.CONFIG, + options=options, + ) + entity_detail = { + "node": node, + "control": control, + "unique_id": f"{isy_data.uid_base(node)}_{control}", + "description": description, + "device_info": device_info.get(node.primary_node), + } + + if control == PROP_RAMP_RATE: + entities.append(ISYRampRateSelectEntity(**entity_detail)) + continue + if control == CMD_BACKLIGHT: + entities.append(ISYBacklightSelectEntity(**entity_detail)) + continue + if node.uom == UOM_INDEX and options: + entities.append(ISYAuxControlIndexSelectEntity(**entity_detail)) + continue + # Future: support Node Server custom index UOMs + _LOGGER.debug( + "ISY missing node index unit definitions for %s: %s", node.name, name + ) + async_add_entities(entities) + + +class ISYRampRateSelectEntity(ISYAuxControlEntity, SelectEntity): + """Representation of a ISY/IoX Aux Control Ramp Rate Select entity.""" + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + node_prop: NodeProperty = self._node.aux_properties[self._control] + if node_prop.value == ISY_VALUE_UNKNOWN: + return None + + return RAMP_RATE_OPTIONS[int(node_prop.value)] + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + + await self._node.set_ramp_rate(RAMP_RATE_OPTIONS.index(option)) + + +class ISYAuxControlIndexSelectEntity(ISYAuxControlEntity, SelectEntity): + """Representation of a ISY/IoX Aux Control Index Select entity.""" + + @property + def current_option(self) -> str | None: + """Return the selected entity option to represent the entity state.""" + node_prop: NodeProperty = self._node.aux_properties[self._control] + if node_prop.value == ISY_VALUE_UNKNOWN: + return None + + if options_dict := UOM_TO_STATES.get(node_prop.uom): + return cast(str, options_dict.get(node_prop.value, node_prop.value)) + return cast(str, node_prop.formatted) + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + node_prop: NodeProperty = self._node.aux_properties[self._control] + + await self._node.send_cmd( + self._control, val=self.options.index(option), uom=node_prop.uom + ) + + +class ISYBacklightSelectEntity(ISYAuxControlEntity, SelectEntity, RestoreEntity): + """Representation of a ISY/IoX Backlight Select entity.""" + + _assumed_state = True # Backlight values aren't read from device + + def __init__( + self, + node: Node, + control: str, + unique_id: str, + description: SelectEntityDescription, + device_info: DeviceInfo | None, + ) -> None: + """Initialize the ISY Backlight Select entity.""" + super().__init__(node, control, unique_id, description, device_info) + self._memory_change_handler: EventListener | None = None + self._attr_current_option = None + + async def async_added_to_hass(self) -> None: + """Load the last known state when added to hass.""" + await super().async_added_to_hass() + if ( + last_state := await self.async_get_last_state() + ) and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): + self._attr_current_option = last_state.state + + # Listen to memory writing events to update state if changed in ISY + self._memory_change_handler = self._node.isy.nodes.status_events.subscribe( + self.async_on_memory_write, + event_filter={ + TAG_ADDRESS: self._node.address, + ATTR_ACTION: DEV_MEMORY, + }, + key=self.unique_id, + ) + + @callback + def async_on_memory_write(self, event: NodeChangedEvent, key: str) -> None: + """Handle a memory write event from the ISY Node.""" + if not (BACKLIGHT_MEMORY_FILTER.items() <= event.event_info.items()): + return # This was not a backlight event + option = BACKLIGHT_INDEX[event.event_info["value"]] + if option == self._attr_current_option: + return # Change was from this entity, don't update twice + self._attr_current_option = option + self.async_write_ha_state() + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + + if not await self._node.send_cmd( + CMD_BACKLIGHT, val=BACKLIGHT_INDEX.index(option), uom=ISY_UOM_INDEX + ): + raise HomeAssistantError( + f"Could not set backlight to {option} for {self._node.address}" + ) + self._attr_current_option = option + self.async_write_ha_state() diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 3931c9d4cf9..b44321d9b59 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -1,13 +1,15 @@ -"""Support for ISY994 sensors.""" +"""Support for ISY sensors.""" from __future__ import annotations from typing import Any, cast from pyisy.constants import ( + ATTR_ACTION, + ATTR_CONTROL, COMMAND_FRIENDLY_NAME, ISY_VALUE_UNKNOWN, + NC_NODE_ENABLED, PROP_BATTERY_LEVEL, - PROP_BUSY, PROP_COMMS_ERROR, PROP_ENERGY_MODE, PROP_HEAT_COOL_STATE, @@ -16,28 +18,26 @@ from pyisy.constants import ( PROP_RAMP_RATE, PROP_STATUS, PROP_TEMPERATURE, + TAG_ADDRESS, ) -from pyisy.helpers import NodeProperty -from pyisy.nodes import Node +from pyisy.helpers import EventListener, NodeProperty +from pyisy.nodes import Node, NodeChangedEvent +from pyisy.variables import Variable from homeassistant.components.sensor import ( - DOMAIN as SENSOR, SensorDeviceClass, SensorEntity, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import UnitOfTemperature -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import EntityCategory +from homeassistant.const import Platform, UnitOfTemperature +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( _LOGGER, - DOMAIN as ISY994_DOMAIN, - ISY994_NODES, - ISY994_VARIABLES, - SENSOR_AUX, + DOMAIN, UOM_DOUBLE_TEMP, UOM_FRIENDLY_NAME, UOM_INDEX, @@ -45,28 +45,56 @@ from .const import ( UOM_TO_STATES, ) from .entity import ISYEntity, ISYNodeEntity -from .helpers import convert_isy_value_to_hass, migrate_old_unique_ids +from .helpers import convert_isy_value_to_hass # Disable general purpose and redundant sensors by default AUX_DISABLED_BY_DEFAULT_MATCH = ["GV", "DO"] AUX_DISABLED_BY_DEFAULT_EXACT = { + PROP_COMMS_ERROR, PROP_ENERGY_MODE, PROP_HEAT_COOL_STATE, PROP_ON_LEVEL, PROP_RAMP_RATE, PROP_STATUS, } -SKIP_AUX_PROPERTIES = {PROP_BUSY, PROP_COMMS_ERROR, PROP_STATUS} +# Reference pyisy.constants.COMMAND_FRIENDLY_NAME for API details. +# Note: "LUMIN"/Illuminance removed, some devices use non-conformant "%" unit +# "VOCLVL"/VOC removed, uses qualitative UOM not ug/m^3 ISY_CONTROL_TO_DEVICE_CLASS = { PROP_BATTERY_LEVEL: SensorDeviceClass.BATTERY, PROP_HUMIDITY: SensorDeviceClass.HUMIDITY, PROP_TEMPERATURE: SensorDeviceClass.TEMPERATURE, - "BARPRES": SensorDeviceClass.PRESSURE, + "BARPRES": SensorDeviceClass.ATMOSPHERIC_PRESSURE, + "CC": SensorDeviceClass.CURRENT, "CO2LVL": SensorDeviceClass.CO2, + "CPW": SensorDeviceClass.POWER, "CV": SensorDeviceClass.VOLTAGE, - "LUMIN": SensorDeviceClass.ILLUMINANCE, + "DEWPT": SensorDeviceClass.TEMPERATURE, + "DISTANC": SensorDeviceClass.DISTANCE, + "ETO": SensorDeviceClass.PRECIPITATION_INTENSITY, + "FATM": SensorDeviceClass.WEIGHT, + "FREQ": SensorDeviceClass.FREQUENCY, + "MUSCLEM": SensorDeviceClass.WEIGHT, "PF": SensorDeviceClass.POWER_FACTOR, + "PM10": SensorDeviceClass.PM10, + "PM25": SensorDeviceClass.PM25, + "PRECIP": SensorDeviceClass.PRECIPITATION, + "RAINRT": SensorDeviceClass.PRECIPITATION_INTENSITY, + "RFSS": SensorDeviceClass.SIGNAL_STRENGTH, + "SOILH": SensorDeviceClass.MOISTURE, + "SOILT": SensorDeviceClass.TEMPERATURE, + "SOLRAD": SensorDeviceClass.IRRADIANCE, + "SPEED": SensorDeviceClass.SPEED, + "TEMPEXH": SensorDeviceClass.TEMPERATURE, + "TEMPOUT": SensorDeviceClass.TEMPERATURE, + "TPW": SensorDeviceClass.ENERGY, + "WATERP": SensorDeviceClass.PRESSURE, + "WATERT": SensorDeviceClass.TEMPERATURE, + "WATERTB": SensorDeviceClass.TEMPERATURE, + "WATERTD": SensorDeviceClass.TEMPERATURE, + "WEIGHT": SensorDeviceClass.WEIGHT, + "WINDCH": SensorDeviceClass.TEMPERATURE, } ISY_CONTROL_TO_STATE_CLASS = { control: SensorStateClass.MEASUREMENT for control in ISY_CONTROL_TO_DEVICE_CLASS @@ -81,38 +109,39 @@ ISY_CONTROL_TO_ENTITY_CATEGORY = { async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the ISY994 sensor platform.""" - hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] + """Set up the ISY sensor platform.""" + isy_data = hass.data[DOMAIN][entry.entry_id] entities: list[ISYSensorEntity | ISYSensorVariableEntity] = [] + devices: dict[str, DeviceInfo] = isy_data.devices - for node in hass_isy_data[ISY994_NODES][SENSOR]: + for node in isy_data.nodes[Platform.SENSOR]: _LOGGER.debug("Loading %s", node.name) - entities.append(ISYSensorEntity(node)) + entities.append(ISYSensorEntity(node, devices.get(node.primary_node))) - aux_nodes = set() - for node, control in hass_isy_data[ISY994_NODES][SENSOR_AUX]: - aux_nodes.add(node) - if control in SKIP_AUX_PROPERTIES: - continue - _LOGGER.debug("Loading %s %s", node.name, node.aux_properties[control]) + aux_sensors_list = isy_data.aux_properties[Platform.SENSOR] + for node, control in aux_sensors_list: + _LOGGER.debug("Loading %s %s", node.name, COMMAND_FRIENDLY_NAME.get(control)) enabled_default = control not in AUX_DISABLED_BY_DEFAULT_EXACT and not any( control.startswith(match) for match in AUX_DISABLED_BY_DEFAULT_MATCH ) - entities.append(ISYAuxSensorEntity(node, control, enabled_default)) + entities.append( + ISYAuxSensorEntity( + node=node, + control=control, + enabled_default=enabled_default, + unique_id=f"{isy_data.uid_base(node)}_{control}", + device_info=devices.get(node.primary_node), + ) + ) - for node in aux_nodes: - # Any node in SENSOR_AUX can potentially have communication errors - entities.append(ISYAuxSensorEntity(node, PROP_COMMS_ERROR, False)) + for variable in isy_data.variables[Platform.SENSOR]: + entities.append(ISYSensorVariableEntity(variable)) - for vname, vobj in hass_isy_data[ISY994_VARIABLES]: - entities.append(ISYSensorVariableEntity(vname, vobj)) - - await migrate_old_unique_ids(hass, SENSOR, entities) async_add_entities(entities) class ISYSensorEntity(ISYNodeEntity, SensorEntity): - """Representation of an ISY994 sensor device.""" + """Representation of an ISY sensor device.""" @property def target(self) -> Node | NodeProperty | None: @@ -126,7 +155,7 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): @property def raw_unit_of_measurement(self) -> dict | str | None: - """Get the raw unit of measurement for the ISY994 sensor device.""" + """Get the raw unit of measurement for the ISY sensor device.""" if self.target is None: return None @@ -148,7 +177,7 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): @property def native_value(self) -> float | int | str | None: - """Get the state of the ISY994 sensor device.""" + """Get the state of the ISY sensor device.""" if self.target is None: return None @@ -199,16 +228,29 @@ class ISYSensorEntity(ISYNodeEntity, SensorEntity): class ISYAuxSensorEntity(ISYSensorEntity): - """Representation of an ISY994 aux sensor device.""" + """Representation of an ISY aux sensor device.""" - def __init__(self, node: Node, control: str, enabled_default: bool) -> None: - """Initialize the ISY994 aux sensor.""" - super().__init__(node) + def __init__( + self, + node: Node, + control: str, + enabled_default: bool, + unique_id: str, + device_info: DeviceInfo | None = None, + ) -> None: + """Initialize the ISY aux sensor.""" + super().__init__(node, device_info=device_info) self._control = control self._attr_entity_registry_enabled_default = enabled_default self._attr_entity_category = ISY_CONTROL_TO_ENTITY_CATEGORY.get(control) self._attr_device_class = ISY_CONTROL_TO_DEVICE_CLASS.get(control) self._attr_state_class = ISY_CONTROL_TO_STATE_CLASS.get(control) + self._attr_unique_id = unique_id + self._change_handler: EventListener = None + self._availability_handler: EventListener = None + + name = COMMAND_FRIENDLY_NAME.get(self._control, self._control) + self._attr_name = f"{node.name} {name.replace('_', ' ').title()}" @property def target(self) -> Node | NodeProperty | None: @@ -223,28 +265,45 @@ class ISYAuxSensorEntity(ISYSensorEntity): """Return the target value.""" return None if self.target is None else self.target.value - @property - def unique_id(self) -> str | None: - """Get the unique identifier of the device and aux sensor.""" - if not hasattr(self._node, "address"): - return None - return f"{self._node.isy.configuration['uuid']}_{self._node.address}_{self._control}" + async def async_added_to_hass(self) -> None: + """Subscribe to the node control change events. + + Overloads the default ISYNodeEntity updater to only update when + this control is changed on the device and prevent duplicate firing + of `isy994_control` events. + """ + self._change_handler = self._node.control_events.subscribe( + self.async_on_update, event_filter={ATTR_CONTROL: self._control} + ) + self._availability_handler = self._node.isy.nodes.status_events.subscribe( + self.async_on_update, + event_filter={ + TAG_ADDRESS: self._node.address, + ATTR_ACTION: NC_NODE_ENABLED, + }, + ) + + @callback + def async_on_update(self, event: NodeProperty | NodeChangedEvent) -> None: + """Handle a control event from the ISY Node.""" + self.async_write_ha_state() @property - def name(self) -> str: - """Get the name of the device and aux sensor.""" - base_name = self._name or str(self._node.name) - name = COMMAND_FRIENDLY_NAME.get(self._control, self._control) - return f"{base_name} {name.replace('_', ' ').title()}" + def available(self) -> bool: + """Return entity availability.""" + return cast(bool, self._node.enabled) class ISYSensorVariableEntity(ISYEntity, SensorEntity): - """Representation of an ISY994 variable as a sensor device.""" + """Representation of an ISY variable as a sensor device.""" - def __init__(self, vname: str, vobj: object) -> None: - """Initialize the ISY994 binary sensor program.""" - super().__init__(vobj) - self._name = vname + # Deprecated sensors, will be removed in 2023.5.0 + _attr_entity_registry_enabled_default = False + + def __init__(self, variable_node: Variable) -> None: + """Initialize the ISY binary sensor program.""" + super().__init__(variable_node) + self._name = variable_node.name @property def native_value(self) -> float | int | None: diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index 18076e2da98..759ebfbde0e 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -13,16 +13,18 @@ from homeassistant.const import ( CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, SERVICE_RELOAD, + Platform, ) from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import async_get_platforms import homeassistant.helpers.entity_registry as er +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.service import entity_service_call -from .const import _LOGGER, DOMAIN, ISY994_ISY -from .util import unique_ids_for_config_entry_id +from .const import _LOGGER, CONF_NETWORK, DOMAIN, ISY_CONF_NAME, ISY_CONF_NETWORKING +from .util import _async_cleanup_registry_entries # Common Services for All Platforms: SERVICE_SYSTEM_QUERY = "system_query" @@ -181,10 +183,11 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 """Handle a system query service call.""" address = service.data.get(CONF_ADDRESS) isy_name = service.data.get(CONF_ISY) - + entity_registry = er.async_get(hass) for config_entry_id in hass.data[DOMAIN]: - isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] - if isy_name and isy_name != isy.configuration["name"]: + isy_data = hass.data[DOMAIN][config_entry_id] + isy = isy_data.root + if isy_name and isy_name != isy.conf["name"]: continue # If an address is provided, make sure we query the correct ISY. # Otherwise, query the whole system on all ISY's connected. @@ -192,14 +195,32 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 _LOGGER.debug( "Requesting query of device %s on ISY %s", address, - isy.configuration["uuid"], + isy.uuid, ) await isy.query(address) + async_log_deprecated_service_call( + hass, + call=service, + alternate_service="button.press", + alternate_target=entity_registry.async_get_entity_id( + Platform.BUTTON, + DOMAIN, + f"{isy.uuid}_{address}_query", + ), + breaks_in_ha_version="2023.5.0", + ) return - _LOGGER.debug( - "Requesting system query of ISY %s", isy.configuration["uuid"] - ) + _LOGGER.debug("Requesting system query of ISY %s", isy.uuid) await isy.query() + async_log_deprecated_service_call( + hass, + call=service, + alternate_service="button.press", + alternate_target=entity_registry.async_get_entity_id( + Platform.BUTTON, DOMAIN, f"{isy.uuid}_query" + ), + breaks_in_ha_version="2023.5.0", + ) async def async_run_network_resource_service_handler(service: ServiceCall) -> None: """Handle a network resource service call.""" @@ -208,10 +229,11 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 isy_name = service.data.get(CONF_ISY) for config_entry_id in hass.data[DOMAIN]: - isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] - if isy_name and isy_name != isy.configuration["name"]: + isy_data = hass.data[DOMAIN][config_entry_id] + isy = isy_data.root + if isy_name and isy_name != isy.conf[ISY_CONF_NAME]: continue - if not hasattr(isy, "networking") or isy.networking is None: + if isy.networking is None or not isy.conf[ISY_CONF_NETWORKING]: continue command = None if address: @@ -220,6 +242,18 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 command = isy.networking.get_by_name(name) if command is not None: await command.run() + entity_registry = er.async_get(hass) + async_log_deprecated_service_call( + hass, + call=service, + alternate_service="button.press", + alternate_target=entity_registry.async_get_entity_id( + Platform.BUTTON, + DOMAIN, + f"{isy.uuid}_{CONF_NETWORK}_{address}", + ), + breaks_in_ha_version="2023.5.0", + ) return _LOGGER.error( "Could not run network resource command; not found or enabled on the ISY" @@ -233,8 +267,9 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 isy_name = service.data.get(CONF_ISY) for config_entry_id in hass.data[DOMAIN]: - isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] - if isy_name and isy_name != isy.configuration["name"]: + isy_data = hass.data[DOMAIN][config_entry_id] + isy = isy_data.root + if isy_name and isy_name != isy.conf["name"]: continue program = None if address: @@ -256,8 +291,9 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 isy_name = service.data.get(CONF_ISY) for config_entry_id in hass.data[DOMAIN]: - isy = hass.data[DOMAIN][config_entry_id][ISY994_ISY] - if isy_name and isy_name != isy.configuration["name"]: + isy_data = hass.data[DOMAIN][config_entry_id] + isy = isy_data.root + if isy_name and isy_name != isy.conf["name"]: continue variable = None if name: @@ -266,50 +302,43 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901 variable = isy.variables.vobjs[vtype].get(address) if variable is not None: await variable.set_value(value, init) + entity_registry = er.async_get(hass) + async_log_deprecated_service_call( + hass, + call=service, + alternate_service="number.set_value", + alternate_target=entity_registry.async_get_entity_id( + Platform.NUMBER, + DOMAIN, + f"{isy.uuid}_{address}{'_init' if init else ''}", + ), + breaks_in_ha_version="2023.5.0", + ) return _LOGGER.error("Could not set variable value; not found or enabled on the ISY") @callback def async_cleanup_registry_entries(service: ServiceCall) -> None: """Remove extra entities that are no longer part of the integration.""" - entity_registry = er.async_get(hass) - config_ids = [] - current_unique_ids: set[str] = set() - - for config_entry_id in hass.data[DOMAIN]: - entries_for_this_config = er.async_entries_for_config_entry( - entity_registry, config_entry_id - ) - config_ids.extend( - [ - (entity.unique_id, entity.entity_id) - for entity in entries_for_this_config - ] - ) - current_unique_ids |= unique_ids_for_config_entry_id(hass, config_entry_id) - - extra_entities = [ - entity_id - for unique_id, entity_id in config_ids - if unique_id not in current_unique_ids - ] - - for entity_id in extra_entities: - if entity_registry.async_is_registered(entity_id): - entity_registry.async_remove(entity_id) - - _LOGGER.debug( - ( - "Cleaning up ISY994 Entities and devices: Config Entries: %s, Current" - " Entries: %s, Extra Entries Removed: %s" - ), - len(config_ids), - len(current_unique_ids), - len(extra_entities), + async_log_deprecated_service_call( + hass, + call=service, + alternate_service="homeassistant.reload_core_config", + alternate_target=None, + breaks_in_ha_version="2023.5.0", ) + for config_entry_id in hass.data[DOMAIN]: + _async_cleanup_registry_entries(hass, config_entry_id) async def async_reload_config_entries(service: ServiceCall) -> None: - """Trigger a reload of all ISY994 config entries.""" + """Trigger a reload of all ISY config entries.""" + async_log_deprecated_service_call( + hass, + call=service, + alternate_service="homeassistant.reload_core_config", + alternate_target=None, + breaks_in_ha_version="2023.5.0", + ) for config_entry_id in hass.data[DOMAIN]: hass.async_create_task(hass.config_entries.async_reload(config_entry_id)) @@ -447,3 +476,47 @@ def async_setup_light_services(hass: HomeAssistant) -> None: platform.async_register_entity_service( SERVICE_SET_RAMP_RATE, SERVICE_SET_RAMP_RATE_SCHEMA, "async_set_ramp_rate" ) + + +@callback +def async_log_deprecated_service_call( + hass: HomeAssistant, + call: ServiceCall, + alternate_service: str, + alternate_target: str | None, + breaks_in_ha_version: str, +) -> None: + """Log a warning about a deprecated service call.""" + deprecated_service = f"{call.domain}.{call.service}" + alternate_target = alternate_target or "this device" + + async_create_issue( + hass, + DOMAIN, + f"deprecated_service_{deprecated_service}", + breaks_in_ha_version=breaks_in_ha_version, + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key="deprecated_service", + translation_placeholders={ + "alternate_service": alternate_service, + "alternate_target": alternate_target, + "deprecated_service": deprecated_service, + }, + ) + + alternate_text = "" + if alternate_target: + alternate_text = f' and pass it a target entity ID of "{alternate_target}"' + + _LOGGER.warning( + ( + 'The "%s" service is deprecated and will be removed in %s; use the "%s" ' + "service %s" + ), + deprecated_service, + breaks_in_ha_version, + alternate_service, + alternate_text, + ) diff --git a/homeassistant/components/isy994/services.yaml b/homeassistant/components/isy994/services.yaml index 923dfe1fd6f..e336eaa574b 100644 --- a/homeassistant/components/isy994/services.yaml +++ b/homeassistant/components/isy994/services.yaml @@ -1,4 +1,4 @@ -# Describes the ISY994-specific services available +# Describes the ISY-specific services available # Note: controlling many entity_ids with one call is not recommended since it may result in # flooding the ISY with requests. To control multiple devices with a service call @@ -119,9 +119,9 @@ set_zwave_parameter: - "2" - "4" rename_node: - name: Rename Node on ISY994 + name: Rename Node on ISY description: >- - Rename a node or group (scene) on the ISY994. Note: this will not automatically change the Home Assistant Entity Name or Entity ID to match. + Rename a node or group (scene) on the ISY. Note: this will not automatically change the Home Assistant Entity Name or Entity ID to match. The entity name and ID will only be updated after calling `isy994.reload` or restarting Home Assistant, and ONLY IF you have not already customized the name within Home Assistant. target: @@ -130,14 +130,14 @@ rename_node: fields: name: name: New Name - description: The new name to use within the ISY994. + description: The new name to use within the ISY. required: true example: "Front Door Light" selector: text: set_on_level: - name: Set On Level - description: Send a ISY set_on_level command to a Node. + name: Set On Level (Deprecated) + description: "Send a ISY set_on_level command to a Node. Deprecated: Use On Level Number entity instead." target: entity: integration: isy994 @@ -152,8 +152,8 @@ set_on_level: min: 0 max: 255 set_ramp_rate: - name: Set ramp rate - description: Send a ISY set_ramp_rate command to a Node. + name: Set ramp rate (Deprecated) + description: "Send a ISY set_ramp_rate command to a Node. Deprecated: Use On Level Number entity instead." target: entity: integration: isy994 @@ -168,8 +168,8 @@ set_ramp_rate: min: 0 max: 31 system_query: - name: System query - description: Request the ISY Query the connected devices. + name: System query (Deprecated) + description: "Request the ISY Query the connected devices. Deprecated: Use device Query button entity." fields: address: name: Address @@ -184,8 +184,8 @@ system_query: selector: text: set_variable: - name: Set variable - description: Set an ISY variable's current or initial value. Variables can be set by either type/address or by name. + name: Set variable (Deprecated) + description: "Set an ISY variable's current or initial value. Variables can be set by either type/address or by name. Deprecated: Use number entities instead." fields: address: name: Address @@ -267,8 +267,8 @@ send_program_command: selector: text: run_network_resource: - name: Run network resource - description: Run a network resource on the ISY. + name: Run network resource (Deprecated) + description: "Run a network resource on the ISY. Deprecated: Use Network Resource button entity." fields: address: name: Address @@ -291,7 +291,7 @@ run_network_resource: text: reload: name: Reload - description: Reload the ISY994 connection(s) without restarting Home Assistant. Use to pick up new devices that have been added or changed on the ISY. + description: Reload the ISY connection(s) without restarting Home Assistant. Use to pick up new devices that have been added or changed on the ISY. cleanup_entities: name: Cleanup entities - description: Cleanup old entities and devices no longer used by the ISY994 integrations. Useful if you've removed devices from the ISY or changed the options in the configuration to exclude additional items. + description: Cleanup old entities and devices no longer used by the ISY integration. Useful if you've removed devices from the ISY or changed the options in the configuration to exclude additional items. diff --git a/homeassistant/components/isy994/strings.json b/homeassistant/components/isy994/strings.json index 821f8889978..69852394890 100644 --- a/homeassistant/components/isy994/strings.json +++ b/homeassistant/components/isy994/strings.json @@ -53,5 +53,22 @@ "last_heartbeat": "Last Heartbeat Time", "websocket_status": "Event Socket Status" } + }, + "issues": { + "deprecated_service": { + "title": "The {deprecated_service} service will be removed", + "fix_flow": { + "step": { + "confirm": { + "title": "The {deprecated_service} service will be removed", + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`." + }, + "deprecated_yaml": { + "title": "The ISY/IoX YAML configuration is being removed", + "description": "Configuring Universal Devices ISY/IoX using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `isy994` YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } + } + } } } diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index a92be5d4d23..f6cb68b8a0f 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -1,55 +1,95 @@ -"""Support for ISY994 switches.""" +"""Support for ISY switches.""" from __future__ import annotations from typing import Any -from pyisy.constants import ISY_VALUE_UNKNOWN, PROTO_GROUP +from pyisy.constants import ( + ATTR_ACTION, + ISY_VALUE_UNKNOWN, + NC_NODE_ENABLED, + PROTO_GROUP, + TAG_ADDRESS, +) +from pyisy.helpers import EventListener +from pyisy.nodes import Node, NodeChangedEvent -from homeassistant.components.switch import DOMAIN as SWITCH, SwitchEntity +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import DeviceInfo, EntityCategory, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import _LOGGER, DOMAIN as ISY994_DOMAIN, ISY994_NODES, ISY994_PROGRAMS -from .entity import ISYNodeEntity, ISYProgramEntity -from .helpers import migrate_old_unique_ids +from .const import DOMAIN +from .entity import ISYAuxControlEntity, ISYNodeEntity, ISYProgramEntity +from .models import IsyData async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the ISY994 switch platform.""" - hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id] - entities: list[ISYSwitchProgramEntity | ISYSwitchEntity] = [] - for node in hass_isy_data[ISY994_NODES][SWITCH]: - entities.append(ISYSwitchEntity(node)) + """Set up the ISY switch platform.""" + isy_data: IsyData = hass.data[DOMAIN][entry.entry_id] + entities: list[ + ISYSwitchProgramEntity | ISYSwitchEntity | ISYEnableSwitchEntity + ] = [] + device_info = isy_data.devices + for node in isy_data.nodes[Platform.SWITCH]: + primary = node.primary_node + if node.protocol == PROTO_GROUP and len(node.controllers) == 1: + # If Group has only 1 Controller, link to that device instead of the hub + primary = node.isy.nodes.get_by_id(node.controllers[0]).primary_node - for name, status, actions in hass_isy_data[ISY994_PROGRAMS][SWITCH]: + entities.append(ISYSwitchEntity(node, device_info.get(primary))) + + for name, status, actions in isy_data.programs[Platform.SWITCH]: entities.append(ISYSwitchProgramEntity(name, status, actions)) - await migrate_old_unique_ids(hass, SWITCH, entities) + for node, control in isy_data.aux_properties[Platform.SWITCH]: + # Currently only used for enable switches, will need to be updated for NS support + # by making sure control == TAG_ENABLED + description = SwitchEntityDescription( + key=control, + device_class=SwitchDeviceClass.SWITCH, + name=control.title(), + entity_category=EntityCategory.CONFIG, + ) + entities.append( + ISYEnableSwitchEntity( + node=node, + control=control, + unique_id=f"{isy_data.uid_base(node)}_{control}", + description=description, + device_info=device_info.get(node.primary_node), + ) + ) async_add_entities(entities) class ISYSwitchEntity(ISYNodeEntity, SwitchEntity): - """Representation of an ISY994 switch device.""" + """Representation of an ISY switch device.""" @property def is_on(self) -> bool | None: - """Get whether the ISY994 device is in the on state.""" + """Get whether the ISY 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: Any) -> None: - """Send the turn off command to the ISY994 switch.""" + """Send the turn off command to the ISY switch.""" if not await self._node.turn_off(): - _LOGGER.debug("Unable to turn off switch") + raise HomeAssistantError(f"Unable to turn off switch {self._node.address}") async def async_turn_on(self, **kwargs: Any) -> None: - """Send the turn on command to the ISY994 switch.""" + """Send the turn on command to the ISY switch.""" if not await self._node.turn_on(): - _LOGGER.debug("Unable to turn on switch") + raise HomeAssistantError(f"Unable to turn on switch {self._node.address}") @property def icon(self) -> str | None: @@ -60,24 +100,87 @@ class ISYSwitchEntity(ISYNodeEntity, SwitchEntity): class ISYSwitchProgramEntity(ISYProgramEntity, SwitchEntity): - """A representation of an ISY994 program switch.""" + """A representation of an ISY program switch.""" @property def is_on(self) -> bool: - """Get whether the ISY994 switch program is on.""" + """Get whether the ISY switch program is on.""" return bool(self._node.status) async def async_turn_on(self, **kwargs: Any) -> None: - """Send the turn on command to the ISY994 switch program.""" + """Send the turn on command to the ISY switch program.""" if not await self._actions.run_then(): - _LOGGER.error("Unable to turn on switch") + raise HomeAssistantError( + f"Unable to run 'then' clause on program switch {self._actions.address}" + ) async def async_turn_off(self, **kwargs: Any) -> None: - """Send the turn off command to the ISY994 switch program.""" + """Send the turn off command to the ISY switch program.""" if not await self._actions.run_else(): - _LOGGER.error("Unable to turn off switch") + raise HomeAssistantError( + f"Unable to run 'else' clause on program switch {self._actions.address}" + ) @property def icon(self) -> str: """Get the icon for programs.""" return "mdi:script-text-outline" # Matches isy program icon + + +class ISYEnableSwitchEntity(ISYAuxControlEntity, SwitchEntity): + """A representation of an ISY enable/disable switch.""" + + def __init__( + self, + node: Node, + control: str, + unique_id: str, + description: EntityDescription, + device_info: DeviceInfo | None, + ) -> None: + """Initialize the ISY Aux Control Number entity.""" + super().__init__( + node=node, + control=control, + unique_id=unique_id, + description=description, + device_info=device_info, + ) + self._attr_name = description.name # Override super + self._change_handler: EventListener = None + + async def async_added_to_hass(self) -> None: + """Subscribe to the node control change events.""" + self._change_handler = self._node.isy.nodes.status_events.subscribe( + self.async_on_update, + event_filter={ + TAG_ADDRESS: self._node.address, + ATTR_ACTION: NC_NODE_ENABLED, + }, + key=self.unique_id, + ) + + @callback + def async_on_update(self, event: NodeChangedEvent, key: str) -> None: + """Handle a control event from the ISY Node.""" + self.async_write_ha_state() + + @property + def available(self) -> bool: + """Return entity availability.""" + return True # Enable switch is always available + + @property + def is_on(self) -> bool | None: + """Get whether the ISY device is in the on state.""" + return bool(self._node.enabled) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Send the turn off command to the ISY switch.""" + if not await self._node.disable(): + raise HomeAssistantError(f"Unable to disable device {self._node.address}") + + async def async_turn_on(self, **kwargs: Any) -> None: + """Send the turn on command to the ISY switch.""" + if not await self._node.enable(): + raise HomeAssistantError(f"Unable to enable device {self._node.address}") diff --git a/homeassistant/components/isy994/system_health.py b/homeassistant/components/isy994/system_health.py index a8497ba0b27..44286111a62 100644 --- a/homeassistant/components/isy994/system_health.py +++ b/homeassistant/components/isy994/system_health.py @@ -10,7 +10,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant, callback -from .const import DOMAIN, ISY994_ISY, ISY_URL_POSTFIX +from .const import DOMAIN, ISY_URL_POSTFIX +from .models import IsyData @callback @@ -28,7 +29,8 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]: config_entry_id = next( iter(hass.data[DOMAIN]) ) # Only first ISY is supported for now - isy: ISY = hass.data[DOMAIN][config_entry_id][ISY994_ISY] + isy_data: IsyData = hass.data[DOMAIN][config_entry_id] + isy: ISY = isy_data.root entry = hass.config_entries.async_get_entry(config_entry_id) assert isinstance(entry, ConfigEntry) diff --git a/homeassistant/components/isy994/translations/bg.json b/homeassistant/components/isy994/translations/bg.json index 75bd10bbca9..9d6c39ffbd0 100644 --- a/homeassistant/components/isy994/translations/bg.json +++ b/homeassistant/components/isy994/translations/bg.json @@ -25,5 +25,21 @@ } } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 {deprecated_service} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" + }, + "deprecated_yaml": { + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Universal Devices ISY/IoX \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438\u044f \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 `isy994` \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 ISY/IoX \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } + } + }, + "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 {deprecated_service} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/ca.json b/homeassistant/components/isy994/translations/ca.json index 69c9d02b1bb..979bc4ef8ea 100644 --- a/homeassistant/components/isy994/translations/ca.json +++ b/homeassistant/components/isy994/translations/ca.json @@ -32,6 +32,23 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei perqu\u00e8 passin a utilitzar el servei `{alternate_service}` amb un ID d'entitat objectiu o 'target' `{alternate_target}`.", + "title": "El servei {deprecated_service} s'eliminar\u00e0" + }, + "deprecated_yaml": { + "description": "La configuraci\u00f3 d'ISY/IoX mitjan\u00e7ant YAML s'eliminar\u00e0 de Home Assistant.\n\nLa configuraci\u00f3 YAML existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari.\n\nElimina la configuraci\u00f3 YAML de `isy994` del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML d'ISY/IoX est\u00e0 sent eliminada" + } + } + }, + "title": "El servei {deprecated_service} s'eliminar\u00e0" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/de.json b/homeassistant/components/isy994/translations/de.json index ecc42152766..b9860a877c4 100644 --- a/homeassistant/components/isy994/translations/de.json +++ b/homeassistant/components/isy994/translations/de.json @@ -32,6 +32,23 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst `{alternate_service}` mit einer Zielentit\u00e4ts-ID von `{alternate_target}` zu verwenden.", + "title": "Der Dienst {deprecated_service} wird entfernt" + }, + "deprecated_yaml": { + "description": "Die Konfiguration von Universal Devices ISY/IoX mittels YAML wird entfernt.\n\nDeine bestehende YAML-Konfiguration wurde automatisch in das UI importiert.\n\nEntferne die YAML-Konfiguration `isy994` aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die ISY/IoX YAML-Konfiguration wird entfernt" + } + } + }, + "title": "Der Dienst {deprecated_service} wird entfernt" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/el.json b/homeassistant/components/isy994/translations/el.json index ec32ea756bc..15f538877ba 100644 --- a/homeassistant/components/isy994/translations/el.json +++ b/homeassistant/components/isy994/translations/el.json @@ -28,10 +28,27 @@ "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" + "title": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf ISY \u03c3\u03b1\u03c2" } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03c5\u03c7\u03cc\u03bd \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 `{alternate_service}` \u03bc\u03b5 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2-\u03c3\u03c4\u03cc\u03c7\u03bf\u03c5 `{alternate_target}`.", + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + }, + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Universal Devices ISY/IoX \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9. \n\n \u0397 \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 YAML \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML `isy994` \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 ISY/IoX YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } + }, + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + } + }, "options": { "step": { "init": { @@ -42,7 +59,7 @@ "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" + "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 ISY" } } }, diff --git a/homeassistant/components/isy994/translations/en.json b/homeassistant/components/isy994/translations/en.json index 3610b35c194..1a4ca90d420 100644 --- a/homeassistant/components/isy994/translations/en.json +++ b/homeassistant/components/isy994/translations/en.json @@ -32,6 +32,23 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`.", + "title": "The {deprecated_service} service will be removed" + }, + "deprecated_yaml": { + "description": "Configuring Universal Devices ISY/IoX using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `isy994` YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The ISY/IoX YAML configuration is being removed" + } + } + }, + "title": "The {deprecated_service} service will be removed" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/es.json b/homeassistant/components/isy994/translations/es.json index b320167b3f1..d789aa612c6 100644 --- a/homeassistant/components/isy994/translations/es.json +++ b/homeassistant/components/isy994/translations/es.json @@ -32,6 +32,23 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con una ID de entidad de destino de `{alternate_target}`.", + "title": "Se eliminar\u00e1 el servicio {deprecated_service}" + }, + "deprecated_yaml": { + "description": "Se va a eliminar la configuraci\u00f3n de Universal Devices ISY/IoX mediante YAML. \n\nTu configuraci\u00f3n YAML existente se ha importado a la IU autom\u00e1ticamente. \n\nElimina la configuraci\u00f3n YAML `isy994` de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de ISY/IoX" + } + } + }, + "title": "Se eliminar\u00e1 el servicio {deprecated_service}" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/et.json b/homeassistant/components/isy994/translations/et.json index 0f7e0e45503..2ae215fac5d 100644 --- a/homeassistant/components/isy994/translations/et.json +++ b/homeassistant/components/isy994/translations/et.json @@ -32,6 +32,23 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "V\u00e4rskenda k\u00f5iki automaatikaid v\u00f5i skripte, mis seda teenust kasutavad, et kasutada selle asemel teenust '{alternate_service}', mille sihtolemi ID on '{alternate_target}'.", + "title": "Teenus {deprecated_service} eemaldatakse" + }, + "deprecated_yaml": { + "description": "Universal Devices ISY/IoX konfigureerimine YAML-i abil eemaldatakse.\n\nTeie olemasolev YAML-i konfiguratsioon imporditakse kasutajaliidesesse automaatselt.\n\nEemaldage failist configuration.yaml YAML-i konfiguratsioon \"isy994\" ja taask\u00e4ivitage selle probleemi lahendamiseks koduabiline.", + "title": "ISY/IoX YAML-i konfiguratsiooni eemaldatakse" + } + } + }, + "title": "Teenus {deprecated_service} eemaldatakse" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/hu.json b/homeassistant/components/isy994/translations/hu.json index 4ea107b469f..cad0e2466a5 100644 --- a/homeassistant/components/isy994/translations/hu.json +++ b/homeassistant/components/isy994/translations/hu.json @@ -32,6 +32,23 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Friss\u00edtse a szolg\u00e1ltat\u00e1st haszn\u00e1l\u00f3 automatizmusokat vagy szkripteket, hogy helyette az `{alternate_service}` szolg\u00e1ltat\u00e1st haszn\u00e1lhassa `{alternate_target}` c\u00e9lentit\u00e1s-azonos\u00edt\u00f3val.", + "title": "A(z) {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + }, + "deprecated_yaml": { + "description": "Az Univerz\u00e1lis eszk\u00f6z\u00f6k ISY/IoX konfigur\u00e1l\u00e1sa YAML haszn\u00e1lat\u00e1val elt\u00e1vol\u00edt\u00e1sra ker\u00fcl.\n\nA megl\u00e9v\u0151 YAML konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a `isy994` YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "Az ISY/IoX YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } + }, + "title": "A(z) {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/id.json b/homeassistant/components/isy994/translations/id.json index f27a8c41f5c..f035dc892af 100644 --- a/homeassistant/components/isy994/translations/id.json +++ b/homeassistant/components/isy994/translations/id.json @@ -32,6 +32,23 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Perbarui semua otomasi atau skrip yang menggunakan layanan ini untuk menggunakan layanan `{alternate_service}` dengan ID entitas target `{alternate_target}`.", + "title": "Layanan {deprecated_service} akan dihapus" + }, + "deprecated_yaml": { + "description": "Proses konfigurasi Integrasi Universal Devices ISY/IoX lewat YAML dalam proses penghapusan.\n\nKonfigurasi YAML yang ada telah diimpor ke antarmuka secara otomatis.\n\nHapus konfigurasi YAML Integrasi `isy994` dari file configuration.yaml dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Integrasi ISY/IoX dalam proses penghapusan" + } + } + }, + "title": "Layanan {deprecated_service} akan dihapus" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/it.json b/homeassistant/components/isy994/translations/it.json index dae46ed8275..d003c332598 100644 --- a/homeassistant/components/isy994/translations/it.json +++ b/homeassistant/components/isy994/translations/it.json @@ -32,6 +32,23 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aggiorna tutte le automazioni o gli script che utilizzano questo servizio per utilizzare invece il servizio `{alternate_service}` con un ID entit\u00e0 di destinazione di `{alternate_target}`.", + "title": "Il servizio {deprecated_service} sar\u00e0 rimosso" + }, + "deprecated_yaml": { + "description": "La configurazione di Universal Devices ISY/IoX tramite YAML \u00e8 stata rimossa. \n\nLa configurazione YAML esistente \u00e8 stata importata automaticamente nell'interfaccia utente. \n\nRimuovi la configurazione YAML `isy994` dal tuo file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "La configurazione YAML di ISY/IoX \u00e8 in fase di rimozione" + } + } + }, + "title": "Il servizio {deprecated_service} sar\u00e0 rimosso" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/lt.json b/homeassistant/components/isy994/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/isy994/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/lv.json b/homeassistant/components/isy994/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/isy994/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/nl.json b/homeassistant/components/isy994/translations/nl.json index 261be2929fc..8fa4f6ff463 100644 --- a/homeassistant/components/isy994/translations/nl.json +++ b/homeassistant/components/isy994/translations/nl.json @@ -32,6 +32,17 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "deprecated_yaml": { + "title": "De ISY/IoX YAML configuratie wordt verwijderd" + } + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/no.json b/homeassistant/components/isy994/translations/no.json index 813053aa83a..16aea15fe1e 100644 --- a/homeassistant/components/isy994/translations/no.json +++ b/homeassistant/components/isy994/translations/no.json @@ -32,6 +32,23 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten for i stedet \u00e5 bruke ` {alternate_service} `-tjenesten med en m\u00e5lenhets-ID p\u00e5 ` {alternate_target} `.", + "title": "{deprecated_service} -tjenesten vil bli fjernet" + }, + "deprecated_yaml": { + "description": "Konfigurering av universelle enheter ISY/IoX ved hjelp av YAML blir fjernet. \n\n Din eksisterende YAML-konfigurasjon har blitt importert til brukergrensesnittet automatisk. \n\n Fjern `isy994` YAML-konfigurasjonen fra filen configuration.yaml og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "ISY/IoX YAML-konfigurasjonen blir fjernet" + } + } + }, + "title": "{deprecated_service} -tjenesten vil bli fjernet" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/pl.json b/homeassistant/components/isy994/translations/pl.json index a1ca157bd14..a60661df3ee 100644 --- a/homeassistant/components/isy994/translations/pl.json +++ b/homeassistant/components/isy994/translations/pl.json @@ -32,6 +32,23 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Zaktualizuj wszelkie automatyzacje lub skrypty korzystaj\u0105ce z tej us\u0142ugi, aby zamiast tego korzysta\u0142y z us\u0142ugi `{alternate_service}` z identyfikatorem encji docelowej `{alternate_target}`.", + "title": "Us\u0142uga {deprecated_service} zostanie usuni\u0119ta" + }, + "deprecated_yaml": { + "description": "Konfiguracja urz\u0105dze\u0144 uniwersalnych ISY/iox przy u\u017cyciu YAML zostanie usuni\u0119ta. \n\nTwoja istniej\u0105ca konfiguracja YAML zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika. \n\nUsu\u0144 konfiguracj\u0119 YAML 'isy995' z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla ISY/iox zostanie usuni\u0119ta" + } + } + }, + "title": "Us\u0142uga {deprecated_service} zostanie usuni\u0119ta" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/pt-BR.json b/homeassistant/components/isy994/translations/pt-BR.json index 7b1d8e796ba..9ccf16c9631 100644 --- a/homeassistant/components/isy994/translations/pt-BR.json +++ b/homeassistant/components/isy994/translations/pt-BR.json @@ -32,6 +32,23 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Atualize todas as automa\u00e7\u00f5es ou scripts que usam esse servi\u00e7o para usar o servi\u00e7o `{alternate_service}` com um ID de entidade de destino de `{alternate_target}`.", + "title": "O servi\u00e7o {deprecated_service} ser\u00e1 removido" + }, + "deprecated_yaml": { + "description": "Configurando Dispositivos Universais ISY/IoX usando YAML est\u00e1 sendo removido. \n\n Sua configura\u00e7\u00e3o YAML existente foi importada para a IU automaticamente. \n\n Remova a configura\u00e7\u00e3o YAML `isy994` do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML de ISY/IoX est\u00e1 sendo removida" + } + } + }, + "title": "O servi\u00e7o {deprecated_service} ser\u00e1 removido" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/ru.json b/homeassistant/components/isy994/translations/ru.json index ea7d5c6e36c..3382d9c6401 100644 --- a/homeassistant/components/isy994/translations/ru.json +++ b/homeassistant/components/isy994/translations/ru.json @@ -32,6 +32,23 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0412 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f\u0445 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u0430\u0445, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0445 \u044d\u0442\u0443 \u0441\u043b\u0443\u0436\u0431\u0443, \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0443\u0436\u0431\u0443 `{alternate_service}` \u0441 \u0446\u0435\u043b\u0435\u0432\u044b\u043c \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u043c `{alternate_target}`.", + "title": "\u0421\u043b\u0443\u0436\u0431\u0430 {deprecated_service} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + }, + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Universal Devices ISY/IoX \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430.\n\n\u0412\u0430\u0448\u0430 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u0435\u0451 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 ISY/IoX \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } + }, + "title": "\u0421\u043b\u0443\u0436\u0431\u0430 {deprecated_service} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/sk.json b/homeassistant/components/isy994/translations/sk.json index 6779170653a..764d0b6cdf8 100644 --- a/homeassistant/components/isy994/translations/sk.json +++ b/homeassistant/components/isy994/translations/sk.json @@ -32,6 +32,23 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualizujte v\u0161etky automatiz\u00e1cie alebo skripty, ktor\u00e9 pou\u017e\u00edvaj\u00fa t\u00fato slu\u017ebu, aby namiesto nej pou\u017e\u00edvali slu\u017ebu `{alternate_service}` s ID cie\u013eovej entity `{alternate_target}`.", + "title": "Slu\u017eba {deprecated_service} bude odstr\u00e1nen\u00e1" + }, + "deprecated_yaml": { + "description": "Konfigur\u00e1cia Universal Devices ISY/IoX pomocou YAML sa odstra\u0148uje. \n\n Va\u0161a existuj\u00faca konfigur\u00e1cia YAML bola importovan\u00e1 do pou\u017e\u00edvate\u013esk\u00e9ho rozhrania automaticky. \n\n Odstr\u00e1\u0148te konfigur\u00e1ciu YAML `isy994` zo s\u00faboru configuration.yaml a re\u0161tartujte Home Assistant, aby ste tento probl\u00e9m vyrie\u0161ili.", + "title": "Konfigur\u00e1cia ISY/IoX YAML sa odstra\u0148uje" + } + } + }, + "title": "Slu\u017eba {deprecated_service} bude odstr\u00e1nen\u00e1" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/tr.json b/homeassistant/components/isy994/translations/tr.json index 3999ee16163..8e836bdf88e 100644 --- a/homeassistant/components/isy994/translations/tr.json +++ b/homeassistant/components/isy994/translations/tr.json @@ -32,6 +32,23 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Bu hizmeti kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131, bunun yerine \" {alternate_service} {alternate_target} hizmetini kullanacak \u015fekilde g\u00fcncelleyin.", + "title": "{deprecated_service} hizmeti kald\u0131r\u0131lacak" + }, + "deprecated_yaml": { + "description": "YAML kullanarak Evrensel Cihazlar\u0131 Yap\u0131land\u0131rma ISY/IoX kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z, kullan\u0131c\u0131 aray\u00fcz\u00fcne otomatik olarak aktar\u0131ld\u0131. \n\n Bu sorunu \u00e7\u00f6zmek i\u00e7in \"isy994\" YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "ISY/IoX YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + } + }, + "title": "{deprecated_service} hizmeti kald\u0131r\u0131lacak" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/uk.json b/homeassistant/components/isy994/translations/uk.json index e50bc5cb26b..f2b24c42e0b 100644 --- a/homeassistant/components/isy994/translations/uk.json +++ b/homeassistant/components/isy994/translations/uk.json @@ -11,6 +11,12 @@ }, "flow_title": "Universal Devices ISY994 {name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, "user": { "data": { "host": "URL-\u0430\u0434\u0440\u0435\u0441\u0430", @@ -23,6 +29,22 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "title": "\u0421\u043b\u0443\u0436\u0431\u0443 {deprecated_service} \u0431\u0443\u0434\u0435 \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043e" + }, + "deprecated_yaml": { + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0443\u043d\u0456\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 ISY/IoX \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e YAML \u0432\u0438\u0434\u0430\u043b\u044f\u0454\u0442\u044c\u0441\u044f.\n\n\u0412\u0430\u0448\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f YAML \u0431\u0443\u043b\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0456\u043c\u043f\u043e\u0440\u0442\u043e\u0432\u0430\u043d\u0430 \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430.\n\n\u0412\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e YAML `isy994` \u0456\u0437 \u0444\u0430\u0439\u043b\u0443 configuration.yaml \u0456 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c Home Assistant, \u0449\u043e\u0431 \u0440\u043e\u0437\u0432'\u044f\u0437\u0430\u0442\u0438 \u0446\u044e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f YAML ISY/IoX \u0432\u0438\u0434\u0430\u043b\u044f\u0454\u0442\u044c\u0441\u044f" + } + } + }, + "title": "\u0421\u043b\u0443\u0436\u0431\u0443 {deprecated_service} \u0431\u0443\u0434\u0435 \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043e" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/translations/zh-Hant.json b/homeassistant/components/isy994/translations/zh-Hant.json index b70d9601ba2..857d44549a6 100644 --- a/homeassistant/components/isy994/translations/zh-Hant.json +++ b/homeassistant/components/isy994/translations/zh-Hant.json @@ -32,6 +32,23 @@ } } }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u4f7f\u7528\u6b64\u670d\u52d9\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\u3001\u4ee5\u53d6\u4ee3\u4f7f\u7528\u76ee\u6a19\u5be6\u9ad4 ID \u70ba `{alternate_target}` \u4e4b `{alternate_service}` \u670d\u52d9\u3002", + "title": "{deprecated_service} \u670d\u52d9\u5c07\u79fb\u9664" + }, + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684\u901a\u7528\u88dd\u7f6e ISY/IoX \u5373\u5c07\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684 YAML \u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\u3002\n\n\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 `isy994` YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "ISY/IoX YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + } + }, + "title": "{deprecated_service} \u670d\u52d9\u5c07\u79fb\u9664" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/isy994/util.py b/homeassistant/components/isy994/util.py index 196801c58ce..f4846b61aed 100644 --- a/homeassistant/components/isy994/util.py +++ b/homeassistant/components/isy994/util.py @@ -1,39 +1,34 @@ """ISY utils.""" from __future__ import annotations -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +import homeassistant.helpers.entity_registry as er -from .const import ( - DOMAIN, - ISY994_ISY, - ISY994_NODES, - ISY994_PROGRAMS, - ISY994_VARIABLES, - PLATFORMS, - PROGRAM_PLATFORMS, -) +from .const import _LOGGER, DOMAIN -def unique_ids_for_config_entry_id( - hass: HomeAssistant, config_entry_id: str -) -> set[str]: - """Find all the unique ids for a config entry id.""" - hass_isy_data = hass.data[DOMAIN][config_entry_id] - uuid = hass_isy_data[ISY994_ISY].configuration["uuid"] - current_unique_ids: set[str] = {uuid} +@callback +def _async_cleanup_registry_entries(hass: HomeAssistant, entry_id: str) -> None: + """Remove extra entities that are no longer part of the integration.""" + entity_registry = er.async_get(hass) + isy_data = hass.data[DOMAIN][entry_id] - for platform in PLATFORMS: - for node in hass_isy_data[ISY994_NODES][platform]: - if hasattr(node, "address"): - current_unique_ids.add(f"{uuid}_{node.address}") + existing_entries = er.async_entries_for_config_entry(entity_registry, entry_id) + entities = { + (entity.domain, entity.unique_id): entity.entity_id + for entity in existing_entries + } - for platform in PROGRAM_PLATFORMS: - for _, node, _ in hass_isy_data[ISY994_PROGRAMS][platform]: - if hasattr(node, "address"): - current_unique_ids.add(f"{uuid}_{node.address}") + extra_entities = set(entities.keys()).difference(isy_data.unique_ids) + if not extra_entities: + return - for node in hass_isy_data[ISY994_VARIABLES]: - if hasattr(node, "address"): - current_unique_ids.add(f"{uuid}_{node.address}") + for entity in extra_entities: + if entity_registry.async_is_registered(entities[entity]): + entity_registry.async_remove(entities[entity]) - return current_unique_ids + _LOGGER.debug( + ("Cleaning up ISY entities: removed %s extra entities for config entry %s"), + len(extra_entities), + entry_id, + ) diff --git a/homeassistant/components/jellyfin/browse_media.py b/homeassistant/components/jellyfin/browse_media.py index 0e63cb2f5d2..ac47bcf732f 100644 --- a/homeassistant/components/jellyfin/browse_media.py +++ b/homeassistant/components/jellyfin/browse_media.py @@ -11,7 +11,12 @@ from homeassistant.components.media_player.browse_media import BrowseMedia from homeassistant.core import HomeAssistant from .client_wrapper import get_artwork_url -from .const import CONTENT_TYPE_MAP, MEDIA_CLASS_MAP, MEDIA_TYPE_NONE +from .const import ( + CONTENT_TYPE_MAP, + MEDIA_CLASS_MAP, + MEDIA_TYPE_NONE, + SUPPORTED_COLLECTION_TYPES, +) CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS: dict[str, str] = { MediaType.MUSIC: MediaClass.MUSIC, @@ -22,8 +27,6 @@ CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS: dict[str, str] = { "library": MediaClass.DIRECTORY, } -JF_SUPPORTED_LIBRARY_TYPES = ["movies", "music", "tvshows"] - PLAYABLE_MEDIA_TYPES = [ MediaType.EPISODE, MediaType.MOVIE, @@ -65,7 +68,7 @@ async def build_root_response( children = [ await item_payload(hass, client, user_id, folder) for folder in folders["Items"] - if folder["CollectionType"] in JF_SUPPORTED_LIBRARY_TYPES + if folder["CollectionType"] in SUPPORTED_COLLECTION_TYPES ] return BrowseMedia( diff --git a/homeassistant/components/jellyfin/const.py b/homeassistant/components/jellyfin/const.py index 865e05a0081..fb8b4f15d82 100644 --- a/homeassistant/components/jellyfin/const.py +++ b/homeassistant/components/jellyfin/const.py @@ -11,6 +11,7 @@ CLIENT_VERSION: Final = hass_version COLLECTION_TYPE_MOVIES: Final = "movies" COLLECTION_TYPE_MUSIC: Final = "music" +COLLECTION_TYPE_TVSHOWS: Final = "tvshows" CONF_CLIENT_DEVICE_ID: Final = "client_device_id" @@ -27,8 +28,11 @@ ITEM_KEY_NAME: Final = "Name" ITEM_TYPE_ALBUM: Final = "MusicAlbum" ITEM_TYPE_ARTIST: Final = "MusicArtist" ITEM_TYPE_AUDIO: Final = "Audio" +ITEM_TYPE_EPISODE: Final = "Episode" ITEM_TYPE_LIBRARY: Final = "CollectionFolder" ITEM_TYPE_MOVIE: Final = "Movie" +ITEM_TYPE_SERIES: Final = "Series" +ITEM_TYPE_SEASON: Final = "Season" MAX_IMAGE_WIDTH: Final = 500 MAX_STREAMING_BITRATE: Final = "140000000" @@ -39,7 +43,14 @@ MEDIA_TYPE_AUDIO: Final = "Audio" MEDIA_TYPE_NONE: Final = "" MEDIA_TYPE_VIDEO: Final = "Video" -SUPPORTED_COLLECTION_TYPES: Final = [COLLECTION_TYPE_MUSIC, COLLECTION_TYPE_MOVIES] +SUPPORTED_COLLECTION_TYPES: Final = [ + COLLECTION_TYPE_MUSIC, + COLLECTION_TYPE_MOVIES, + COLLECTION_TYPE_TVSHOWS, +] + +PLAYABLE_ITEM_TYPES: Final = [ITEM_TYPE_AUDIO, ITEM_TYPE_EPISODE, ITEM_TYPE_MOVIE] + USER_APP_NAME: Final = "Home Assistant" USER_AGENT: Final = f"Home-Assistant/{CLIENT_VERSION}" diff --git a/homeassistant/components/jellyfin/coordinator.py b/homeassistant/components/jellyfin/coordinator.py index ac2cd78d257..b7563dcd862 100644 --- a/homeassistant/components/jellyfin/coordinator.py +++ b/homeassistant/components/jellyfin/coordinator.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from datetime import timedelta -from typing import Any, TypeVar, Union +from typing import Any, TypeVar from jellyfin_apiclient_python import JellyfinClient @@ -15,10 +15,7 @@ from .const import DOMAIN, LOGGER JellyfinDataT = TypeVar( "JellyfinDataT", - bound=Union[ - dict[str, dict[str, Any]], - dict[str, Any], - ], + bound=dict[str, dict[str, Any]] | dict[str, Any], ) diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index b81b5d81445..b2e7e1468fd 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant from .const import ( COLLECTION_TYPE_MOVIES, COLLECTION_TYPE_MUSIC, + COLLECTION_TYPE_TVSHOWS, DOMAIN, ITEM_KEY_COLLECTION_TYPE, ITEM_KEY_ID, @@ -32,13 +33,17 @@ from .const import ( ITEM_TYPE_ALBUM, ITEM_TYPE_ARTIST, ITEM_TYPE_AUDIO, + ITEM_TYPE_EPISODE, ITEM_TYPE_LIBRARY, ITEM_TYPE_MOVIE, + ITEM_TYPE_SEASON, + ITEM_TYPE_SERIES, MAX_IMAGE_WIDTH, MEDIA_SOURCE_KEY_PATH, MEDIA_TYPE_AUDIO, MEDIA_TYPE_NONE, MEDIA_TYPE_VIDEO, + PLAYABLE_ITEM_TYPES, SUPPORTED_COLLECTION_TYPES, ) from .models import JellyfinData @@ -100,6 +105,10 @@ class JellyfinSource(MediaSource): return await self._build_artist(media_item, True) if item_type == ITEM_TYPE_ALBUM: return await self._build_album(media_item, True) + if item_type == ITEM_TYPE_SERIES: + return await self._build_series(media_item, True) + if item_type == ITEM_TYPE_SEASON: + return await self._build_season(media_item, True) raise BrowseError(f"Unsupported item type {item_type}") @@ -146,6 +155,8 @@ class JellyfinSource(MediaSource): return await self._build_music_library(library, include_children) if collection_type == COLLECTION_TYPE_MOVIES: return await self._build_movie_library(library, include_children) + if collection_type == COLLECTION_TYPE_TVSHOWS: + return await self._build_tv_library(library, include_children) raise BrowseError(f"Unsupported collection type {collection_type}") @@ -326,6 +337,121 @@ class JellyfinSource(MediaSource): return result + async def _build_tv_library( + self, library: dict[str, Any], include_children: bool + ) -> BrowseMediaSource: + """Return a single tv show library as a browsable media source.""" + library_id = library[ITEM_KEY_ID] + library_name = library[ITEM_KEY_NAME] + + result = BrowseMediaSource( + domain=DOMAIN, + identifier=library_id, + media_class=MediaClass.DIRECTORY, + media_content_type=MEDIA_TYPE_NONE, + title=library_name, + can_play=False, + can_expand=True, + ) + + if include_children: + result.children_media_class = MediaClass.TV_SHOW + result.children = await self._build_tvshow(library_id) + + return result + + async def _build_tvshow(self, library_id: str) -> list[BrowseMediaSource]: + """Return all series in the tv library.""" + series = await self._get_children(library_id, ITEM_TYPE_SERIES) + series = sorted(series, key=lambda k: k[ITEM_KEY_NAME]) # type: ignore[no-any-return] + return [await self._build_series(serie, False) for serie in series] + + async def _build_series( + self, series: dict[str, Any], include_children: bool + ) -> BrowseMediaSource: + """Return a single series as a browsable media source.""" + series_id = series[ITEM_KEY_ID] + series_title = series[ITEM_KEY_NAME] + thumbnail_url = self._get_thumbnail_url(series) + + result = BrowseMediaSource( + domain=DOMAIN, + identifier=series_id, + media_class=MediaClass.TV_SHOW, + media_content_type=MEDIA_TYPE_NONE, + title=series_title, + can_play=False, + can_expand=True, + thumbnail=thumbnail_url, + ) + + if include_children: + result.children_media_class = MediaClass.SEASON + result.children = await self._build_seasons(series_id) + + return result + + async def _build_seasons(self, series_id: str) -> list[BrowseMediaSource]: + """Return all seasons in the series.""" + seasons = await self._get_children(series_id, ITEM_TYPE_SEASON) + seasons = sorted(seasons, key=lambda k: k[ITEM_KEY_NAME]) # type: ignore[no-any-return] + return [await self._build_season(season, False) for season in seasons] + + async def _build_season( + self, season: dict[str, Any], include_children: bool + ) -> BrowseMediaSource: + """Return a single series as a browsable media source.""" + season_id = season[ITEM_KEY_ID] + season_title = season[ITEM_KEY_NAME] + thumbnail_url = self._get_thumbnail_url(season) + + result = BrowseMediaSource( + domain=DOMAIN, + identifier=season_id, + media_class=MediaClass.TV_SHOW, + media_content_type=MEDIA_TYPE_NONE, + title=season_title, + can_play=False, + can_expand=True, + thumbnail=thumbnail_url, + ) + + if include_children: + result.children_media_class = MediaClass.EPISODE + result.children = await self._build_episodes(season_id) + + return result + + async def _build_episodes(self, season_id: str) -> list[BrowseMediaSource]: + """Return all episode in the season.""" + episodes = await self._get_children(season_id, ITEM_TYPE_EPISODE) + episodes = sorted(episodes, key=lambda k: k[ITEM_KEY_NAME]) # type: ignore[no-any-return] + return [ + self._build_episode(episode) + for episode in episodes + if _media_mime_type(episode) is not None + ] + + def _build_episode(self, episode: dict[str, Any]) -> BrowseMediaSource: + """Return a single episode as a browsable media source.""" + episode_id = episode[ITEM_KEY_ID] + episode_title = episode[ITEM_KEY_NAME] + mime_type = _media_mime_type(episode) + thumbnail_url = self._get_thumbnail_url(episode) + + result = BrowseMediaSource( + domain=DOMAIN, + identifier=episode_id, + media_class=MediaClass.EPISODE, + media_content_type=mime_type, + title=episode_title, + can_play=True, + can_expand=False, + thumbnail=thumbnail_url, + ) + + return result + async def _get_children( self, parent_id: str, item_type: str ) -> list[dict[str, Any]]: @@ -335,7 +461,7 @@ class JellyfinSource(MediaSource): "ParentId": parent_id, "IncludeItemTypes": item_type, } - if item_type in {ITEM_TYPE_AUDIO, ITEM_TYPE_MOVIE}: + if item_type in PLAYABLE_ITEM_TYPES: params["Fields"] = ITEM_KEY_MEDIA_SOURCES result = await self.hass.async_add_executor_job(self.api.user_items, "", params) diff --git a/homeassistant/components/jellyfin/strings.json b/homeassistant/components/jellyfin/strings.json index 2c832f4003b..8d74d416a94 100644 --- a/homeassistant/components/jellyfin/strings.json +++ b/homeassistant/components/jellyfin/strings.json @@ -15,6 +15,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } } diff --git a/homeassistant/components/jellyfin/translations/bg.json b/homeassistant/components/jellyfin/translations/bg.json index e99b99fdb61..5c127c57e9e 100644 --- a/homeassistant/components/jellyfin/translations/bg.json +++ b/homeassistant/components/jellyfin/translations/bg.json @@ -1,6 +1,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", "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, "error": { diff --git a/homeassistant/components/jellyfin/translations/ca.json b/homeassistant/components/jellyfin/translations/ca.json index feb588e0fd9..a664ed84817 100644 --- a/homeassistant/components/jellyfin/translations/ca.json +++ b/homeassistant/components/jellyfin/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El compte ja est\u00e0 configurat", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "error": { diff --git a/homeassistant/components/jellyfin/translations/de.json b/homeassistant/components/jellyfin/translations/de.json index c94ba7e9662..badc2c21529 100644 --- a/homeassistant/components/jellyfin/translations/de.json +++ b/homeassistant/components/jellyfin/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Konto wurde bereits konfiguriert", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { diff --git a/homeassistant/components/jellyfin/translations/en.json b/homeassistant/components/jellyfin/translations/en.json index fd85dc7acb6..359f158880c 100644 --- a/homeassistant/components/jellyfin/translations/en.json +++ b/homeassistant/components/jellyfin/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Account is already configured", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { diff --git a/homeassistant/components/jellyfin/translations/et.json b/homeassistant/components/jellyfin/translations/et.json index 65665ff244f..a6dc674e33f 100644 --- a/homeassistant/components/jellyfin/translations/et.json +++ b/homeassistant/components/jellyfin/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Kasutaja on juba seadistatud", "single_instance_allowed": "Juba seadistatud, lubatud on ainult \u00fcks seadistus." }, "error": { diff --git a/homeassistant/components/jellyfin/translations/no.json b/homeassistant/components/jellyfin/translations/no.json index c1351b9a97f..a3ee28b5dba 100644 --- a/homeassistant/components/jellyfin/translations/no.json +++ b/homeassistant/components/jellyfin/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Kontoen er allerede konfigurert", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { diff --git a/homeassistant/components/jellyfin/translations/ru.json b/homeassistant/components/jellyfin/translations/ru.json index b94d8def5e3..092c3ac8256 100644 --- a/homeassistant/components/jellyfin/translations/ru.json +++ b/homeassistant/components/jellyfin/translations/ru.json @@ -1,6 +1,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.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "error": { diff --git a/homeassistant/components/jellyfin/translations/uk.json b/homeassistant/components/jellyfin/translations/uk.json new file mode 100644 index 00000000000..2aed6be91ba --- /dev/null +++ b/homeassistant/components/jellyfin/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/jellyfin/translations/zh-Hant.json b/homeassistant/components/jellyfin/translations/zh-Hant.json index 886d6e3676e..22dd1b7a320 100644 --- a/homeassistant/components/jellyfin/translations/zh-Hant.json +++ b/homeassistant/components/jellyfin/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { diff --git a/homeassistant/components/justnimbus/translations/lv.json b/homeassistant/components/justnimbus/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/justnimbus/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/justnimbus/translations/nl.json b/homeassistant/components/justnimbus/translations/nl.json index 87e2082c011..849a75f254f 100644 --- a/homeassistant/components/justnimbus/translations/nl.json +++ b/homeassistant/components/justnimbus/translations/nl.json @@ -7,6 +7,13 @@ "cannot_connect": "Kan geen verbinding maken", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "client_id": "Client-ID" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/lv.json b/homeassistant/components/kaleidescape/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kaleidescape/translations/uk.json b/homeassistant/components/kaleidescape/translations/uk.json new file mode 100644 index 00000000000..87adbbf71cf --- /dev/null +++ b/homeassistant/components/kaleidescape/translations/uk.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "unsupported": "\u041d\u0435\u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + }, + "error": { + "unsupported": "\u041d\u0435\u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index be1ffd1f0b2..acb92dffe59 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -3,7 +3,7 @@ "name": "Keenetic NDMS2 Router", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2", - "requirements": ["ndms2_client==0.1.1"], + "requirements": ["ndms2_client==0.1.2"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", diff --git a/homeassistant/components/keenetic_ndms2/translations/sk.json b/homeassistant/components/keenetic_ndms2/translations/sk.json index eb8cc33bc5b..d9dabe0e3f8 100644 --- a/homeassistant/components/keenetic_ndms2/translations/sk.json +++ b/homeassistant/components/keenetic_ndms2/translations/sk.json @@ -30,7 +30,7 @@ "include_associated": "Pou\u017ei\u0165 \u00fadaje o pridru\u017een\u00ed WiFi AP (ignorovan\u00e9, ak sa pou\u017e\u00edvaj\u00fa \u00fadaje hotspotu)", "interfaces": "Vyberte rozhrania na skenovanie", "scan_interval": "Interval skenovania", - "try_hotspot": "Pou\u017ei\u0165 \u00fadaje \u201eip hotspot\u201c (najpresnej\u0161ie)" + "try_hotspot": "Pou\u017ei\u0165 \u00fadaje `ip hotspot` (najpresnej\u0161ie)" } } } diff --git a/homeassistant/components/keenetic_ndms2/translations/uk.json b/homeassistant/components/keenetic_ndms2/translations/uk.json new file mode 100644 index 00000000000..2aed6be91ba --- /dev/null +++ b/homeassistant/components/keenetic_ndms2/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/manifest.json b/homeassistant/components/kegtron/manifest.json index c3205736226..d64b34e9e60 100644 --- a/homeassistant/components/kegtron/manifest.json +++ b/homeassistant/components/kegtron/manifest.json @@ -10,7 +10,7 @@ } ], "requirements": ["kegtron-ble==0.4.0"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@Ernst79"], "iot_class": "local_push" } diff --git a/homeassistant/components/kegtron/sensor.py b/homeassistant/components/kegtron/sensor.py index c80788d2503..5d9895248d3 100644 --- a/homeassistant/components/kegtron/sensor.py +++ b/homeassistant/components/kegtron/sensor.py @@ -1,8 +1,6 @@ """Support for Kegtron sensors.""" from __future__ import annotations -from typing import Optional, Union - from kegtron_ble import ( SensorDeviceClass as KegtronSensorDeviceClass, SensorUpdate, @@ -126,9 +124,7 @@ async def async_setup_entry( class KegtronBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a Kegtron sensor.""" diff --git a/homeassistant/components/kegtron/translations/lv.json b/homeassistant/components/kegtron/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/kegtron/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kegtron/translations/tr.json b/homeassistant/components/kegtron/translations/tr.json index f0ddbc274c9..36347c44f7f 100644 --- a/homeassistant/components/kegtron/translations/tr.json +++ b/homeassistant/components/kegtron/translations/tr.json @@ -9,13 +9,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/kegtron/translations/uk.json b/homeassistant/components/kegtron/translations/uk.json new file mode 100644 index 00000000000..e58b49d4c9e --- /dev/null +++ b/homeassistant/components/kegtron/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/entity.py b/homeassistant/components/keymitt_ble/entity.py index dcda4a94027..31315e59efb 100644 --- a/homeassistant/components/keymitt_ble/entity.py +++ b/homeassistant/components/keymitt_ble/entity.py @@ -1,7 +1,7 @@ """MicroBot class.""" from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, @@ -10,16 +10,12 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo from .const import MANUFACTURER - -if TYPE_CHECKING: - from . import MicroBotDataUpdateCoordinator +from .coordinator import MicroBotDataUpdateCoordinator -class MicroBotEntity(PassiveBluetoothCoordinatorEntity): +class MicroBotEntity(PassiveBluetoothCoordinatorEntity[MicroBotDataUpdateCoordinator]): """Generic entity for all MicroBots.""" - coordinator: MicroBotDataUpdateCoordinator - def __init__(self, coordinator, config_entry): """Initialise the entity.""" super().__init__(coordinator) diff --git a/homeassistant/components/keymitt_ble/manifest.json b/homeassistant/components/keymitt_ble/manifest.json index 2a21074bb12..b2f7d264311 100644 --- a/homeassistant/components/keymitt_ble/manifest.json +++ b/homeassistant/components/keymitt_ble/manifest.json @@ -14,6 +14,6 @@ "codeowners": ["@spycle"], "requirements": ["PyMicroBot==0.0.8"], "iot_class": "assumed_state", - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "loggers": ["keymitt_ble"] } diff --git a/homeassistant/components/keymitt_ble/switch.py b/homeassistant/components/keymitt_ble/switch.py index 92decea53ca..099ad1f228a 100644 --- a/homeassistant/components/keymitt_ble/switch.py +++ b/homeassistant/components/keymitt_ble/switch.py @@ -1,7 +1,7 @@ """Switch platform for MicroBot.""" from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import Any import voluptuous as vol @@ -11,11 +11,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from .const import DOMAIN +from .coordinator import MicroBotDataUpdateCoordinator from .entity import MicroBotEntity -if TYPE_CHECKING: - from . import MicroBotDataUpdateCoordinator - CALIBRATE = "calibrate" CALIBRATE_SCHEMA = { vol.Required("depth"): cv.positive_int, diff --git a/homeassistant/components/keymitt_ble/translations/el.json b/homeassistant/components/keymitt_ble/translations/el.json index bb6521f4b36..4701b284c71 100644 --- a/homeassistant/components/keymitt_ble/translations/el.json +++ b/homeassistant/components/keymitt_ble/translations/el.json @@ -16,7 +16,7 @@ "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2", "name": "\u039f\u03bd\u03bf\u03bc\u03b1" }, - "title": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 MicroBot" + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae MicroBot" }, "link": { "description": "\u03a0\u03b1\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03af \u03c3\u03c4\u03bf MicroBot Push \u03cc\u03c4\u03b1\u03bd \u03b7 \u03bb\u03c5\u03c7\u03bd\u03af\u03b1 LED \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ac \u03c1\u03bf\u03b6 \u03ae \u03c0\u03c1\u03ac\u03c3\u03b9\u03bd\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf Home Assistant.", diff --git a/homeassistant/components/keymitt_ble/translations/lv.json b/homeassistant/components/keymitt_ble/translations/lv.json new file mode 100644 index 00000000000..9eea6cd040d --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured_device": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/keymitt_ble/translations/uk.json b/homeassistant/components/keymitt_ble/translations/uk.json new file mode 100644 index 00000000000..9e49fc4d7a7 --- /dev/null +++ b/homeassistant/components/keymitt_ble/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "name": "\u041d\u0430\u0437\u0432\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kitchen_sink/__init__.py b/homeassistant/components/kitchen_sink/__init__.py new file mode 100644 index 00000000000..3b7b96e90b6 --- /dev/null +++ b/homeassistant/components/kitchen_sink/__init__.py @@ -0,0 +1,281 @@ +"""The Kitchen Sink integration contains demonstrations of various odds and ends. + +This sets up a demo environment of features which are obscure or which represent +incorrect behavior, and are thus not wanted in the demo integration. +""" +from __future__ import annotations + +import datetime +from random import random + +from homeassistant.components.recorder import DOMAIN as RECORDER_DOMAIN, get_instance +from homeassistant.components.recorder.models import StatisticData, StatisticMetaData +from homeassistant.components.recorder.statistics import ( + async_add_external_statistics, + async_import_statistics, + get_last_statistics, +) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import Platform, UnitOfEnergy, UnitOfTemperature, UnitOfVolume +from homeassistant.core import HomeAssistant +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.typing import ConfigType +import homeassistant.util.dt as dt_util + +DOMAIN = "kitchen_sink" + + +COMPONENTS_WITH_DEMO_PLATFORM = [Platform.SENSOR, Platform.LOCK] + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the demo environment.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data={} + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set the config entry up.""" + # Set up demo platforms with config entry + await hass.config_entries.async_forward_entry_setups( + config_entry, COMPONENTS_WITH_DEMO_PLATFORM + ) + + # Create issues + _create_issues(hass) + + # Insert some external statistics + if "recorder" in hass.config.components: + await _insert_statistics(hass) + + return True + + +def _create_issues(hass): + """Create some issue registry issues.""" + async_create_issue( + hass, + DOMAIN, + "transmogrifier_deprecated", + breaks_in_ha_version="2023.1.1", + is_fixable=False, + learn_more_url="https://en.wiktionary.org/wiki/transmogrifier", + severity=IssueSeverity.WARNING, + translation_key="transmogrifier_deprecated", + ) + + async_create_issue( + hass, + DOMAIN, + "out_of_blinker_fluid", + breaks_in_ha_version="2023.1.1", + is_fixable=True, + learn_more_url="https://www.youtube.com/watch?v=b9rntRxLlbU", + severity=IssueSeverity.CRITICAL, + translation_key="out_of_blinker_fluid", + ) + + async_create_issue( + hass, + DOMAIN, + "unfixable_problem", + is_fixable=False, + learn_more_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ", + severity=IssueSeverity.WARNING, + translation_key="unfixable_problem", + ) + + async_create_issue( + hass, + DOMAIN, + "bad_psu", + is_fixable=True, + learn_more_url="https://www.youtube.com/watch?v=b9rntRxLlbU", + severity=IssueSeverity.CRITICAL, + translation_key="bad_psu", + ) + + async_create_issue( + hass, + DOMAIN, + "cold_tea", + is_fixable=True, + severity=IssueSeverity.WARNING, + translation_key="cold_tea", + ) + + +def _generate_mean_statistics( + start: datetime.datetime, end: datetime.datetime, init_value: float, max_diff: float +) -> list[StatisticData]: + statistics: list[StatisticData] = [] + mean = init_value + now = start + while now < end: + mean = mean + random() * max_diff - max_diff / 2 + statistics.append( + { + "start": now, + "mean": mean, + "min": mean - random() * max_diff, + "max": mean + random() * max_diff, + } + ) + now = now + datetime.timedelta(hours=1) + + return statistics + + +async def _insert_sum_statistics( + hass: HomeAssistant, + metadata: StatisticMetaData, + start: datetime.datetime, + end: datetime.datetime, + max_diff: float, +) -> None: + statistics: list[StatisticData] = [] + now = start + sum_ = 0.0 + statistic_id = metadata["statistic_id"] + + last_stats = await get_instance(hass).async_add_executor_job( + get_last_statistics, hass, 1, statistic_id, False, {"sum"} + ) + if statistic_id in last_stats: + sum_ = last_stats[statistic_id][0]["sum"] or 0 + while now < end: + sum_ = sum_ + random() * max_diff + statistics.append( + { + "start": now, + "sum": sum_, + } + ) + now = now + datetime.timedelta(hours=1) + + async_add_external_statistics(hass, metadata, statistics) + + +async def _insert_statistics(hass: HomeAssistant) -> None: + """Insert some fake statistics.""" + now = dt_util.now() + yesterday = now - datetime.timedelta(days=1) + yesterday_midnight = yesterday.replace(hour=0, minute=0, second=0, microsecond=0) + today_midnight = yesterday_midnight + datetime.timedelta(days=1) + + # Fake yesterday's temperatures + metadata: StatisticMetaData = { + "source": DOMAIN, + "name": "Outdoor temperature", + "statistic_id": f"{DOMAIN}:temperature_outdoor", + "unit_of_measurement": UnitOfTemperature.CELSIUS, + "has_mean": True, + "has_sum": False, + } + statistics = _generate_mean_statistics(yesterday_midnight, today_midnight, 15, 1) + async_add_external_statistics(hass, metadata, statistics) + + # Add external energy consumption in kWh, ~ 12 kWh / day + # This should be possible to pick for the energy dashboard + metadata = { + "source": DOMAIN, + "name": "Energy consumption 1", + "statistic_id": f"{DOMAIN}:energy_consumption_kwh", + "unit_of_measurement": UnitOfEnergy.KILO_WATT_HOUR, + "has_mean": False, + "has_sum": True, + } + await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 1) + + # Add external energy consumption in MWh, ~ 12 kWh / day + # This should not be possible to pick for the energy dashboard + metadata = { + "source": DOMAIN, + "name": "Energy consumption 2", + "statistic_id": f"{DOMAIN}:energy_consumption_mwh", + "unit_of_measurement": UnitOfEnergy.MEGA_WATT_HOUR, + "has_mean": False, + "has_sum": True, + } + await _insert_sum_statistics( + hass, metadata, yesterday_midnight, today_midnight, 0.001 + ) + + # Add external gas consumption in m³, ~6 m3/day + # This should be possible to pick for the energy dashboard + metadata = { + "source": DOMAIN, + "name": "Gas consumption 1", + "statistic_id": f"{DOMAIN}:gas_consumption_m3", + "unit_of_measurement": UnitOfVolume.CUBIC_METERS, + "has_mean": False, + "has_sum": True, + } + await _insert_sum_statistics( + hass, metadata, yesterday_midnight, today_midnight, 0.5 + ) + + # Add external gas consumption in ft³, ~180 ft3/day + # This should not be possible to pick for the energy dashboard + metadata = { + "source": DOMAIN, + "name": "Gas consumption 2", + "statistic_id": f"{DOMAIN}:gas_consumption_ft3", + "unit_of_measurement": UnitOfVolume.CUBIC_FEET, + "has_mean": False, + "has_sum": True, + } + await _insert_sum_statistics(hass, metadata, yesterday_midnight, today_midnight, 15) + + # Add some statistics which will raise an issue + # Used to raise an issue where the unit has changed to a non volume unit + metadata = { + "source": RECORDER_DOMAIN, + "name": None, + "statistic_id": "sensor.statistics_issue_1", + "unit_of_measurement": UnitOfVolume.CUBIC_METERS, + "has_mean": True, + "has_sum": False, + } + statistics = _generate_mean_statistics(yesterday_midnight, today_midnight, 15, 1) + async_import_statistics(hass, metadata, statistics) + + # Used to raise an issue where the unit has changed to a different unit + metadata = { + "source": RECORDER_DOMAIN, + "name": None, + "statistic_id": "sensor.statistics_issue_2", + "unit_of_measurement": "cats", + "has_mean": True, + "has_sum": False, + } + statistics = _generate_mean_statistics(yesterday_midnight, today_midnight, 15, 1) + async_import_statistics(hass, metadata, statistics) + + # Used to raise an issue where state class is not compatible with statistics + metadata = { + "source": RECORDER_DOMAIN, + "name": None, + "statistic_id": "sensor.statistics_issue_3", + "unit_of_measurement": UnitOfVolume.CUBIC_METERS, + "has_mean": True, + "has_sum": False, + } + statistics = _generate_mean_statistics(yesterday_midnight, today_midnight, 15, 1) + async_import_statistics(hass, metadata, statistics) + + # Used to raise an issue where the sensor is not in the state machine + metadata = { + "source": RECORDER_DOMAIN, + "name": None, + "statistic_id": "sensor.statistics_issue_4", + "unit_of_measurement": UnitOfVolume.CUBIC_METERS, + "has_mean": True, + "has_sum": False, + } + statistics = _generate_mean_statistics(yesterday_midnight, today_midnight, 15, 1) + async_import_statistics(hass, metadata, statistics) diff --git a/homeassistant/components/kitchen_sink/config_flow.py b/homeassistant/components/kitchen_sink/config_flow.py new file mode 100644 index 00000000000..ded2b84e31c --- /dev/null +++ b/homeassistant/components/kitchen_sink/config_flow.py @@ -0,0 +1,22 @@ +"""Config flow to configure the Kitchen Sink component.""" +from __future__ import annotations + +from typing import Any + +from homeassistant import config_entries +from homeassistant.data_entry_flow import FlowResult + +from . import DOMAIN + + +class KitchenSinkConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Kitchen Sink configuration flow.""" + + VERSION = 1 + + async def async_step_import(self, import_info: dict[str, Any]) -> FlowResult: + """Set the config entry up from yaml.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + return self.async_create_entry(title="Kitchen Sink", data=import_info) diff --git a/homeassistant/components/kitchen_sink/lock.py b/homeassistant/components/kitchen_sink/lock.py new file mode 100644 index 00000000000..343190acb63 --- /dev/null +++ b/homeassistant/components/kitchen_sink/lock.py @@ -0,0 +1,92 @@ +"""Demo platform that has a couple of fake locks.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.lock import LockEntity, LockEntityFeature +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED, STATE_UNLOCKING +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Demo sensors.""" + async_add_entities( + [ + DemoLock( + "kitchen_sink_lock_001", + "Openable lock", + STATE_LOCKED, + LockEntityFeature.OPEN, + ), + DemoLock( + "kitchen_sink_lock_002", + "Another openable lock", + STATE_UNLOCKED, + LockEntityFeature.OPEN, + ), + DemoLock( + "kitchen_sink_lock_003", + "Basic lock", + STATE_LOCKED, + ), + DemoLock( + "kitchen_sink_lock_004", + "Another basic lock", + STATE_UNLOCKED, + ), + ] + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Everything but the Kitchen Sink config entry.""" + await async_setup_platform(hass, {}, async_add_entities) + + +class DemoLock(LockEntity): + """Representation of a Demo lock.""" + + def __init__( + self, + unique_id: str, + name: str, + state: str, + features: LockEntityFeature = LockEntityFeature(0), + ) -> None: + """Initialize the sensor.""" + self._attr_name = name + self._attr_unique_id = unique_id + self._attr_supported_features = features + self._state = state + + @property + def is_locked(self) -> bool: + """Return true if lock is locked.""" + return self._state == STATE_LOCKED + + async def async_lock(self, **kwargs: Any) -> None: + """Lock the device.""" + self._state = STATE_LOCKED + self.async_write_ha_state() + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock the device.""" + self._state = STATE_UNLOCKING + self.async_write_ha_state() + + async def async_open(self, **kwargs: Any) -> None: + """Open the door latch.""" + self._state = STATE_UNLOCKED + self.async_write_ha_state() diff --git a/homeassistant/components/kitchen_sink/manifest.json b/homeassistant/components/kitchen_sink/manifest.json new file mode 100644 index 00000000000..04a4b550b58 --- /dev/null +++ b/homeassistant/components/kitchen_sink/manifest.json @@ -0,0 +1,9 @@ +{ + "after_dependencies": ["recorder"], + "codeowners": ["@home-assistant/core"], + "documentation": "https://www.home-assistant.io/integrations/kitchen_sink", + "domain": "kitchen_sink", + "iot_class": "calculated", + "name": "Everything but the Kitchen Sink", + "quality_scale": "internal" +} diff --git a/homeassistant/components/demo/repairs.py b/homeassistant/components/kitchen_sink/repairs.py similarity index 100% rename from homeassistant/components/demo/repairs.py rename to homeassistant/components/kitchen_sink/repairs.py diff --git a/homeassistant/components/kitchen_sink/sensor.py b/homeassistant/components/kitchen_sink/sensor.py new file mode 100644 index 00000000000..6692f53810b --- /dev/null +++ b/homeassistant/components/kitchen_sink/sensor.py @@ -0,0 +1,91 @@ +"""Demo platform that has a couple of fake sensors.""" +from __future__ import annotations + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_BATTERY_LEVEL, UnitOfPower +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from . import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Everything but the Kitchen Sink config entry.""" + async_add_entities( + [ + DemoSensor( + "statistics_issue_1", + "Statistics issue 1", + 100, + None, + SensorStateClass.MEASUREMENT, + UnitOfPower.WATT, # Not a volume unit + None, + ), + DemoSensor( + "statistics_issue_2", + "Statistics issue 2", + 100, + None, + SensorStateClass.MEASUREMENT, + "dogs", # Can't be converted to cats + None, + ), + DemoSensor( + "statistics_issue_3", + "Statistics issue 3", + 100, + None, + None, # Wrong state class + UnitOfPower.WATT, + None, + ), + ] + ) + + +class DemoSensor(SensorEntity): + """Representation of a Demo sensor.""" + + _attr_should_poll = False + + def __init__( + self, + unique_id: str, + name: str, + state: StateType, + device_class: SensorDeviceClass | None, + state_class: SensorStateClass | None, + unit_of_measurement: str | None, + battery: StateType, + options: list[str] | None = None, + translation_key: str | None = None, + ) -> None: + """Initialize the sensor.""" + self._attr_device_class = device_class + self._attr_name = name + self._attr_native_unit_of_measurement = unit_of_measurement + self._attr_native_value = state + self._attr_state_class = state_class + self._attr_unique_id = unique_id + self._attr_options = options + self._attr_translation_key = translation_key + + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + name=name, + ) + + if battery: + self._attr_extra_state_attributes = {ATTR_BATTERY_LEVEL: battery} diff --git a/homeassistant/components/kmtronic/translations/lv.json b/homeassistant/components/kmtronic/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/kmtronic/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kmtronic/translations/uk.json b/homeassistant/components/kmtronic/translations/uk.json new file mode 100644 index 00000000000..2aed6be91ba --- /dev/null +++ b/homeassistant/components/kmtronic/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 0159f6f1cac..ac606856e3e 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -580,8 +580,8 @@ class KNXModule: raise HomeAssistantError( f"Could not find exposure for '{group_address}' to remove." ) from err - else: - removed_exposure.shutdown() + + removed_exposure.shutdown() return if group_address in self.service_exposures: diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index b03a59b2d4e..7465c394dd1 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -153,7 +153,7 @@ class KNXCommonFlow(ABC, FlowHandler): # keep a reference to the generator to scan in background until user selects a connection type self._async_scan_gen = self._gatewayscanner.async_scan() try: - await self._async_scan_gen.__anext__() + await anext(self._async_scan_gen) except StopAsyncIteration: pass # scan finished, no interfaces discovered else: diff --git a/homeassistant/components/knx/diagnostics.py b/homeassistant/components/knx/diagnostics.py index 3dd14aef653..60a41c9a408 100644 --- a/homeassistant/components/knx/diagnostics.py +++ b/homeassistant/components/knx/diagnostics.py @@ -29,7 +29,7 @@ TO_REDACT = { async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" diag: dict[str, Any] = {} knx_module = hass.data[DOMAIN] diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 60feb3c7419..7c1fa368b83 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -3,7 +3,7 @@ "name": "KNX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==2.2.0"], + "requirements": ["xknx==2.3.0"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "platinum", "iot_class": "local_push", diff --git a/homeassistant/components/knx/translations/bg.json b/homeassistant/components/knx/translations/bg.json index eadda227e1d..7d7dd5321cd 100644 --- a/homeassistant/components/knx/translations/bg.json +++ b/homeassistant/components/knx/translations/bg.json @@ -39,6 +39,9 @@ "user_id": "ID \u043d\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f", "user_password": "\u041f\u0430\u0440\u043e\u043b\u0430 \u043d\u0430 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u044f" } + }, + "tunnel": { + "title": "\u0422\u0443\u043d\u0435\u043b" } } }, @@ -49,6 +52,9 @@ "unsupported_tunnel_type": "\u0418\u0437\u0431\u0440\u0430\u043d\u0438\u044f\u0442 \u0442\u0438\u043f \u0442\u0443\u043d\u0435\u043b\u0438\u0440\u0430\u043d\u0435 \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430 \u043e\u0442 \u0448\u043b\u044e\u0437\u0430." }, "step": { + "communication_settings": { + "title": "\u041a\u043e\u043c\u0443\u043d\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + }, "manual_tunnel": { "data": { "host": "\u0425\u043e\u0441\u0442", @@ -74,7 +80,8 @@ } }, "tunnel": { - "description": "\u041c\u043e\u043b\u044f, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0448\u043b\u044e\u0437 \u043e\u0442 \u0441\u043f\u0438\u0441\u044a\u043a\u0430." + "description": "\u041c\u043e\u043b\u044f, \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0448\u043b\u044e\u0437 \u043e\u0442 \u0441\u043f\u0438\u0441\u044a\u043a\u0430.", + "title": "\u0422\u0443\u043d\u0435\u043b" } } } diff --git a/homeassistant/components/knx/translations/ca.json b/homeassistant/components/knx/translations/ca.json index ff039591f21..6c76a219bb9 100644 --- a/homeassistant/components/knx/translations/ca.json +++ b/homeassistant/components/knx/translations/ca.json @@ -6,11 +6,9 @@ }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "file_not_found": "No s'ha trobat el fitxer `.knxkeys` especificat a la ruta config/.storage/knx/", "invalid_backbone_key": "Clau troncal inv\u00e0lida. S'esperen 32 nombres hexadecimals.", "invalid_individual_address": "El valor no coincideix amb el patr\u00f3 d'adre\u00e7a KNX individual.\n'area.line.device'", "invalid_ip_address": "Adre\u00e7a IPv4 inv\u00e0lida.", - "invalid_signature": "La contrasenya per desxifrar el fitxer `.knxkeys` \u00e9s incorrecta.", "keyfile_invalid_signature": "La contrasenya per desxifrar el fitxer `.knxkeys` \u00e9s incorrecta.", "keyfile_no_backbone_key": "El fitxer `.knxkeys` no cont\u00e9 una clau de 'backbone' per a l'encaminament segur.", "keyfile_no_tunnel_for_host": "El fitxer `.knxkeys` no cont\u00e9 credencials per a l'amfitri\u00f3 `{host}`.", @@ -27,6 +25,13 @@ "description": "Introdueix el tipus de connexi\u00f3 a utilitzar per a la connexi\u00f3 KNX.\n AUTOM\u00c0TICA: la integraci\u00f3 s'encarrega de la connectivitat al bus KNX realitzant una exploraci\u00f3 de la passarel\u00b7la.\n T\u00daNEL: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant un t\u00fanel.\n ENCAMINAMENT: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant l'encaminament.", "title": "Connexi\u00f3 KNX" }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Autom\u00e0tic` utilitzar\u00e0 el primer punt final ('endpoint') de t\u00fanel lliure." + }, + "description": "Seleccioneu el t\u00fanel utilitzat per a la connexi\u00f3.", + "title": "Punt final ('endpoint') del t\u00fanel" + }, "manual_tunnel": { "data": { "host": "Amfitri\u00f3", @@ -118,11 +123,9 @@ "options": { "error": { "cannot_connect": "Ha fallat la connexi\u00f3", - "file_not_found": "No s'ha trobat el fitxer `.knxkeys` especificat a la ruta config/.storage/knx/", "invalid_backbone_key": "Clau troncal inv\u00e0lida. S'esperen 32 nombres hexadecimals.", "invalid_individual_address": "El valor no coincideix amb el patr\u00f3 d'adre\u00e7a KNX individual.\n'area.line.device'", "invalid_ip_address": "Adre\u00e7a IPv4 inv\u00e0lida.", - "invalid_signature": "La contrasenya per desxifrar el fitxer `.knxkeys` \u00e9s incorrecta.", "keyfile_invalid_signature": "La contrasenya per desxifrar el fitxer `.knxkeys` \u00e9s incorrecta.", "keyfile_no_backbone_key": "El fitxer `.knxkeys` no cont\u00e9 una clau de 'backbone' per a l'encaminament segur.", "keyfile_no_tunnel_for_host": "El fitxer `.knxkeys` no cont\u00e9 credencials per a l'amfitri\u00f3 `{host}`.", @@ -150,6 +153,13 @@ "description": "Introdueix el tipus de connexi\u00f3 a utilitzar per a la connexi\u00f3 KNX.\n AUTOM\u00c0TICA: la integraci\u00f3 s'encarrega de la connectivitat al bus KNX realitzant una exploraci\u00f3 de la passarel\u00b7la.\n T\u00daNEL: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant un t\u00fanel.\n ENCAMINAMENT: la integraci\u00f3 es connectar\u00e0 al bus KNX mitjan\u00e7ant l'encaminament.", "title": "Connexi\u00f3 KNX" }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Autom\u00e0tic` utilitzar\u00e0 el primer punt final ('endpoint') de t\u00fanel lliure." + }, + "description": "Seleccioneu el t\u00fanel utilitzat per a la connexi\u00f3.", + "title": "Punt final ('endpoint') del t\u00fanel" + }, "manual_tunnel": { "data": { "host": "Amfitri\u00f3", diff --git a/homeassistant/components/knx/translations/de.json b/homeassistant/components/knx/translations/de.json index f2a63223cbe..ae8e15142bf 100644 --- a/homeassistant/components/knx/translations/de.json +++ b/homeassistant/components/knx/translations/de.json @@ -6,11 +6,13 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "file_not_found": "Die angegebene `.knxkeys` Datei wurde im Pfad config/.storage/knx/ nicht gefunden.", "invalid_backbone_key": "Ung\u00fcltiger Backbone-Schl\u00fcssel. 32 Hexadezimalzahlen erwartet.", "invalid_individual_address": "Wert ist keine g\u00fcltige physikalische Adresse. 'Bereich.Linie.Teilnehmer'", "invalid_ip_address": "Ung\u00fcltige IPv4 Adresse.", - "invalid_signature": "Das Passwort zum Entschl\u00fcsseln der `.knxkeys` Datei ist ung\u00fcltig.", + "keyfile_invalid_signature": "Das Passwort f\u00fcr die `.knxkeys` Datei ist falsch.", + "keyfile_no_backbone_key": "Die `.knxkeys` Datei enth\u00e4lt keinen Backbone-Schl\u00fcssel f\u00fcr Secure Routing.", + "keyfile_no_tunnel_for_host": "Die `.knxkeys` Datei enth\u00e4lt keine Verbindungsinformationen f\u00fcr Host `{host}`.", + "keyfile_not_found": "Die angegebene `.knxkeys` Datei wurde nicht in config/.storage/knx/ gefunden", "no_router_discovered": "Es wurde kein KNXnet/IP-Router im Netzwerk gefunden.", "no_tunnel_discovered": "Es konnte kein KNX Tunneling Server in deinem Netzwerk gefunden werden.", "unsupported_tunnel_type": "Ausgew\u00e4hlter Tunneltyp wird vom Gateway nicht unterst\u00fctzt." @@ -20,7 +22,15 @@ "data": { "connection_type": "KNX-Verbindungstyp" }, - "description": "Bitte gib den Verbindungstyp ein, den wir f\u00fcr deine KNX-Verbindung verwenden sollen. \n AUTOMATISCH - Die Integration k\u00fcmmert sich um die Verbindung zu deinem KNX Bus, indem sie einen Gateway-Scan durchf\u00fchrt. \n TUNNELING - Die Integration stellt die Verbindung zu deinem KNX Bus \u00fcber Tunneling her. \n ROUTING - Die Integration stellt die Verbindung zu deinem KNX-Bus \u00fcber Routing her." + "description": "Bitte gib den Verbindungstyp ein, den wir f\u00fcr deine KNX-Verbindung verwenden sollen. \n AUTOMATISCH - Die Integration k\u00fcmmert sich um die Verbindung zu deinem KNX Bus, indem sie einen Gateway-Scan durchf\u00fchrt. \n TUNNELING - Die Integration stellt die Verbindung zu deinem KNX Bus \u00fcber Tunneling her. \n ROUTING - Die Integration stellt die Verbindung zu deinem KNX-Bus \u00fcber Routing her.", + "title": "KNX Verbindung" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "\u201eAutomatisch\u201c verwendet den ersten freien Tunnelendpunkt." + }, + "description": "W\u00e4hle den f\u00fcr die Verbindung verwendeten Tunnel aus.", + "title": "Tunnelendpunkt" }, "manual_tunnel": { "data": { @@ -36,7 +46,8 @@ "port": "Port der KNX/IP-Tunneling Schnittstelle.", "route_back": "Aktiviere diese Option, wenn sich dein KNXnet/IP-Tunnelserver hinter NAT befindet. Gilt nur f\u00fcr UDP-Verbindungen." }, - "description": "Bitte gib die Verbindungsinformationen deiner Tunnel-Schnittstelle ein." + "description": "Bitte gib die Verbindungsinformationen deiner Tunnel-Schnittstelle ein.", + "title": "Tunnel Einstellungen" }, "routing": { "data": { @@ -50,15 +61,17 @@ "individual_address": "Physikalische Adresse, die von Home Assistant verwendet werden soll, z.B. \u201e0.0.4\u201c.", "local_ip": "Lasse das Feld leer, um die automatische Erkennung zu verwenden." }, - "description": "Bitte konfiguriere die Routing-Optionen." + "description": "Bitte konfiguriere die Routing-Optionen.", + "title": "Routing" }, "secure_key_source": { "description": "W\u00e4hle aus, wie du KNX/IP-Secure konfigurieren m\u00f6chtest.", "menu_options": { - "secure_knxkeys": "Verwende eine \".knxkeys\" Datei mit IP-Secure-Schl\u00fcsseln", + "secure_knxkeys": "Verwende eine \".knxkeys\" Datei mit IP-Secure Schl\u00fcsseln", "secure_routing_manual": "IP-Secure Backbone-Schl\u00fcssel manuell konfigurieren", - "secure_tunnel_manual": "IP-Secure-Schl\u00fcssel manuell konfigurieren" - } + "secure_tunnel_manual": "IP-Secure Schl\u00fcssel manuell konfigurieren" + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -69,7 +82,8 @@ "knxkeys_filename": "Die Datei wird in deinem Konfigurationsverzeichnis unter `.storage/knx/` erwartet.\nIm Home Assistant OS w\u00e4re dies `/config/.storage/knx/`\nBeispiel: `my_project.knxkeys`", "knxkeys_password": "Dies wurde beim Exportieren der Datei aus ETS gesetzt." }, - "description": "Bitte gib die Informationen f\u00fcr deine `.knxkeys` Datei ein." + "description": "Bitte gib die Informationen f\u00fcr deine `.knxkeys` Datei ein.", + "title": "Schl\u00fcsselbund" }, "secure_routing_manual": { "data": { @@ -80,7 +94,8 @@ "backbone_key": "Kann im Report \"Projekt-Sicherheit\" eines ETS-Projekts eingesehen werden. z.B. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Der Standardwert ist 1000." }, - "description": "Bitte gib deine IP-Secure Informationen ein." + "description": "Bitte gib deine IP-Secure Informationen ein.", + "title": "Secure Routing" }, "secure_tunnel_manual": { "data": { @@ -93,24 +108,28 @@ "user_id": "Dies ist oft die Tunnelnummer +1. \u201eTunnel 2\u201c h\u00e4tte also die Benutzer-ID \u201e3\u201c.", "user_password": "Passwort f\u00fcr die spezifische Tunnelverbindung, die im Bereich \u201eEigenschaften\u201c des Tunnels in ETS festgelegt wurde." }, - "description": "Bitte gib deine IP-Secure Informationen ein." + "description": "Bitte gib deine IP-Secure Informationen ein.", + "title": "Secure Tunneling" }, "tunnel": { "data": { "gateway": "KNX Tunnel Verbindung" }, - "description": "Bitte w\u00e4hle eine Schnittstelle aus der Liste aus." + "description": "Bitte w\u00e4hle eine Schnittstelle aus der Liste aus.", + "title": "Tunnel" } } }, "options": { "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "file_not_found": "Die angegebene `.knxkeys` Datei wurde im Pfad config/.storage/knx/ nicht gefunden.", "invalid_backbone_key": "Ung\u00fcltiger Backbone-Schl\u00fcssel. 32 Hexadezimalzahlen erwartet.", "invalid_individual_address": "Wert ist keine g\u00fcltige physikalische Adresse. 'Bereich.Linie.Teilnehmer'", "invalid_ip_address": "Ung\u00fcltige IPv4 Adresse.", - "invalid_signature": "Das Passwort zum Entschl\u00fcsseln der `.knxkeys` Datei ist ung\u00fcltig.", + "keyfile_invalid_signature": "Das Passwort f\u00fcr die `.knxkeys` Datei ist falsch.", + "keyfile_no_backbone_key": "Die `.knxkeys` Datei enth\u00e4lt keinen Backbone-Schl\u00fcssel f\u00fcr Secure Routing.", + "keyfile_no_tunnel_for_host": "Die `.knxkeys` Datei enth\u00e4lt keine Verbindungsinformationen f\u00fcr Host `{host}`.", + "keyfile_not_found": "Die angegebene `.knxkeys` Datei wurde nicht in config/.storage/knx/ gefunden", "no_router_discovered": "Es wurde kein KNXnet/IP-Router im Netzwerk gefunden.", "no_tunnel_discovered": "Es konnte kein KNX Tunneling Server in deinem Netzwerk gefunden werden.", "unsupported_tunnel_type": "Ausgew\u00e4hlter Tunneltyp wird vom Gateway nicht unterst\u00fctzt." @@ -124,13 +143,22 @@ "data_description": { "rate_limit": "Maximal ausgehende Telegramme pro Sekunde.\n `0`, um das Limit zu deaktivieren. Empfohlen: 0 oder 20 bis 40", "state_updater": "Standardeinstellung f\u00fcr das Lesen von Zust\u00e4nden aus dem KNX-Bus. Wenn diese Option deaktiviert ist, wird der Home Assistant den Zustand der Entit\u00e4ten nicht aktiv vom KNX-Bus abrufen. Kann durch die Entity-Optionen `sync_state` au\u00dfer Kraft gesetzt werden." - } + }, + "title": "Kommunikationseinstellungen" }, "connection_type": { "data": { - "connection_type": "KNX-Verbindungstyp" + "connection_type": "KNX Verbindungstyp" }, - "description": "Bitte gib den Verbindungstyp ein, den wir f\u00fcr deine KNX-Verbindung verwenden sollen. \n AUTOMATISCH - Die Integration k\u00fcmmert sich um die Verbindung zu deinem KNX Bus, indem sie einen Gateway-Scan durchf\u00fchrt. \n TUNNELING - Die Integration stellt die Verbindung zu deinem KNX Bus \u00fcber Tunneling her. \n ROUTING - Die Integration stellt die Verbindung zu deinem KNX-Bus \u00fcber Routing her." + "description": "Bitte gib den Verbindungstyp ein, den wir f\u00fcr deine KNX-Verbindung verwenden sollen. \n AUTOMATISCH - Die Integration k\u00fcmmert sich um die Verbindung zu deinem KNX Bus, indem sie einen Gateway-Scan durchf\u00fchrt. \n TUNNELING - Die Integration stellt die Verbindung zu deinem KNX Bus \u00fcber eine Tunnel-Schnittstelle her. \n ROUTING - Die Integration kommuniziert mit deinem KNX-Bus \u00fcber Routing.", + "title": "KNX Verbindung" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "\u201eAutomatisch\u201c verwendet den ersten freien Tunnelendpunkt." + }, + "description": "W\u00e4hle den f\u00fcr die Verbindung verwendeten Tunnel aus.", + "title": "Tunnelendpunkt" }, "manual_tunnel": { "data": { @@ -146,13 +174,15 @@ "port": "Port der KNX/IP-Tunneling Schnittstelle.", "route_back": "Aktiviere diese Option, wenn sich dein KNXnet/IP-Tunnelserver hinter NAT befindet. Gilt nur f\u00fcr UDP-Verbindungen." }, - "description": "Bitte gib die Verbindungsinformationen deiner Tunnel-Schnittstelle ein." + "description": "Bitte gib die Verbindungsinformationen deiner Tunnel-Schnittstelle ein.", + "title": "Tunnel Einstellungen" }, "options_init": { "menu_options": { "communication_settings": "Kommunikationseinstellungen", "connection_type": "KNX-Schnittstelle konfigurieren" - } + }, + "title": "KNX Einstellungen" }, "routing": { "data": { @@ -166,15 +196,17 @@ "individual_address": "Physikalische Adresse, die von Home Assistant verwendet werden soll, z.B. \u201e0.0.4\u201c.", "local_ip": "Lasse das Feld leer, um die automatische Erkennung zu verwenden." }, - "description": "Bitte konfiguriere die Routing-Optionen." + "description": "Bitte konfiguriere die Routing-Optionen.", + "title": "Routing" }, "secure_key_source": { "description": "W\u00e4hle aus, wie du KNX/IP-Secure konfigurieren m\u00f6chtest.", "menu_options": { - "secure_knxkeys": "Verwende eine \".knxkeys\" Datei mit IP-Secure-Schl\u00fcsseln", + "secure_knxkeys": "Verwende eine \".knxkeys\" Datei mit IP-Secure Schl\u00fcsseln", "secure_routing_manual": "IP-Secure Backbone-Schl\u00fcssel manuell konfigurieren", - "secure_tunnel_manual": "IP-Secure-Schl\u00fcssel manuell konfigurieren" - } + "secure_tunnel_manual": "IP-Secure Schl\u00fcssel manuell konfigurieren" + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +217,8 @@ "knxkeys_filename": "Die Datei wird in deinem Konfigurationsverzeichnis unter `.storage/knx/` erwartet.\nIm Home Assistant OS w\u00e4re dies `/config/.storage/knx/`\nBeispiel: `my_project.knxkeys`", "knxkeys_password": "Dies wurde beim Exportieren der Datei aus ETS gesetzt." }, - "description": "Bitte gib die Informationen f\u00fcr deine `.knxkeys` Datei ein." + "description": "Bitte gib die Informationen f\u00fcr deine `.knxkeys` Datei ein.", + "title": "Schl\u00fcsselbund" }, "secure_routing_manual": { "data": { @@ -196,7 +229,8 @@ "backbone_key": "Kann im Report \"Projekt-Sicherheit\" eines ETS-Projekts eingesehen werden. z.B. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Der Standardwert ist 1000." }, - "description": "Bitte gib deine IP-Secure Informationen ein." + "description": "Bitte gib deine IP-Secure Informationen ein.", + "title": "Secure Routing" }, "secure_tunnel_manual": { "data": { @@ -209,13 +243,15 @@ "user_id": "Dies ist oft die Tunnelnummer +1. \u201eTunnel 2\u201c h\u00e4tte also die Benutzer-ID \u201e3\u201c.", "user_password": "Passwort f\u00fcr die spezifische Tunnelverbindung, die im Bereich \u201eEigenschaften\u201c des Tunnels in ETS festgelegt wurde." }, - "description": "Bitte gib deine IP-Secure Informationen ein." + "description": "Bitte gib deine IP-Secure Informationen ein.", + "title": "Secure Tunneling" }, "tunnel": { "data": { "gateway": "KNX Tunnel Verbindung" }, - "description": "Bitte w\u00e4hle eine Schnittstelle aus der Liste aus." + "description": "Bitte w\u00e4hle eine Schnittstelle aus der Liste aus.", + "title": "Tunnel" } } } diff --git a/homeassistant/components/knx/translations/el.json b/homeassistant/components/knx/translations/el.json index b9e71239cdb..55d2b08ec62 100644 --- a/homeassistant/components/knx/translations/el.json +++ b/homeassistant/components/knx/translations/el.json @@ -6,11 +6,13 @@ }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "file_not_found": "\u03a4\u03bf \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf knxkeys \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae config/.storage/knx/", "invalid_backbone_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03bf\u03c1\u03bc\u03bf\u03cd. \u0391\u03bd\u03b1\u03bc\u03ad\u03bd\u03bf\u03bd\u03c4\u03b1\u03b9 32 \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03bf\u03af \u03b1\u03c1\u03b9\u03b8\u03bc\u03bf\u03af.", "invalid_individual_address": "\u0397 \u03c4\u03b9\u03bc\u03ae \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03b5\u03b9 \u03bc\u03b5 \u03c4\u03bf \u03bc\u03bf\u03c4\u03af\u03b2\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03bc\u03b5\u03bc\u03bf\u03bd\u03c9\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 KNX.\n \"area.line.device\"", "invalid_ip_address": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IPv4.", - "invalid_signature": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 knxkeys \u03b5\u03af\u03bd\u03b1\u03b9 \u03bb\u03ac\u03b8\u03bf\u03c2.", + "keyfile_invalid_signature": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 `.knxkeys` \u03b5\u03af\u03bd\u03b1\u03b9 \u03bb\u03ac\u03b8\u03bf\u03c2.", + "keyfile_no_backbone_key": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03b2\u03b1\u03c3\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b3\u03b9\u03b1 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ae \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7.", + "keyfile_no_tunnel_for_host": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \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}`.", + "keyfile_not_found": "\u03a4\u03bf \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae config/.storage/knx/", "no_router_discovered": "\u0394\u03b5\u03bd \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae\u03c2 KNXnet/IP \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf.", "no_tunnel_discovered": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc\u03c2 \u03bf \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2.", "unsupported_tunnel_type": "\u039f \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03c0\u03cd\u03bb\u03b7." @@ -20,12 +22,20 @@ "data": { "connection_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 KNX" }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03bf\u03c5\u03bc\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c3\u03b1\u03c2 KNX.\n AUTOMATIC - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c6\u03c1\u03bf\u03bd\u03c4\u03af\u03b6\u03b5\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03bf KNX Bus \u03c3\u03b1\u03c2 \u03b5\u03ba\u03c4\u03b5\u03bb\u03ce\u03bd\u03c4\u03b1\u03c2 \u03bc\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7 \u03c0\u03cd\u03bb\u03b7\u03c2.\n TUNNELING - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2.\n \u0394\u03a1\u039f\u039c\u039f\u039b\u039f\u0393\u0397\u03a3\u0397 - \u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03bf\u03c5\u03bc\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c3\u03b1\u03c2 KNX.\n AUTOMATIC - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c6\u03c1\u03bf\u03bd\u03c4\u03af\u03b6\u03b5\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03bf KNX Bus \u03c3\u03b1\u03c2 \u03b5\u03ba\u03c4\u03b5\u03bb\u03ce\u03bd\u03c4\u03b1\u03c2 \u03bc\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7 \u03c0\u03cd\u03bb\u03b7\u03c2.\n TUNNELING - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2.\n \u0394\u03a1\u039f\u039c\u039f\u039b\u039f\u0393\u0397\u03a3\u0397 - \u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2.", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "\u03a4\u03bf \"\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf\" \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03c4\u03bf \u03c0\u03c1\u03ce\u03c4\u03bf \u03b5\u03bb\u03b5\u03cd\u03b8\u03b5\u03c1\u03bf \u03c4\u03b5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b7\u03bc\u03b5\u03af\u03bf \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2." + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7.", + "title": "\u03a4\u03b5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b7\u03bc\u03b5\u03af\u03bf \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2" }, "manual_tunnel": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", - "local_ip": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae IP \u03c4\u03bf\u03c5 Home Assistant (\u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b5\u03bd\u03ae \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7)", + "local_ip": "\u03a4\u03bf\u03c0\u03b9\u03ba\u03ae IP \u03c4\u03bf\u03c5 Home Assistant", "port": "\u0398\u03cd\u03c1\u03b1", "route_back": "\u03a0\u03af\u03c3\u03c9 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae / \u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 NAT", "tunneling_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX" @@ -36,7 +46,8 @@ "port": "\u0398\u03cd\u03c1\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX/IP.", "route_back": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03b5\u03ac\u03bd \u03bf \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 \u03c3\u03b1\u03c2 KNXnet/IP tunneling \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c0\u03af\u03c3\u03c9 \u03b1\u03c0\u03cc \u03c4\u03bf NAT. \u0399\u03c3\u03c7\u03cd\u03b5\u03b9 \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03b9\u03c2 UDP." }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b9\u03ac\u03bd\u03bf\u03b9\u03be\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b9\u03ac\u03bd\u03bf\u03b9\u03be\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2.", + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2" }, "routing": { "data": { @@ -50,7 +61,8 @@ "individual_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 KNX \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant, \u03c0.\u03c7. `0.0.4`.", "local_ip": "\u0391\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7." }, - "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2." + "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2.", + "title": "\u0394\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7" }, "secure_key_source": { "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03ce\u03c2 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf KNX/IP Secure.", @@ -58,18 +70,20 @@ "secure_knxkeys": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03ac \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 IP", "secure_routing_manual": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd IP \u03b1\u03c3\u03c6\u03b1\u03bb\u03bf\u03cd\u03c2 \u03ba\u03bf\u03c1\u03bc\u03bf\u03cd \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1", "secure_tunnel_manual": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5\u03c2 \u03c4\u03c9\u03bd \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03b7\u03c1\u03af\u03c9\u03bd \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 IP \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { "knxkeys_filename": "\u03a4\u03bf \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 \u03c3\u03b1\u03c2 `.knxkeys` (\u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b1\u03bc\u03b2\u03b1\u03bd\u03bf\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03b5\u03c0\u03ad\u03ba\u03c4\u03b1\u03c3\u03b7\u03c2)", - "knxkeys_password": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 knxkeys" + "knxkeys_password": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 `.knxkeys`" }, "data_description": { "knxkeys_filename": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03b1\u03bd\u03b1\u03bc\u03ad\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03bf\u03bd \u03ba\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd \u03c3\u03c4\u03bf `.storage/knx/`.\n \u03a3\u03c4\u03bf Home Assistant OS \u03b1\u03c5\u03c4\u03cc \u03b8\u03b1 \u03ae\u03c4\u03b1\u03bd `/config/.storage/knx/`\n \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: `my_project.knxkeys`", "knxkeys_password": "\u0391\u03c5\u03c4\u03cc \u03bf\u03c1\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03be\u03b1\u03b3\u03c9\u03b3\u03ae \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 \u03b1\u03c0\u03cc \u03c4\u03bf ETS." }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf knxkeys \u03c3\u03b1\u03c2." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys`.", + "title": "\u0391\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd" }, "secure_routing_manual": { "data": { @@ -80,7 +94,8 @@ "backbone_key": "\u039c\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c6\u03b1\u03bd\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u00ab\u0391\u03c3\u03c6\u03ac\u03bb\u03b5\u03b9\u03b1\u00bb \u03b5\u03bd\u03cc\u03c2 \u03ad\u03c1\u03b3\u03bf\u03c5 ETS. \u03a0.\u03c7. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "\u0397 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 1000." }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP \u03c3\u03b1\u03c2." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP \u03c3\u03b1\u03c2.", + "title": "\u0391\u03c3\u03c6\u03b1\u03bb\u03ae\u03c2 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7" }, "secure_tunnel_manual": { "data": { @@ -93,24 +108,28 @@ "user_id": "\u0391\u03c5\u03c4\u03cc \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c7\u03bd\u03ac \u03bf \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 +1. \u0386\u03c1\u03b1 \u03c4\u03bf \"Tunnel 2\" \u03b8\u03b1 \u03ad\u03c7\u03b5\u03b9 User-ID \"3\".", "user_password": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03bf\u03bd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u00ab\u0399\u03b4\u03b9\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\u00bb \u03c4\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 \u03c3\u03c4\u03bf ETS." }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP \u03c3\u03b1\u03c2." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP \u03c3\u03b1\u03c2.", + "title": "\u0391\u03c3\u03c6\u03b1\u03bb\u03ae\u03c2 \u03b4\u03b9\u03ac\u03bd\u03bf\u03b9\u03be\u03b7 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2" }, "tunnel": { "data": { "gateway": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX" }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1." + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1.", + "title": "\u03a3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1" } } }, "options": { "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", - "file_not_found": "\u03a4\u03bf \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf knxkeys \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae config/.storage/knx/", "invalid_backbone_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03bf\u03c1\u03bc\u03bf\u03cd. \u0391\u03bd\u03b1\u03bc\u03ad\u03bd\u03bf\u03bd\u03c4\u03b1\u03b9 32 \u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03bf\u03af \u03b1\u03c1\u03b9\u03b8\u03bc\u03bf\u03af.", "invalid_individual_address": "\u0397 \u03c4\u03b9\u03bc\u03ae \u03b4\u03b5\u03bd \u03c4\u03b1\u03b9\u03c1\u03b9\u03ac\u03b6\u03b5\u03b9 \u03bc\u03b5 \u03c4\u03bf \u03bc\u03bf\u03c4\u03af\u03b2\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03bc\u03b5\u03bc\u03bf\u03bd\u03c9\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 KNX.\n \"area.line.device\"", "invalid_ip_address": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IPv4.", - "invalid_signature": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 knxkeys \u03b5\u03af\u03bd\u03b1\u03b9 \u03bb\u03ac\u03b8\u03bf\u03c2.", + "keyfile_invalid_signature": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 `.knxkeys` \u03b5\u03af\u03bd\u03b1\u03b9 \u03bb\u03ac\u03b8\u03bf\u03c2.", + "keyfile_no_backbone_key": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03b2\u03b1\u03c3\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b3\u03b9\u03b1 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ae \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7.", + "keyfile_no_tunnel_for_host": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03ae\u03c1\u03b9\u03b1 \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}`.", + "keyfile_not_found": "\u03a4\u03bf \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae config/.storage/knx/", "no_router_discovered": "\u0394\u03b5\u03bd \u03b1\u03bd\u03b1\u03ba\u03b1\u03bb\u03cd\u03c6\u03b8\u03b7\u03ba\u03b5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae\u03c2 KNXnet/IP \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf.", "no_tunnel_discovered": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03cc\u03c2 \u03bf \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03cc\u03c2 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2.", "unsupported_tunnel_type": "\u039f \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03c0\u03cd\u03bb\u03b7." @@ -124,13 +143,22 @@ "data_description": { "rate_limit": "\u039c\u03ad\u03b3\u03b9\u03c3\u03c4\u03b1 \u03b5\u03be\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03b1 \u03c4\u03b7\u03bb\u03b5\u03b3\u03c1\u03b1\u03c6\u03ae\u03bc\u03b1\u03c4\u03b1 \u03b1\u03bd\u03ac \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03bf.\n \"0\" \u03b3\u03b9\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03bf\u03c1\u03af\u03bf\u03c5. \u03a3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9: 0 \u03ae 20 \u03ad\u03c9\u03c2 40", "state_updater": "\u039f\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03c9\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX. \u038c\u03c4\u03b1\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf, \u03c4\u03bf Home Assistant \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03ac \u03b5\u03bd\u03b5\u03c1\u03b3\u03ac \u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ac\u03c3\u03b5\u03b9\u03c2 \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf KNX Bus. \u039c\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bc\u03c6\u03b8\u03b5\u03af \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03bf\u03bd\u03c4\u03bf\u03c4\u03ae\u03c4\u03c9\u03bd \u00absync_state\u00bb." - } + }, + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b1\u03c2" }, "connection_type": { "data": { "connection_type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 KNX" }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03bf\u03c5\u03bc\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c3\u03b1\u03c2 KNX.\n AUTOMATIC - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c6\u03c1\u03bf\u03bd\u03c4\u03af\u03b6\u03b5\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03bf KNX Bus \u03c3\u03b1\u03c2 \u03b5\u03ba\u03c4\u03b5\u03bb\u03ce\u03bd\u03c4\u03b1\u03c2 \u03bc\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7 \u03c0\u03cd\u03bb\u03b7\u03c2.\n TUNNELING - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2.\n \u0394\u03a1\u039f\u039c\u039f\u039b\u039f\u0393\u0397\u03a3\u0397 - \u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03bf\u03c5\u03bc\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae \u03c3\u03b1\u03c2 KNX.\n AUTOMATIC - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c6\u03c1\u03bf\u03bd\u03c4\u03af\u03b6\u03b5\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03c3\u03b9\u03bc\u03cc\u03c4\u03b7\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03bf KNX Bus \u03c3\u03b1\u03c2 \u03b5\u03ba\u03c4\u03b5\u03bb\u03ce\u03bd\u03c4\u03b1\u03c2 \u03bc\u03b9\u03b1 \u03c3\u03ac\u03c1\u03c9\u03c3\u03b7 \u03c0\u03cd\u03bb\u03b7\u03c2.\n TUNNELING - \u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2.\n \u0394\u03a1\u039f\u039c\u039f\u039b\u039f\u0393\u0397\u03a3\u0397 - \u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b8\u03b1 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf \u03b4\u03af\u03b1\u03c5\u03bb\u03bf KNX \u03bc\u03ad\u03c3\u03c9 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2.", + "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "\u03a4\u03bf \"\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf\" \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9 \u03c4\u03bf \u03c0\u03c1\u03ce\u03c4\u03bf \u03b5\u03bb\u03b5\u03cd\u03b8\u03b5\u03c1\u03bf \u03c4\u03b5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b7\u03bc\u03b5\u03af\u03bf \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2." + }, + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c4\u03b7 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7.", + "title": "\u03a4\u03b5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b7\u03bc\u03b5\u03af\u03bf \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2" }, "manual_tunnel": { "data": { @@ -146,13 +174,15 @@ "port": "\u0398\u03cd\u03c1\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX/IP.", "route_back": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03b5\u03ac\u03bd \u03bf \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae\u03c2 \u03c3\u03b1\u03c2 KNXnet/IP tunneling \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c0\u03af\u03c3\u03c9 \u03b1\u03c0\u03cc \u03c4\u03bf NAT. \u0399\u03c3\u03c7\u03cd\u03b5\u03b9 \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03b9\u03c2 UDP." }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b9\u03ac\u03bd\u03bf\u03b9\u03be\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b4\u03b9\u03ac\u03bd\u03bf\u03b9\u03be\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2.", + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 Tunnel" }, "options_init": { "menu_options": { "communication_settings": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b1\u03c2", "connection_type": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b4\u03b9\u03b1\u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 KNX" - } + }, + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 KNX" }, "routing": { "data": { @@ -166,7 +196,8 @@ "individual_address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 KNX \u03c0\u03bf\u03c5 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03b5\u03af \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant, \u03c0.\u03c7. `0.0.4`.", "local_ip": "\u0391\u03c6\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03bd\u03cc \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7." }, - "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2." + "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7\u03c2.", + "title": "\u0394\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7" }, "secure_key_source": { "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03c0\u03ce\u03c2 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf KNX/IP Secure.", @@ -174,7 +205,8 @@ "secure_knxkeys": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf `.knxkeys` \u03c0\u03bf\u03c5 \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03ac \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 IP", "secure_routing_manual": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd IP \u03b1\u03c3\u03c6\u03b1\u03bb\u03bf\u03cd\u03c2 \u03ba\u03bf\u03c1\u03bc\u03bf\u03cd \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1", "secure_tunnel_manual": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5\u03c2 \u03c4\u03c9\u03bd \u03b4\u03b9\u03b1\u03c0\u03b9\u03c3\u03c4\u03b5\u03c5\u03c4\u03b7\u03c1\u03af\u03c9\u03bd \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03b1\u03c2 IP \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +217,8 @@ "knxkeys_filename": "\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03b1\u03bd\u03b1\u03bc\u03ad\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03bf\u03bd \u03ba\u03b1\u03c4\u03ac\u03bb\u03bf\u03b3\u03bf \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd \u03c3\u03c4\u03bf `.storage/knx/`.\n \u03a3\u03c4\u03bf Home Assistant OS \u03b1\u03c5\u03c4\u03cc \u03b8\u03b1 \u03ae\u03c4\u03b1\u03bd `/config/.storage/knx/`\n \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1: `my_project.knxkeys`", "knxkeys_password": "\u0391\u03c5\u03c4\u03cc \u03bf\u03c1\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03be\u03b1\u03b3\u03c9\u03b3\u03ae \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 \u03b1\u03c0\u03cc \u03c4\u03bf ETS." }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf knxkeys \u03c3\u03b1\u03c2." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf knxkeys \u03c3\u03b1\u03c2.", + "title": "\u0391\u03c1\u03c7\u03b5\u03af\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd" }, "secure_routing_manual": { "data": { @@ -196,7 +229,8 @@ "backbone_key": "\u039c\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c6\u03b1\u03bd\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac \u00ab\u0391\u03c3\u03c6\u03ac\u03bb\u03b5\u03b9\u03b1\u00bb \u03b5\u03bd\u03cc\u03c2 \u03ad\u03c1\u03b3\u03bf\u03c5 ETS. \u03a0.\u03c7. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "\u0397 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 1000." }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP \u03c3\u03b1\u03c2." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP \u03c3\u03b1\u03c2.", + "title": "\u0391\u03c3\u03c6\u03b1\u03bb\u03ae\u03c2 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03cc\u03b3\u03b7\u03c3\u03b7" }, "secure_tunnel_manual": { "data": { @@ -209,13 +243,15 @@ "user_id": "\u0391\u03c5\u03c4\u03cc \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c5\u03c7\u03bd\u03ac \u03bf \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 +1. \u0386\u03c1\u03b1 \u03c4\u03bf \"Tunnel 2\" \u03b8\u03b1 \u03ad\u03c7\u03b5\u03b9 User-ID \"3\".", "user_password": "\u039f \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03b3\u03ba\u03b5\u03ba\u03c1\u03b9\u03bc\u03ad\u03bd\u03b7 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03bf\u03bd \u03c0\u03af\u03bd\u03b1\u03ba\u03b1 \u00ab\u0399\u03b4\u03b9\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2\u00bb \u03c4\u03b7\u03c2 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 \u03c3\u03c4\u03bf ETS." }, - "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP \u03c3\u03b1\u03c2." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b1\u03c3\u03c6\u03b1\u03bb\u03b5\u03af\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 IP \u03c3\u03b1\u03c2.", + "title": "\u0391\u03c3\u03c6\u03b1\u03bb\u03ad\u03c2 tunneling" }, "tunnel": { "data": { "gateway": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1\u03c2 KNX" }, - "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1." + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c0\u03cd\u03bb\u03b7 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03bb\u03af\u03c3\u03c4\u03b1.", + "title": "\u03a3\u03ae\u03c1\u03b1\u03b3\u03b3\u03b1" } } } diff --git a/homeassistant/components/knx/translations/en.json b/homeassistant/components/knx/translations/en.json index 54cbabc8272..34073c0b49f 100644 --- a/homeassistant/components/knx/translations/en.json +++ b/homeassistant/components/knx/translations/en.json @@ -6,11 +6,9 @@ }, "error": { "cannot_connect": "Failed to connect", - "file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/", "invalid_backbone_key": "Invalid backbone key. 32 hexadecimal numbers expected.", "invalid_individual_address": "Value does not match pattern for KNX individual address.\n'area.line.device'", "invalid_ip_address": "Invalid IPv4 address.", - "invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.", "keyfile_invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.", "keyfile_no_backbone_key": "The `.knxkeys` file does not contain a backbone key for secure routing.", "keyfile_no_tunnel_for_host": "The `.knxkeys` file does not contain credentials for host `{host}`.", @@ -125,11 +123,9 @@ "options": { "error": { "cannot_connect": "Failed to connect", - "file_not_found": "The specified `.knxkeys` file was not found in the path config/.storage/knx/", "invalid_backbone_key": "Invalid backbone key. 32 hexadecimal numbers expected.", "invalid_individual_address": "Value does not match pattern for KNX individual address.\n'area.line.device'", "invalid_ip_address": "Invalid IPv4 address.", - "invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.", "keyfile_invalid_signature": "The password to decrypt the `.knxkeys` file is wrong.", "keyfile_no_backbone_key": "The `.knxkeys` file does not contain a backbone key for secure routing.", "keyfile_no_tunnel_for_host": "The `.knxkeys` file does not contain credentials for host `{host}`.", diff --git a/homeassistant/components/knx/translations/es.json b/homeassistant/components/knx/translations/es.json index 25605e9d471..d5da18139b3 100644 --- a/homeassistant/components/knx/translations/es.json +++ b/homeassistant/components/knx/translations/es.json @@ -6,11 +6,9 @@ }, "error": { "cannot_connect": "No se pudo conectar", - "file_not_found": "El archivo `.knxkeys` especificado no se encontr\u00f3 en la ruta config/.storage/knx/", "invalid_backbone_key": "Clave de red troncal no v\u00e1lida. Se esperan 32 n\u00fameros hexadecimales.", "invalid_individual_address": "El valor no coincide con el patr\u00f3n de la direcci\u00f3n KNX individual. 'area.line.device'", "invalid_ip_address": "Direcci\u00f3n IPv4 no v\u00e1lida.", - "invalid_signature": "La contrase\u00f1a para descifrar el archivo `.knxkeys` es incorrecta.", "keyfile_invalid_signature": "La contrase\u00f1a para descifrar el archivo `.knxkeys` es incorrecta.", "keyfile_no_backbone_key": "El archivo `.knxkeys` no contiene una clave principal para el enrutamiento seguro.", "keyfile_no_tunnel_for_host": "El archivo `.knxkeys` no contiene credenciales para el host `{host}`.", @@ -27,6 +25,13 @@ "description": "Por favor, introduce el tipo de conexi\u00f3n que debemos usar para tu conexi\u00f3n KNX.\n AUTOM\u00c1TICO: la integraci\u00f3n se encarga de la conectividad a tu bus KNX mediante la realizaci\u00f3n de un escaneo de la puerta de enlace.\n T\u00daNELES: la integraci\u00f3n se conectar\u00e1 a tu bus KNX a trav\u00e9s de t\u00faneles.\n ENRUTAMIENTO: la integraci\u00f3n se conectar\u00e1 a su tus KNX a trav\u00e9s del enrutamiento.", "title": "Conexi\u00f3n KNX" }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "'Autom\u00e1tico' utilizar\u00e1 el primer extremo del t\u00fanel libre." + }, + "description": "Selecciona el t\u00fanel utilizado para la conexi\u00f3n.", + "title": "Extremo del t\u00fanel" + }, "manual_tunnel": { "data": { "host": "Host", @@ -118,11 +123,9 @@ "options": { "error": { "cannot_connect": "No se pudo conectar", - "file_not_found": "El archivo `.knxkeys` especificado no se encontr\u00f3 en la ruta config/.storage/knx/", "invalid_backbone_key": "Clave de red troncal no v\u00e1lida. Se esperan 32 n\u00fameros hexadecimales.", "invalid_individual_address": "El valor no coincide con el patr\u00f3n de la direcci\u00f3n KNX individual. 'area.line.device'", "invalid_ip_address": "Direcci\u00f3n IPv4 no v\u00e1lida.", - "invalid_signature": "La contrase\u00f1a para descifrar el archivo `.knxkeys` es incorrecta.", "keyfile_invalid_signature": "La contrase\u00f1a para descifrar el archivo `.knxkeys` es incorrecta.", "keyfile_no_backbone_key": "El archivo `.knxkeys` no contiene una clave principal para el enrutamiento seguro.", "keyfile_no_tunnel_for_host": "El archivo `.knxkeys` no contiene credenciales para el host `{host}`.", @@ -150,6 +153,13 @@ "description": "Por favor, introduce el tipo de conexi\u00f3n que debemos usar para tu conexi\u00f3n KNX.\n AUTOM\u00c1TICO: la integraci\u00f3n se encarga de la conectividad a tu bus KNX mediante la realizaci\u00f3n de un escaneo de la puerta de enlace.\n T\u00daNELES: la integraci\u00f3n se conectar\u00e1 a tu bus KNX a trav\u00e9s de t\u00faneles.\n ENRUTAMIENTO: la integraci\u00f3n se conectar\u00e1 a su tus KNX a trav\u00e9s del enrutamiento.", "title": "Conexi\u00f3n KNX" }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "'Autom\u00e1tico' utilizar\u00e1 el primer extremo del t\u00fanel libre." + }, + "description": "Selecciona el t\u00fanel utilizado para la conexi\u00f3n.", + "title": "Extremo del t\u00fanel" + }, "manual_tunnel": { "data": { "host": "Host", diff --git a/homeassistant/components/knx/translations/et.json b/homeassistant/components/knx/translations/et.json index ab2e5b21819..a4cb95f6598 100644 --- a/homeassistant/components/knx/translations/et.json +++ b/homeassistant/components/knx/translations/et.json @@ -6,11 +6,13 @@ }, "error": { "cannot_connect": "\u00dchendamine nurjus", - "file_not_found": "M\u00e4\u00e4ratud faili \".knxkeys\" ei leitud asukohas config/.storage/knx/", "invalid_backbone_key": "Kehtetu magistraalv\u00f5ti. Eeldatakse 32 kuueteistk\u00fcmnendarvu.", "invalid_individual_address": "V\u00e4\u00e4rtus ei \u00fchti KNX-i individuaalse aadressi mustriga.\n 'area.line.device'", "invalid_ip_address": "Kehtetu IPv4 aadress.", - "invalid_signature": "Parool faili `.knxkeys` dekr\u00fcpteerimiseks on vale.", + "keyfile_invalid_signature": "Parool faili `.knxkeys` dekr\u00fcpteerimiseks on vale.", + "keyfile_no_backbone_key": "Fail '.knxkeys' ei sisalda turvalise marsruutimise magistraalv\u00f5tit.", + "keyfile_no_tunnel_for_host": "Fail '.knxkeys' ei sisalda hosti '{host}' mandaati.", + "keyfile_not_found": "M\u00e4\u00e4ratud faili \".knxkeys\" ei leitud asukohas config/.storage/knx/", "no_router_discovered": "V\u00f5rgus ei leitud \u00fchtegi KNXnet/IP-ruuterit.", "no_tunnel_discovered": "V\u00f5rgust ei leitud KNX tunneliserverit.", "unsupported_tunnel_type": "L\u00fc\u00fcs ei toeta valitud tunnelit\u00fc\u00fcpi." @@ -20,7 +22,15 @@ "data": { "connection_type": "KNX \u00fchenduse t\u00fc\u00fcp" }, - "description": "Sisesta \u00fchenduse t\u00fc\u00fcp, mida kasutada KNX-\u00fchenduse jaoks. \n AUTOMAATNE \u2013 sidumine hoolitseb KNX siini \u00fchenduvuse eest, tehes l\u00fc\u00fcsikontrolli. \n TUNNELING - sidumine \u00fchendub KNX siiniga tunneli kaudu. \n MARSRUUTIMINE \u2013 sidumine \u00fchendub marsruudi kaudu KNX siiniga." + "description": "Sisesta \u00fchenduse t\u00fc\u00fcp, mida kasutada KNX-\u00fchenduse jaoks. \n AUTOMAATNE \u2013 sidumine hoolitseb KNX siini \u00fchenduvuse eest, tehes l\u00fc\u00fcsikontrolli. \n TUNNELING - sidumine \u00fchendub KNX siiniga tunneli kaudu. \n MARSRUUTIMINE \u2013 sidumine \u00fchendub marsruudi kaudu KNX siiniga.", + "title": "KNX \u00fchendus" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "\"Auto\" kasutab esimest vaba tunneli l\u00f5pp-punkti." + }, + "description": "Vali \u00fchenduseks kasutatav tunnel.", + "title": "Tunneli l\u00f5pppunkt" }, "manual_tunnel": { "data": { @@ -36,7 +46,8 @@ "port": "KNX/IP-tunneldusseadme port.", "route_back": "Luba, kui KNXneti/IP tunneldusserver on NAT-i taga. Kehtib ainult UDP-\u00fchenduste puhul." }, - "description": "Sisesta tunneldamisseadme \u00fchenduse teave." + "description": "Sisesta tunneldamisseadme \u00fchenduse teave.", + "title": "Tunneli seaded" }, "routing": { "data": { @@ -50,7 +61,8 @@ "individual_address": "Home Assistantis kasutatav KNX-aadress, nt \"0.0.4\".", "local_ip": "Automaatse avastamise kasutamiseks j\u00e4ta t\u00fchjaks." }, - "description": "Konfigureeri marsruutimissuvandid." + "description": "Konfigureeri marsruutimissuvandid.", + "title": "Marsruutimine" }, "secure_key_source": { "description": "Vali kuidas soovid KNX/IP Secure'i seadistada.", @@ -58,7 +70,8 @@ "secure_knxkeys": "Kasuta knxkeys faili mis sisaldab IP Secure teavet.", "secure_routing_manual": "Seadista IP secure magistraalv\u00f5ti k\u00e4sitsi", "secure_tunnel_manual": "Seadista IP secure mandaadid k\u00e4sitsi" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -69,7 +82,8 @@ "knxkeys_filename": "Eeldatakse, et fail asub konfiguratsioonikataloogis kaustas \".storage/knx/\".\nHome Assistant OS-is oleks see `/config/.storage/knx/`\n N\u00e4ide: \"minu_projekt.knxkeys\".", "knxkeys_password": "See m\u00e4\u00e4rati faili eksportimisel ETSist." }, - "description": "Sisesta oma `.knxkeys` faili teave." + "description": "Sisesta oma `.knxkeys` faili teave.", + "title": "V\u00f5tmefail" }, "secure_routing_manual": { "data": { @@ -80,7 +94,8 @@ "backbone_key": "Kuvatakse ETS projekti 'Turvalisus' vaates. N\u00e4iteks '0011223344...'", "sync_latency_tolerance": "Vaikev\u00e4\u00e4rtus on 1000." }, - "description": "Sisesta IP Secure teave." + "description": "Sisesta IP Secure teave.", + "title": "Turvaline marsruutimine" }, "secure_tunnel_manual": { "data": { @@ -93,24 +108,28 @@ "user_id": "See on sageli tunneli number +1. Nii et tunnel 2 oleks kasutaja ID-ga 3.", "user_password": "Konkreetse tunneli\u00fchenduse parool, mis on m\u00e4\u00e4ratud ETS-i tunneli paneelil \u201eAtribuudid\u201d." }, - "description": "Sisesta oma IP secure teave." + "description": "Sisesta oma IP secure teave.", + "title": "Turvaline tunneldamine" }, "tunnel": { "data": { "gateway": "KNX tunneli \u00fchendus" }, - "description": "Vali loendist l\u00fc\u00fcs." + "description": "Vali loendist l\u00fc\u00fcs.", + "title": "Tunnel" } } }, "options": { "error": { "cannot_connect": "\u00dchendamine nurjus", - "file_not_found": "M\u00e4\u00e4ratud kirjet '.knxkeys' ei leitud asukohast config/.storage/knx/", "invalid_backbone_key": "Kehtetu magistraalv\u00f5ti. Eeldatakse 32 kuueteistk\u00fcmnendarvu.", "invalid_individual_address": "V\u00e4\u00e4rtuse mall ei vasta KNX seadme \u00fcksuse aadressile.\n'area.line.device'", "invalid_ip_address": "Vigane IPv4 aadress", - "invalid_signature": "'.knxkeys' kirje dekr\u00fcptimisv\u00f5ti on vale.", + "keyfile_invalid_signature": "Parool faili `.knxkeys` dekr\u00fcpteerimiseks on vale.", + "keyfile_no_backbone_key": "Fail '.knxkeys' ei sisalda turvalise marsruutimise magistraalv\u00f5tit.", + "keyfile_no_tunnel_for_host": "Fail '.knxkeys' ei sisalda hosti '{host}' mandaati.", + "keyfile_not_found": "M\u00e4\u00e4ratud faili \".knxkeys\" ei leitud asukohas config/.storage/knx/", "no_router_discovered": "V\u00f5rgus ei leitud \u00fchtegi KNXnet/IP-ruuterit.", "no_tunnel_discovered": "V\u00f5rgust ei leitud KNX tunneliserverit.", "unsupported_tunnel_type": "L\u00fc\u00fcs ei toeta valitud tunnelit\u00fc\u00fcpi." @@ -124,13 +143,22 @@ "data_description": { "rate_limit": "Maksimaalne v\u00e4ljaminevate telegrammide arv sekundis. '0 piirangu eemaldamiseks. Soovitatav: 20 kuni 40", "state_updater": "M\u00e4\u00e4ra KNX siini olekute lugemise vaikev\u00e4\u00e4rtused. Kui see on keelatud, ei too Home Assistant aktiivselt olemi olekuid KNX siinilt. Saab alistada olemivalikute s\u00fcnkroonimise_olekuga." - } + }, + "title": "\u00dchenduse s\u00e4tted" }, "connection_type": { "data": { "connection_type": "KNX \u00fchenduse t\u00fc\u00fcp" }, - "description": "Sisesta \u00fchenduse t\u00fc\u00fcp, mida kasutada KNX-\u00fchenduse jaoks. \n AUTOMAATNE \u2013 sidumine hoolitseb KNX siini \u00fchenduvuse eest, tehes l\u00fc\u00fcsikontrolli. \n TUNNELING - sidumine \u00fchendub KNX siiniga tunneli kaudu. \n MARSRUUTIMINE \u2013 sidumine \u00fchendub marsruudi kaudu KNX siiniga." + "description": "Sisesta \u00fchenduse t\u00fc\u00fcp, mida kasutada KNX-\u00fchenduse jaoks. \n AUTOMAATNE \u2013 sidumine hoolitseb KNX siini \u00fchenduvuse eest, tehes l\u00fc\u00fcsikontrolli. \n TUNNELING - sidumine \u00fchendub KNX siiniga tunneli kaudu. \n MARSRUUTIMINE \u2013 sidumine \u00fchendub marsruudi kaudu KNX siiniga.", + "title": "KNX \u00fchendus" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "'Auto' kasutab esimest vaba tunneli l\u00f5pppunkti." + }, + "description": "Vali \u00fchenduseks kasutatav tunnel", + "title": "Tunneli l\u00f5pppunkt" }, "manual_tunnel": { "data": { @@ -146,13 +174,15 @@ "port": "KNX/IP tunneldusseadme port.", "route_back": "Luba kui KNXnet/IP server on NAT-i taga. Kehtib ainult UDP \u00fchendustele." }, - "description": "Sisesta tunnel\u00fchenduse parameetrid." + "description": "Sisesta tunnel\u00fchenduse parameetrid.", + "title": "Tunneli s\u00e4tted" }, "options_init": { "menu_options": { "communication_settings": "\u00dchenduse seaded", "connection_type": "Seadista KNX liides" - } + }, + "title": "KNX seaded" }, "routing": { "data": { @@ -166,7 +196,8 @@ "individual_address": "Home Assistantis kasutatav KNX aadress, n\u00e4iteks '0.0.4''", "local_ip": "Automaatseks tuvastamiseks j\u00e4ta t\u00fchjaks." }, - "description": "Seadista marsruutimine" + "description": "Seadista marsruutimine", + "title": "Ruutimine" }, "secure_key_source": { "description": "Vali kuidas soovid KNX/IP Secure'i seadistada.", @@ -174,7 +205,8 @@ "secure_knxkeys": "Kasuta knxkeys faili mis sisaldab IP Secure teavet.", "secure_routing_manual": "Seadista IP secure magistraalv\u00f5ti k\u00e4sitsi", "secure_tunnel_manual": "Seadista IP secure mandaadid k\u00e4sitsi" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +217,8 @@ "knxkeys_filename": "See kirje peaks asuma seadete kaustas '.storage/knx/'.\nHome Assistant OS puhul oleks see 'config/.storage/knx/'\nN\u00e4iteks: 'my_project.knxkeys'", "knxkeys_password": "See saadi kirje eksportisel ETS-ist." }, - "description": "Sisesta oma '.knxkeys' kirje teave" + "description": "Sisesta oma '.knxkeys' kirje teave", + "title": "V\u00f5tmekirje" }, "secure_routing_manual": { "data": { @@ -196,7 +229,8 @@ "backbone_key": "Kuvatakse ETS projekti 'Turvalisus' vaates. N\u00e4iteks '0011223344...'", "sync_latency_tolerance": "Vaikev\u00e4\u00e4rtus on 1000." }, - "description": "Sisesta IP Secure teave." + "description": "Sisesta IP Secure teave.", + "title": "Turvaline ruutimine" }, "secure_tunnel_manual": { "data": { @@ -209,13 +243,15 @@ "user_id": "See on tavaliselt tunneli number+1. Seega 'Tunnel 2' on kasutaja ID-ga '3'.", "user_password": "Konkreetse tunneli\u00fchenduse parool, mis on m\u00e4\u00e4ratud ETS-i tunneli paneelil \u201eAtribuudid\u201d." }, - "description": "Sisesta IP secure teave." + "description": "Sisesta IP secure teave.", + "title": "Turvaline tunnel" }, "tunnel": { "data": { "gateway": "KNX tunnel\u00fchendus" }, - "description": "Vali nimekirjast l\u00fc\u00fcs" + "description": "Vali nimekirjast l\u00fc\u00fcs", + "title": "Tunnel" } } } diff --git a/homeassistant/components/knx/translations/fr.json b/homeassistant/components/knx/translations/fr.json index 184a725b777..0102d025659 100644 --- a/homeassistant/components/knx/translations/fr.json +++ b/homeassistant/components/knx/translations/fr.json @@ -6,10 +6,8 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "file_not_found": "Le fichier `.knxkeys` sp\u00e9cifi\u00e9 n'a pas \u00e9t\u00e9 trouv\u00e9 dans config/.storage/knx/", "invalid_individual_address": "La valeur de l'adresse individuelle KNX ne correspond pas au mod\u00e8le.\n'area.line.device'", - "invalid_ip_address": "Adresse IPv4 non valide.", - "invalid_signature": "Le mot de passe pour d\u00e9chiffrer le fichier `.knxkeys` est erron\u00e9." + "invalid_ip_address": "Adresse IPv4 non valide." }, "step": { "connection_type": { @@ -82,10 +80,8 @@ "options": { "error": { "cannot_connect": "\u00c9chec de connexion", - "file_not_found": "Le fichier `.knxkeys` sp\u00e9cifi\u00e9 n'a pas \u00e9t\u00e9 trouv\u00e9 dans config/.storage/knx/", "invalid_individual_address": "La valeur de l'adresse individuelle KNX ne correspond pas au mod\u00e8le.\n'area.line.device'", - "invalid_ip_address": "Adresse IPv4 non valide.", - "invalid_signature": "Le mot de passe pour d\u00e9chiffrer le fichier `.knxkeys` est erron\u00e9." + "invalid_ip_address": "Adresse IPv4 non valide." }, "step": { "connection_type": { diff --git a/homeassistant/components/knx/translations/hu.json b/homeassistant/components/knx/translations/hu.json index 033ae3d84dd..485464a8b07 100644 --- a/homeassistant/components/knx/translations/hu.json +++ b/homeassistant/components/knx/translations/hu.json @@ -6,11 +6,13 @@ }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "file_not_found": "A megadott '.knxkeys' f\u00e1jl nem tal\u00e1lhat\u00f3 a config/.storage/knx/ el\u00e9r\u00e9si \u00fatvonalon.", "invalid_backbone_key": "\u00c9rv\u00e9nytelen gerinckulcs. 32 hexadecim\u00e1lis sz\u00e1m az elv\u00e1rt.", "invalid_individual_address": "Az \u00e9rt\u00e9k nem felel meg a KNX egyedi c\u00edm mint\u00e1j\u00e1nak.\n'area.line.device'", "invalid_ip_address": "\u00c9rv\u00e9nytelen IPv4-c\u00edm.", - "invalid_signature": "A '.knxkeys' f\u00e1jl visszafejt\u00e9s\u00e9hez haszn\u00e1lt jelsz\u00f3 helytelen.", + "keyfile_invalid_signature": "A '.knxkeys' f\u00e1jl visszafejt\u00e9s\u00e9hez haszn\u00e1lt jelsz\u00f3 helytelen.", + "keyfile_no_backbone_key": "A \"knxkeys\" f\u00e1jl nem tartalmaz gerinckulcsot a biztons\u00e1gos \u00fatv\u00e1laszt\u00e1shoz.", + "keyfile_no_tunnel_for_host": "A `.knxkeys` f\u00e1jl nem tartalmazza a `{host}` hiteles\u00edt\u0151 adatait.", + "keyfile_not_found": "A megadott '.knxkeys' f\u00e1jl nem tal\u00e1lhat\u00f3 a config/.storage/knx/ el\u00e9r\u00e9si \u00fatvonalon.", "no_router_discovered": "Nem tal\u00e1lhat\u00f3 KNXnet/IP \u00fatv\u00e1laszt\u00f3 a h\u00e1l\u00f3zaton.", "no_tunnel_discovered": "Nem tal\u00e1lhat\u00f3 KNX alag\u00fat-kiszolg\u00e1l\u00f3 a h\u00e1l\u00f3zaton.", "unsupported_tunnel_type": "A kiv\u00e1lasztott alag\u00fatt\u00edpust az \u00e1tj\u00e1r\u00f3 nem t\u00e1mogatja." @@ -20,7 +22,15 @@ "data": { "connection_type": "KNX csatlakoz\u00e1s t\u00edpusa" }, - "description": "K\u00e9rem, adja meg a KNX-kapcsolathoz haszn\u00e1land\u00f3 kapcsolatt\u00edpust. \n AUTOMATIKUS - Az integr\u00e1ci\u00f3 gondoskodik a KNX buszhoz val\u00f3 kapcsol\u00f3d\u00e1sr\u00f3l egy \u00e1tj\u00e1r\u00f3 keres\u00e9s elv\u00e9gz\u00e9s\u00e9vel. \n TUNNELING - Az integr\u00e1ci\u00f3 alag\u00faton kereszt\u00fcl csatlakozik a KNX buszhoz. \n ROUTING - Az integr\u00e1ci\u00f3 a KNX buszhoz \u00fatv\u00e1laszt\u00e1ssal csatlakozik." + "description": "K\u00e9rem, adja meg a KNX-kapcsolathoz haszn\u00e1land\u00f3 kapcsolatt\u00edpust. \n AUTOMATIKUS - Az integr\u00e1ci\u00f3 gondoskodik a KNX buszhoz val\u00f3 kapcsol\u00f3d\u00e1sr\u00f3l egy \u00e1tj\u00e1r\u00f3 keres\u00e9s elv\u00e9gz\u00e9s\u00e9vel. \n TUNNELING - Az integr\u00e1ci\u00f3 alag\u00faton kereszt\u00fcl csatlakozik a KNX buszhoz. \n ROUTING - Az integr\u00e1ci\u00f3 a KNX buszhoz \u00fatv\u00e1laszt\u00e1ssal csatlakozik.", + "title": "KNX kapcsolat" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "Az `Automatikus` az els\u0151 szabad alag\u00fatv\u00e9gpontot fogja haszn\u00e1lni." + }, + "description": "V\u00e1lassza ki a csatlakoz\u00e1shoz haszn\u00e1lt alagutat.", + "title": "Alag\u00fat v\u00e9gpont" }, "manual_tunnel": { "data": { @@ -36,7 +46,8 @@ "port": "A KNX/IP tunnel eszk\u00f6z portsz\u00e1ma.", "route_back": "Enged\u00e9lyezze, ha a KNXnet/IP alag\u00fatkiszolg\u00e1l\u00f3 NAT m\u00f6g\u00f6tt van. Csak UDP-kapcsolatokra vonatkozik." }, - "description": "Adja meg az alag\u00fatkezel\u0151 (tunneling) eszk\u00f6z csatlakoz\u00e1si adatait." + "description": "Adja meg az alag\u00fatkezel\u0151 (tunneling) eszk\u00f6z csatlakoz\u00e1si adatait.", + "title": "Alag\u00fat be\u00e1ll\u00edt\u00e1sok" }, "routing": { "data": { @@ -50,7 +61,8 @@ "individual_address": "A Home Assistant \u00e1ltal haszn\u00e1land\u00f3 KNX-c\u00edm, pl. \"0.0.4\".", "local_ip": "Az automatikus felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz hagyja \u00fcresen." }, - "description": "K\u00e9rem, konfigur\u00e1lja az \u00fatv\u00e1laszt\u00e1si (routing) be\u00e1ll\u00edt\u00e1sokat." + "description": "K\u00e9rem, konfigur\u00e1lja az \u00fatv\u00e1laszt\u00e1si (routing) be\u00e1ll\u00edt\u00e1sokat.", + "title": "\u00datv\u00e1laszt\u00e1s" }, "secure_key_source": { "description": "V\u00e1lassza ki, hogyan szeretn\u00e9 konfigur\u00e1lni az KNX/IP secure-t.", @@ -58,7 +70,8 @@ "secure_knxkeys": "IP secure kulcsokat tartalmaz\u00f3 '.knxkeys' f\u00e1jl haszn\u00e1lata", "secure_routing_manual": "IP biztons\u00e1gos gerinch\u00e1l\u00f3zati kulcs manu\u00e1lis konfigur\u00e1l\u00e1sa", "secure_tunnel_manual": "Biztons\u00e1gos IP-hiteles\u00edt\u0151 adatok manu\u00e1lis konfigur\u00e1l\u00e1sa" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -69,7 +82,8 @@ "knxkeys_filename": "A f\u00e1jl a `.storage/knx/` konfigur\u00e1ci\u00f3s k\u00f6nyvt\u00e1r\u00e1ban helyezend\u0151.\nHome Assistant oper\u00e1ci\u00f3s rendszer eset\u00e9n ez a k\u00f6vetkez\u0151 lenne: `/config/.storage/knx/`\nP\u00e9lda: \"my_project.knxkeys\".", "knxkeys_password": "Ez a be\u00e1ll\u00edt\u00e1s a f\u00e1jl ETS-b\u0151l t\u00f6rt\u00e9n\u0151 export\u00e1l\u00e1sakor t\u00f6rt\u00e9nt." }, - "description": "K\u00e9rem, adja meg a '.knxkeys' f\u00e1jl adatait." + "description": "K\u00e9rem, adja meg a '.knxkeys' f\u00e1jl adatait.", + "title": "Kulcsf\u00e1jl" }, "secure_routing_manual": { "data": { @@ -80,7 +94,8 @@ "backbone_key": "Megtekinthet\u0151 egy ETS projekt 'Biztons\u00e1g' jelent\u00e9s\u00e9ben. P\u00e9ld\u00e1ul '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Az alap\u00e9rtelmezett \u00e9rt\u00e9k 1000." }, - "description": "K\u00e9rem, adja meg az IP secure adatokat." + "description": "K\u00e9rem, adja meg az IP secure adatokat.", + "title": "Biztons\u00e1gos \u00fatv\u00e1laszt\u00e1s" }, "secure_tunnel_manual": { "data": { @@ -93,24 +108,28 @@ "user_id": "Ez gyakran a tunnel sz\u00e1ma +1. Teh\u00e1t a \"Tunnel 2\" felhaszn\u00e1l\u00f3i azonos\u00edt\u00f3ja \"3\".", "user_password": "Jelsz\u00f3 az adott tunnelhez, amely a tunnel \u201eProperties\u201d panelj\u00e9n van be\u00e1ll\u00edtva az ETS-ben." }, - "description": "K\u00e9rem, adja meg az IP secure adatokat." + "description": "K\u00e9rem, adja meg az IP secure adatokat.", + "title": "Biztons\u00e1gos alag\u00fat" }, "tunnel": { "data": { "gateway": "KNX alag\u00fat (tunnel) kapcsolat" }, - "description": "V\u00e1lasszon egy \u00e1tj\u00e1r\u00f3t a list\u00e1b\u00f3l." + "description": "V\u00e1lasszon egy \u00e1tj\u00e1r\u00f3t a list\u00e1b\u00f3l.", + "title": "Alag\u00fat" } } }, "options": { "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "file_not_found": "A megadott '.knxkeys' f\u00e1jl nem tal\u00e1lhat\u00f3 a config/.storage/knx/ el\u00e9r\u00e9si \u00fatvonalon.", "invalid_backbone_key": "\u00c9rv\u00e9nytelen gerinckulcs. 32 hexadecim\u00e1lis sz\u00e1m az elv\u00e1rt.", "invalid_individual_address": "Az \u00e9rt\u00e9k nem felel meg a KNX egyedi c\u00edm mint\u00e1j\u00e1nak.\n'area.line.device'", "invalid_ip_address": "\u00c9rv\u00e9nytelen IPv4-c\u00edm.", - "invalid_signature": "A '.knxkeys' f\u00e1jl visszafejt\u00e9s\u00e9hez haszn\u00e1lt jelsz\u00f3 helytelen.", + "keyfile_invalid_signature": "A '.knxkeys' f\u00e1jl visszafejt\u00e9s\u00e9hez haszn\u00e1lt jelsz\u00f3 helytelen.", + "keyfile_no_backbone_key": "A \"knxkeys\" f\u00e1jl nem tartalmaz gerinckulcsot a biztons\u00e1gos \u00fatv\u00e1laszt\u00e1shoz.", + "keyfile_no_tunnel_for_host": "A `.knxkeys` f\u00e1jl nem tartalmazza a `{host}` hiteles\u00edt\u0151 adatait.", + "keyfile_not_found": "A megadott '.knxkeys' f\u00e1jl nem tal\u00e1lhat\u00f3 a config/.storage/knx/ el\u00e9r\u00e9si \u00fatvonalon.", "no_router_discovered": "Nem tal\u00e1lhat\u00f3 KNXnet/IP \u00fatv\u00e1laszt\u00f3 a h\u00e1l\u00f3zaton.", "no_tunnel_discovered": "Nem tal\u00e1lhat\u00f3 KNX alag\u00fat-kiszolg\u00e1l\u00f3 a h\u00e1l\u00f3zaton.", "unsupported_tunnel_type": "A kiv\u00e1lasztott alag\u00fatt\u00edpust az \u00e1tj\u00e1r\u00f3 nem t\u00e1mogatja." @@ -124,13 +143,22 @@ "data_description": { "rate_limit": "Maxim\u00e1lisan kimen\u0151 \u00fczenet m\u00e1sodpercenk\u00e9nt. 0 a kikapcsol\u00e1shoz.\nAj\u00e1nlott: 0, vagy 20 \u00e9s 40 k\u00f6z\u00f6tt", "state_updater": "Alap\u00e9rtelmezett be\u00e1ll\u00edt\u00e1s a KNX busz \u00e1llapotainak olvas\u00e1s\u00e1hoz. Ha le va tiltva, Home Assistant nem fog akt\u00edvan lek\u00e9rdezni egys\u00e9g\u00e1llapotokat a KNX buszr\u00f3l. Fel\u00fclb\u00edr\u00e1lhat\u00f3 a `sync_state` entit\u00e1s opci\u00f3kkal." - } + }, + "title": "Kommunik\u00e1ci\u00f3s be\u00e1ll\u00edt\u00e1sok" }, "connection_type": { "data": { "connection_type": "KNX csatlakoz\u00e1s t\u00edpusa" }, - "description": "K\u00e9rem, adja meg a KNX-kapcsolathoz haszn\u00e1land\u00f3 kapcsolatt\u00edpust. \n AUTOMATIKUS - Az integr\u00e1ci\u00f3 gondoskodik a KNX buszhoz val\u00f3 kapcsol\u00f3d\u00e1sr\u00f3l egy \u00e1tj\u00e1r\u00f3 keres\u00e9s elv\u00e9gz\u00e9s\u00e9vel. \n TUNNELING - Az integr\u00e1ci\u00f3 alag\u00faton kereszt\u00fcl csatlakozik a KNX buszhoz. \n ROUTING - Az integr\u00e1ci\u00f3 a KNX buszhoz \u00fatv\u00e1laszt\u00e1ssal csatlakozik." + "description": "K\u00e9rem, adja meg a KNX-kapcsolathoz haszn\u00e1land\u00f3 kapcsolatt\u00edpust. \n AUTOMATIKUS - Az integr\u00e1ci\u00f3 gondoskodik a KNX buszhoz val\u00f3 kapcsol\u00f3d\u00e1sr\u00f3l egy \u00e1tj\u00e1r\u00f3 keres\u00e9s elv\u00e9gz\u00e9s\u00e9vel. \n TUNNELING - Az integr\u00e1ci\u00f3 alag\u00faton kereszt\u00fcl csatlakozik a KNX buszhoz. \n ROUTING - Az integr\u00e1ci\u00f3 a KNX buszhoz \u00fatv\u00e1laszt\u00e1ssal csatlakozik.", + "title": "KNX kapcsolat" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "Az `Automatikus` az els\u0151 szabad alag\u00fatv\u00e9gpontot fogja haszn\u00e1lni." + }, + "description": "V\u00e1lassza ki a csatlakoz\u00e1shoz haszn\u00e1lt alagutat.", + "title": "Alag\u00fat v\u00e9gpont" }, "manual_tunnel": { "data": { @@ -146,13 +174,15 @@ "port": "A KNX/IP tunnel eszk\u00f6z portsz\u00e1ma.", "route_back": "Enged\u00e9lyezze, ha a KNXnet/IP alag\u00fatkiszolg\u00e1l\u00f3 NAT m\u00f6g\u00f6tt van. Csak UDP-kapcsolatokra vonatkozik." }, - "description": "Adja meg az alag\u00fatkezel\u0151 (tunneling) eszk\u00f6z csatlakoz\u00e1si adatait." + "description": "Adja meg az alag\u00fatkezel\u0151 (tunneling) eszk\u00f6z csatlakoz\u00e1si adatait.", + "title": "Alag\u00fat be\u00e1ll\u00edt\u00e1sok" }, "options_init": { "menu_options": { "communication_settings": "Kommunik\u00e1ci\u00f3s be\u00e1ll\u00edt\u00e1sok", "connection_type": "KNX interf\u00e9sz konfigur\u00e1l\u00e1sa" - } + }, + "title": "KNX be\u00e1ll\u00edt\u00e1sok" }, "routing": { "data": { @@ -166,7 +196,8 @@ "individual_address": "A Home Assistant \u00e1ltal haszn\u00e1land\u00f3 KNX-c\u00edm, pl. \"0.0.4\".", "local_ip": "Az automatikus felder\u00edt\u00e9s haszn\u00e1lat\u00e1hoz hagyja \u00fcresen." }, - "description": "K\u00e9rem, konfigur\u00e1lja az \u00fatv\u00e1laszt\u00e1si (routing) be\u00e1ll\u00edt\u00e1sokat." + "description": "K\u00e9rem, konfigur\u00e1lja az \u00fatv\u00e1laszt\u00e1si (routing) be\u00e1ll\u00edt\u00e1sokat.", + "title": "\u00datv\u00e1laszt\u00e1s" }, "secure_key_source": { "description": "V\u00e1lassza ki, hogyan szeretn\u00e9 konfigur\u00e1lni az KNX/IP secure-t.", @@ -174,7 +205,8 @@ "secure_knxkeys": "IP secure kulcsokat tartalmaz\u00f3 '.knxkeys' f\u00e1jl haszn\u00e1lata", "secure_routing_manual": "IP biztons\u00e1gos gerinch\u00e1l\u00f3zati kulcs manu\u00e1lis konfigur\u00e1l\u00e1sa", "secure_tunnel_manual": "Biztons\u00e1gos IP-hiteles\u00edt\u0151 adatok manu\u00e1lis konfigur\u00e1l\u00e1sa" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +217,8 @@ "knxkeys_filename": "A f\u00e1jl a `.storage/knx/` konfigur\u00e1ci\u00f3s k\u00f6nyvt\u00e1r\u00e1ban helyezend\u0151.\nHome Assistant oper\u00e1ci\u00f3s rendszer eset\u00e9n ez a k\u00f6vetkez\u0151 lenne: `/config/.storage/knx/`\nP\u00e9lda: \"my_project.knxkeys\".", "knxkeys_password": "Ez a be\u00e1ll\u00edt\u00e1s a f\u00e1jl ETS-b\u0151l t\u00f6rt\u00e9n\u0151 export\u00e1l\u00e1sakor t\u00f6rt\u00e9nt." }, - "description": "K\u00e9rem, adja meg a '.knxkeys' f\u00e1jl adatait." + "description": "K\u00e9rem, adja meg a '.knxkeys' f\u00e1jl adatait.", + "title": "Kulcsf\u00e1jl" }, "secure_routing_manual": { "data": { @@ -196,7 +229,8 @@ "backbone_key": "Megtekinthet\u0151 egy ETS projekt 'Biztons\u00e1g' jelent\u00e9s\u00e9ben. P\u00e9ld\u00e1ul '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Az alap\u00e9rtelmezett \u00e9rt\u00e9k 1000." }, - "description": "K\u00e9rem, adja meg az IP secure adatokat." + "description": "K\u00e9rem, adja meg az IP secure adatokat.", + "title": "Biztons\u00e1gos \u00fatv\u00e1laszt\u00e1s" }, "secure_tunnel_manual": { "data": { @@ -209,13 +243,15 @@ "user_id": "Ez gyakran a tunnel sz\u00e1ma +1. Teh\u00e1t a \"Tunnel 2\" felhaszn\u00e1l\u00f3i azonos\u00edt\u00f3ja \"3\".", "user_password": "Jelsz\u00f3 az adott tunnelhez, amely a tunnel \u201eProperties\u201d panelj\u00e9n van be\u00e1ll\u00edtva az ETS-ben." }, - "description": "K\u00e9rem, adja meg az IP secure adatokat." + "description": "K\u00e9rem, adja meg az IP secure adatokat.", + "title": "Biztons\u00e1gos alag\u00fat" }, "tunnel": { "data": { "gateway": "KNX alag\u00fat (tunnel) kapcsolat" }, - "description": "V\u00e1lasszon egy \u00e1tj\u00e1r\u00f3t a list\u00e1b\u00f3l." + "description": "V\u00e1lasszon egy \u00e1tj\u00e1r\u00f3t a list\u00e1b\u00f3l.", + "title": "Alag\u00fat" } } } diff --git a/homeassistant/components/knx/translations/id.json b/homeassistant/components/knx/translations/id.json index 4da3f26e148..1f145a1a7d2 100644 --- a/homeassistant/components/knx/translations/id.json +++ b/homeassistant/components/knx/translations/id.json @@ -6,11 +6,13 @@ }, "error": { "cannot_connect": "Gagal terhubung", - "file_not_found": "File `.knxkeys` yang ditentukan tidak ditemukan di jalur config/.storage/knx/", "invalid_backbone_key": "Kunci backbone tidak valid. Diharapkan 32 angka heksadesimal.", "invalid_individual_address": "Nilai tidak cocok dengan pola untuk alamat individual KNX.\n'area.line.device'", "invalid_ip_address": "Alamat IPv4 tidak valid", - "invalid_signature": "Kata sandi untuk mendekripsi file `.knxkeys` salah.", + "keyfile_invalid_signature": "Kata sandi untuk mendekripsi file `.knxkeys` salah.", + "keyfile_no_backbone_key": "File `.knxkeys` tidak berisi kunci backbone untuk perutean yang aman.", + "keyfile_no_tunnel_for_host": "File `.knxkeys` tidak berisi kredensial untuk host `{host}`.", + "keyfile_not_found": "File `.knxkeys` yang ditentukan tidak ditemukan di jalur config/.storage/knx/", "no_router_discovered": "Tidak ada router KNXnet/IP yang ditemukan di jaringan.", "no_tunnel_discovered": "Tidak dapat menemukan server tunneling KNX di jaringan Anda.", "unsupported_tunnel_type": "Jenis tunneling yang dipilih tidak didukung oleh gateway." @@ -20,7 +22,15 @@ "data": { "connection_type": "Jenis Koneksi KNX" }, - "description": "Masukkan jenis koneksi yang harus kami gunakan untuk koneksi KNX Anda. \nOTOMATIS - Integrasi melakukan konektivitas ke bus KNX Anda dengan melakukan pemindaian gateway. \nTUNNELING - Integrasi akan terhubung ke bus KNX Anda melalui tunneling. \nROUTING - Integrasi akan terhubung ke bus KNX Anda melalui routing." + "description": "Masukkan jenis koneksi yang harus kami gunakan untuk koneksi KNX Anda. \nOTOMATIS - Integrasi melakukan konektivitas ke bus KNX Anda dengan melakukan pemindaian gateway. \nTUNNELING - Integrasi akan terhubung ke bus KNX Anda melalui tunneling. \nROUTING - Integrasi akan terhubung ke bus KNX Anda melalui routing.", + "title": "Koneksi KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatic` akan menggunakan titik akhir tunnel pertama yang bebas." + }, + "description": "Pilih tunnel yang digunakan untuk koneksi.", + "title": "Titik akhir tunnel" }, "manual_tunnel": { "data": { @@ -36,7 +46,8 @@ "port": "Port perangkat tunneling KNX/IP.", "route_back": "Aktifkan jika server tunneling KNXnet/IP Anda berada di belakang NAT. Hanya berlaku untuk koneksi UDP." }, - "description": "Masukkan informasi koneksi untuk perangkat tunneling Anda." + "description": "Masukkan informasi koneksi untuk perangkat tunneling Anda.", + "title": "Pengaturan tunnel" }, "routing": { "data": { @@ -50,7 +61,8 @@ "individual_address": "Alamat KNX yang akan digunakan oleh Home Assistant, misalnya `0.0.4`", "local_ip": "Kosongkan untuk menggunakan penemuan otomatis." }, - "description": "Konfigurasikan opsi routing." + "description": "Konfigurasikan opsi routing.", + "title": "Perutean" }, "secure_key_source": { "description": "Pilih cara Anda ingin mengonfigurasi KNX/IP Secure.", @@ -58,7 +70,8 @@ "secure_knxkeys": "Gunakan file `.knxkeys` yang berisi kunci aman IP", "secure_routing_manual": "Konfigurasikan kunci backbone aman IP secara manual", "secure_tunnel_manual": "Konfigurasikan kredensial aman IP secara manual" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -69,7 +82,8 @@ "knxkeys_filename": "File diharapkan dapat ditemukan di direktori konfigurasi Anda di `.storage/knx/`.\nDi Home Assistant OS ini akan menjadi `/config/.storage/knx/`\nContoh: `proyek_saya.knxkeys`", "knxkeys_password": "Ini disetel saat mengekspor file dari ETS." }, - "description": "Masukkan informasi untuk file `.knxkeys` Anda." + "description": "Masukkan informasi untuk file `.knxkeys` Anda.", + "title": "File kunci" }, "secure_routing_manual": { "data": { @@ -80,7 +94,8 @@ "backbone_key": "Dapat dilihat dalam laporan 'Security' dari proyek ETS. Mis. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Bawaannya bernilai 1000." }, - "description": "Masukkan informasi IP aman Anda." + "description": "Masukkan informasi IP aman Anda.", + "title": "Perutean aman" }, "secure_tunnel_manual": { "data": { @@ -93,24 +108,28 @@ "user_id": "Ini sering kali merupakan tunnel nomor +1. Jadi 'Tunnel 2' akan memiliki User-ID '3'.", "user_password": "Kata sandi untuk koneksi tunnel tertentu yang diatur di panel 'Properties' tunnel di ETS." }, - "description": "Masukkan informasi IP aman Anda." + "description": "Masukkan informasi IP aman Anda.", + "title": "Tunnel yang aman" }, "tunnel": { "data": { "gateway": "Koneksi Tunnel KNX" }, - "description": "Pilih gateway dari daftar." + "description": "Pilih gateway dari daftar.", + "title": "Tunnel" } } }, "options": { "error": { "cannot_connect": "Gagal terhubung", - "file_not_found": "File `.knxkeys` yang ditentukan tidak ditemukan di jalur config/.storage/knx/", "invalid_backbone_key": "Kunci backbone tidak valid. Diharapkan 32 angka heksadesimal.", "invalid_individual_address": "Nilai tidak cocok dengan pola untuk alamat individual KNX.\n'area.line.device'", "invalid_ip_address": "Alamat IPv4 tidak valid", - "invalid_signature": "Kata sandi untuk mendekripsi file `.knxkeys` salah.", + "keyfile_invalid_signature": "Kata sandi untuk mendekripsi file `.knxkeys` salah.", + "keyfile_no_backbone_key": "File `.knxkeys` tidak berisi kunci backbone untuk perutean yang aman.", + "keyfile_no_tunnel_for_host": "File `.knxkeys` tidak berisi kredensial untuk host `{host}`.", + "keyfile_not_found": "File `.knxkeys` yang ditentukan tidak ditemukan di jalur config/.storage/knx/", "no_router_discovered": "Tidak ada router KNXnet/IP yang ditemukan di jaringan.", "no_tunnel_discovered": "Tidak dapat menemukan server tunneling KNX di jaringan Anda.", "unsupported_tunnel_type": "Jenis tunneling yang dipilih tidak didukung oleh gateway." @@ -124,13 +143,22 @@ "data_description": { "rate_limit": "Telegram keluar maksimum per detik. `0` untuk menonaktifkan batas. Direkomendasikan: 0 atau 20 hingga 40", "state_updater": "Menyetel default untuk status pembacaan KNX Bus. Saat dinonaktifkan, Home Assistant tidak akan secara aktif mengambil status entitas dari KNX Bus. Hal ini bisa ditimpa dengan opsi entitas `sync_state`." - } + }, + "title": "Pengaturan komunikasi" }, "connection_type": { "data": { "connection_type": "Jenis Koneksi KNX" }, - "description": "Masukkan jenis koneksi yang harus kami gunakan untuk koneksi KNX Anda. \nOTOMATIS - Integrasi melakukan konektivitas ke bus KNX Anda dengan melakukan pemindaian gateway. \nTUNNELING - Integrasi akan terhubung ke bus KNX Anda melalui tunneling. \nROUTING - Integrasi akan terhubung ke bus KNX Anda melalui routing." + "description": "Masukkan jenis koneksi yang harus kami gunakan untuk koneksi KNX Anda. \nOTOMATIS - Integrasi melakukan konektivitas ke bus KNX Anda dengan melakukan pemindaian gateway. \nTUNNELING - Integrasi akan terhubung ke bus KNX Anda melalui tunneling. \nROUTING - Integrasi akan terhubung ke bus KNX Anda melalui routing.", + "title": "Koneksi KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatic` akan menggunakan titik akhir tunnel pertama yang bebas." + }, + "description": "Pilih tunnel yang digunakan untuk koneksi.", + "title": "Titik akhir tunnel" }, "manual_tunnel": { "data": { @@ -146,13 +174,15 @@ "port": "Port perangkat tunneling KNX/IP.", "route_back": "Aktifkan jika server tunneling KNXnet/IP Anda berada di belakang NAT. Hanya berlaku untuk koneksi UDP." }, - "description": "Masukkan informasi koneksi untuk perangkat tunneling Anda." + "description": "Masukkan informasi koneksi untuk perangkat tunneling Anda.", + "title": "Pengaturan tunnel" }, "options_init": { "menu_options": { "communication_settings": "Pengaturan komunikasi", "connection_type": "Konfigurasikan antarmuka KNX" - } + }, + "title": "Pengaturan KNX" }, "routing": { "data": { @@ -166,7 +196,8 @@ "individual_address": "Alamat KNX yang akan digunakan oleh Home Assistant, misalnya `0.0.4`", "local_ip": "Kosongkan untuk menggunakan penemuan otomatis." }, - "description": "Konfigurasikan opsi routing." + "description": "Konfigurasikan opsi routing.", + "title": "Perutean" }, "secure_key_source": { "description": "Pilih cara Anda ingin mengonfigurasi KNX/IP Secure.", @@ -174,7 +205,8 @@ "secure_knxkeys": "Gunakan file `.knxkeys` yang berisi kunci aman IP", "secure_routing_manual": "Konfigurasikan kunci backbone aman IP secara manual", "secure_tunnel_manual": "Konfigurasikan kredensial aman IP secara manual" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +217,8 @@ "knxkeys_filename": "File diharapkan dapat ditemukan di direktori konfigurasi Anda di `.storage/knx/`.\nDi Home Assistant OS ini akan menjadi `/config/.storage/knx/`\nContoh: `proyek_saya.knxkeys`", "knxkeys_password": "Ini disetel saat mengekspor file dari ETS." }, - "description": "Masukkan informasi untuk file `.knxkeys` Anda." + "description": "Masukkan informasi untuk file `.knxkeys` Anda.", + "title": "File kunci" }, "secure_routing_manual": { "data": { @@ -196,7 +229,8 @@ "backbone_key": "Dapat dilihat dalam laporan 'Security' dari proyek ETS. Mis. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Bawaannya bernilai 1000." }, - "description": "Masukkan informasi IP aman Anda." + "description": "Masukkan informasi IP aman Anda.", + "title": "Perutean aman" }, "secure_tunnel_manual": { "data": { @@ -209,13 +243,15 @@ "user_id": "Ini sering kali merupakan tunnel nomor +1. Jadi 'Tunnel 2' akan memiliki User-ID '3'.", "user_password": "Kata sandi untuk koneksi tunnel tertentu yang diatur di panel 'Properties' tunnel di ETS." }, - "description": "Masukkan informasi IP aman Anda." + "description": "Masukkan informasi IP aman Anda.", + "title": "Tunnel yang aman" }, "tunnel": { "data": { "gateway": "Koneksi Tunnel KNX" }, - "description": "Pilih gateway dari daftar." + "description": "Pilih gateway dari daftar.", + "title": "Tunnel" } } } diff --git a/homeassistant/components/knx/translations/it.json b/homeassistant/components/knx/translations/it.json index b70b8b06c80..9193eb9401d 100644 --- a/homeassistant/components/knx/translations/it.json +++ b/homeassistant/components/knx/translations/it.json @@ -6,11 +6,13 @@ }, "error": { "cannot_connect": "Impossibile connettersi", - "file_not_found": "Il file `.knxkeys` specificato non \u00e8 stato trovato nel percorso config/.storage/knx/", "invalid_backbone_key": "Chiave backbone non valida. Previsti 32 numeri esadecimali.", "invalid_individual_address": "Il valore non corrisponde al modello per l'indirizzo individuale KNX. 'area.line.device'", "invalid_ip_address": "Indirizzo IPv4 non valido.", - "invalid_signature": "La password per decifrare il file `.knxkeys` \u00e8 errata.", + "keyfile_invalid_signature": "La password per decifrare il file `.knxkeys` \u00e8 errata.", + "keyfile_no_backbone_key": "Il file `.knxkeys` non contiene una chiave backbone per l'instradamento sicuro.", + "keyfile_no_tunnel_for_host": "Il file `.knxkeys` non contiene le credenziali per l'host `{host}`.", + "keyfile_not_found": "Il file `.knxkeys` specificato non \u00e8 stato trovato nel percorso config/.storage/knx/", "no_router_discovered": "Non \u00e8 stato rilevato alcun router KNXnet/IP nella rete.", "no_tunnel_discovered": "Impossibile trovare un server di tunneling KNX sulla rete.", "unsupported_tunnel_type": "Il tipo di tunnel selezionato non \u00e8 supportato dal gateway." @@ -20,7 +22,15 @@ "data": { "connection_type": "Tipo di connessione KNX" }, - "description": "Inserisci il tipo di connessione che dovremmo usare per la tua connessione KNX. \n AUTOMATICO - L'integrazione si occupa della connettivit\u00e0 al tuo bus KNX eseguendo una scansione del gateway. \n TUNNELING - L'integrazione si collegher\u00e0 al tuo bus KNX tramite tunneling. \n ROUTING - L'integrazione si connetter\u00e0 al tuo bus KNX tramite instradamento." + "description": "Inserisci il tipo di connessione che dovremmo usare per la tua connessione KNX. \n AUTOMATICO - L'integrazione si occupa della connettivit\u00e0 al tuo bus KNX eseguendo una scansione del gateway. \n TUNNELING - L'integrazione si collegher\u00e0 al tuo bus KNX tramite tunneling. \n ROUTING - L'integrazione si connetter\u00e0 al tuo bus KNX tramite instradamento.", + "title": "Connessione KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatico` utilizzer\u00e0 il primo punto finale di tunnel libero." + }, + "description": "Selezionare il tunnel utilizzato per la connessione.", + "title": "Punto finale del tunnel" }, "manual_tunnel": { "data": { @@ -36,7 +46,8 @@ "port": "Porta del dispositivo di tunneling KNX/IP.", "route_back": "Abilitare se il server di tunneling KNXnet/IP \u00e8 protetto da NAT. Si applica solo alle connessioni UDP." }, - "description": "Inserisci le informazioni di connessione del tuo dispositivo di tunneling." + "description": "Inserisci le informazioni di connessione del tuo dispositivo di tunneling.", + "title": "Impostazioni del tunnel" }, "routing": { "data": { @@ -50,7 +61,8 @@ "individual_address": "Indirizzo KNX che deve essere utilizzato da Home Assistant, ad es. `0.0.4`", "local_ip": "Lascia vuoto per usare il rilevamento automatico." }, - "description": "Configura le opzioni di instradamento." + "description": "Configura le opzioni di instradamento.", + "title": "Instradamento" }, "secure_key_source": { "description": "Seleziona come vuoi configurare KNX/IP Secure.", @@ -58,7 +70,8 @@ "secure_knxkeys": "Usa un file `.knxkeys` contenente le chiavi di sicurezza IP", "secure_routing_manual": "Configurare manualmente la chiave backbone IP secure", "secure_tunnel_manual": "Configura manualmente le credenziali di protezione IP" - } + }, + "title": "Sicurezza IP KNX" }, "secure_knxkeys": { "data": { @@ -69,7 +82,8 @@ "knxkeys_filename": "Il file dovrebbe trovarsi nella directory di configurazione in '.storage/knx/'.\nNel sistema operativo Home Assistant questa sarebbe '/config/.storage/knx/'\nEsempio: 'my_project.knxkeys'", "knxkeys_password": "Questo \u00e8 stato impostato durante l'esportazione del file da ETS." }, - "description": "Inserisci le informazioni per il tuo file `.knxkeys`." + "description": "Inserisci le informazioni per il tuo file `.knxkeys`.", + "title": "File chiave" }, "secure_routing_manual": { "data": { @@ -80,7 +94,8 @@ "backbone_key": "Pu\u00f2 essere visualizzato nel rapporto \"Sicurezza\" di un progetto ETS. Eg. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Il valore predefinito \u00e8 1000." }, - "description": "Inserisci le tue informazioni di sicurezza IP." + "description": "Inserisci le tue informazioni di sicurezza IP.", + "title": "Instradamento sicuro" }, "secure_tunnel_manual": { "data": { @@ -93,24 +108,28 @@ "user_id": "Questo \u00e8 spesso il tunnel numero +1. Quindi \"Tunnel 2\" avrebbe l'ID utente \"3\".", "user_password": "Password per la connessione specifica del tunnel impostata nel pannello 'Propriet\u00e0' del tunnel in ETS." }, - "description": "Inserisci le tue informazioni di sicurezza IP." + "description": "Inserisci le tue informazioni di sicurezza IP.", + "title": "Tunnelling sicuro" }, "tunnel": { "data": { "gateway": "Connessione tunnel KNX" }, - "description": "Seleziona un gateway dall'elenco." + "description": "Seleziona un gateway dall'elenco.", + "title": "Tunnel" } } }, "options": { "error": { "cannot_connect": "Impossibile connettersi", - "file_not_found": "Il file `.knxkeys` specificato non \u00e8 stato trovato nel percorso config/.storage/knx/", "invalid_backbone_key": "Chiave backbone non valida. Previsti 32 numeri esadecimali.", "invalid_individual_address": "Il valore non corrisponde al modello per l'indirizzo individuale KNX. 'area.line.device'", "invalid_ip_address": "Indirizzo IPv4 non valido.", - "invalid_signature": "La password per decifrare il file `.knxkeys` \u00e8 errata.", + "keyfile_invalid_signature": "La password per decifrare il file `.knxkeys` \u00e8 errata.", + "keyfile_no_backbone_key": "Il file `.knxkeys` non contiene una chiave backbone per l'instradamento sicuro.", + "keyfile_no_tunnel_for_host": "Il file `.knxkeys` non contiene le credenziali per l'host `{host}`.", + "keyfile_not_found": "Il file `.knxkeys` specificato non \u00e8 stato trovato nel percorso config/.storage/knx/", "no_router_discovered": "Non \u00e8 stato rilevato alcun router KNXnet/IP nella rete.", "no_tunnel_discovered": "Impossibile trovare un server di tunneling KNX sulla rete.", "unsupported_tunnel_type": "Il tipo di tunnel selezionato non \u00e8 supportato dal gateway." @@ -124,13 +143,22 @@ "data_description": { "rate_limit": "Numero massimo di telegrammi in uscita al secondo.\n'0' per disabilitare il limite. Consigliato: 0 o da 20 a 40", "state_updater": "Impostazione predefinita per la lettura degli stati dal bus KNX. Quando disabilitato, Home Assistant non recuperer\u00e0 attivamente gli stati delle entit\u00e0 dal bus KNX. Pu\u00f2 essere sovrascritto dalle opzioni dell'entit\u00e0 `sync_state`." - } + }, + "title": "Impostazioni di comunicazione" }, "connection_type": { "data": { "connection_type": "Tipo di connessione KNX" }, - "description": "Inserisci il tipo di connessione che dovremmo usare per la tua connessione KNX. \n AUTOMATICO - L'integrazione si occupa della connettivit\u00e0 al tuo bus KNX eseguendo una scansione del gateway. \n TUNNELING - L'integrazione si collegher\u00e0 al tuo bus KNX tramite tunneling. \n ROUTING - L'integrazione si connetter\u00e0 al tuo bus KNX tramite instradamento." + "description": "Inserisci il tipo di connessione che dovremmo usare per la tua connessione KNX. \n AUTOMATICO - L'integrazione si occupa della connettivit\u00e0 al tuo bus KNX eseguendo una scansione del gateway. \n TUNNELING - L'integrazione si collegher\u00e0 al tuo bus KNX tramite tunneling. \n ROUTING - L'integrazione si connetter\u00e0 al tuo bus KNX tramite instradamento.", + "title": "Connessione KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatico` utilizzer\u00e0 il primo punto finale di tunnel libero." + }, + "description": "Selezionare il tunnel utilizzato per la connessione.", + "title": "Punto finale del tunnel" }, "manual_tunnel": { "data": { @@ -146,13 +174,15 @@ "port": "Porta del dispositivo di tunneling KNX/IP.", "route_back": "Abilitare se il server di tunneling KNXnet/IP \u00e8 protetto da NAT. Si applica solo alle connessioni UDP." }, - "description": "Inserisci le informazioni di connessione del tuo dispositivo di tunneling." + "description": "Inserisci le informazioni di connessione del tuo dispositivo di tunneling.", + "title": "Impostazioni del tunnel" }, "options_init": { "menu_options": { "communication_settings": "Impostazioni di comunicazione", "connection_type": "Configura interfaccia KNX" - } + }, + "title": "Impostazioni KNX" }, "routing": { "data": { @@ -166,7 +196,8 @@ "individual_address": "Indirizzo KNX che deve essere utilizzato da Home Assistant, ad es. `0.0.4`", "local_ip": "Lascia vuoto per usare il rilevamento automatico." }, - "description": "Configura le opzioni di instradamento." + "description": "Configura le opzioni di instradamento.", + "title": "Instradamento" }, "secure_key_source": { "description": "Seleziona come vuoi configurare KNX/IP Secure.", @@ -174,7 +205,8 @@ "secure_knxkeys": "Usa un file `.knxkeys` contenente le chiavi di sicurezza IP", "secure_routing_manual": "Configurare manualmente la chiave backbone IP secure", "secure_tunnel_manual": "Configura manualmente le credenziali di protezione IP" - } + }, + "title": "Sicurezza IP KNX" }, "secure_knxkeys": { "data": { @@ -185,7 +217,8 @@ "knxkeys_filename": "Il file dovrebbe trovarsi nella directory di configurazione in '.storage/knx/'.\nNel sistema operativo Home Assistant questa sarebbe '/config/.storage/knx/'\nEsempio: 'my_project.knxkeys'", "knxkeys_password": "Questo \u00e8 stato impostato durante l'esportazione del file da ETS." }, - "description": "Inserisci le informazioni per il tuo file `.knxkeys`." + "description": "Inserisci le informazioni per il tuo file `.knxkeys`.", + "title": "File chiave" }, "secure_routing_manual": { "data": { @@ -196,7 +229,8 @@ "backbone_key": "Pu\u00f2 essere visualizzato nel rapporto \"Sicurezza\" di un progetto ETS. Eg. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Il valore predefinito \u00e8 1000." }, - "description": "Inserisci le tue informazioni di sicurezza IP." + "description": "Inserisci le tue informazioni di sicurezza IP.", + "title": "Instradamento sicuro" }, "secure_tunnel_manual": { "data": { @@ -209,13 +243,15 @@ "user_id": "Questo \u00e8 spesso il tunnel numero +1. Quindi \"Tunnel 2\" avrebbe l'ID utente \"3\".", "user_password": "Password per la connessione specifica del tunnel impostata nel pannello 'Propriet\u00e0' del tunnel in ETS." }, - "description": "Inserisci le tue informazioni di sicurezza IP." + "description": "Inserisci le tue informazioni di sicurezza IP.", + "title": "Tunnelling sicuro" }, "tunnel": { "data": { "gateway": "Connessione tunnel KNX" }, - "description": "Seleziona un gateway dall'elenco." + "description": "Seleziona un gateway dall'elenco.", + "title": "Tunnel" } } } diff --git a/homeassistant/components/knx/translations/ja.json b/homeassistant/components/knx/translations/ja.json index 42aea352aa7..319c8504522 100644 --- a/homeassistant/components/knx/translations/ja.json +++ b/homeassistant/components/knx/translations/ja.json @@ -6,10 +6,8 @@ }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", - "file_not_found": "\u6307\u5b9a\u3055\u308c\u305f'.knxkeys'\u30d5\u30a1\u30a4\u30eb\u304c\u3001\u30d1\u30b9: config/.storage/knx/ \u306b\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f", "invalid_individual_address": "\u5024\u304cKNX\u500b\u5225\u30a2\u30c9\u30ec\u30b9\u306e\u30d1\u30bf\u30fc\u30f3\u3068\u4e00\u81f4\u3057\u307e\u305b\u3093\u3002\n'area.line.device'", - "invalid_ip_address": "IPv4\u30a2\u30c9\u30ec\u30b9\u304c\u7121\u52b9\u3067\u3059\u3002", - "invalid_signature": "'.knxkeys'\u30d5\u30a1\u30a4\u30eb\u3092\u5fa9\u53f7\u5316\u3059\u308b\u305f\u3081\u306e\u30d1\u30b9\u30ef\u30fc\u30c9\u304c\u9593\u9055\u3063\u3066\u3044\u307e\u3059\u3002" + "invalid_ip_address": "IPv4\u30a2\u30c9\u30ec\u30b9\u304c\u7121\u52b9\u3067\u3059\u3002" }, "step": { "manual_tunnel": { diff --git a/homeassistant/components/knx/translations/nl.json b/homeassistant/components/knx/translations/nl.json index 72d9e6bf1d8..8fccfe659a0 100644 --- a/homeassistant/components/knx/translations/nl.json +++ b/homeassistant/components/knx/translations/nl.json @@ -6,12 +6,13 @@ }, "error": { "cannot_connect": "Kan geen verbinding maken", - "file_not_found": "Het opgegeven `.knxkeys`-bestand is niet gevonden in het pad config/.storage/knx/", "invalid_individual_address": "Waarde komt niet overeen met patroon voor KNX individueel adres.\n\"area.line.device", - "invalid_ip_address": "Ongeldig IPv4-adres.", - "invalid_signature": "Het wachtwoord om het `.knxkeys`-bestand te decoderen is verkeerd." + "invalid_ip_address": "Ongeldig IPv4-adres." }, "step": { + "connection_type": { + "title": "KNX-verbinding" + }, "manual_tunnel": { "data": { "host": "Host", @@ -24,7 +25,8 @@ "local_ip": "Leeg laten om auto-discovery te gebruiken.", "port": "Poort van het KNX/IP-tunnelapparaat." }, - "description": "Voer de verbindingsinformatie van uw tunneling-apparaat in." + "description": "Voer de verbindingsinformatie van uw tunneling-apparaat in.", + "title": "Tunnelinstellingen" }, "routing": { "data": { @@ -48,7 +50,8 @@ "knxkeys_filename": "Het bestand zal naar verwachting worden gevonden in uw configuratiemap in '.storage/knx/'.\nIn Home Assistant OS zou dit '/config/.storage/knx/' zijn.\nVoorbeeld: 'my_project.knxkeys'", "knxkeys_password": "Dit werd ingesteld bij het exporteren van het bestand van ETS." }, - "description": "Voer de informatie voor uw `.knxkeys` bestand in." + "description": "Voer de informatie voor uw `.knxkeys` bestand in.", + "title": "Sleutelbestand" }, "secure_routing_manual": { "data_description": { @@ -59,25 +62,45 @@ "data": { "gateway": "KNX Tunnel Connection" }, - "description": "Selecteer een gateway uit de lijst." + "description": "Selecteer een gateway uit de lijst.", + "title": "Tunnel" } } }, "options": { "error": { "cannot_connect": "Kan geen verbinding maken", - "file_not_found": "Het opgegeven `.knxkeys`-bestand is niet gevonden in het pad config/.storage/knx/", "invalid_individual_address": "Waarde komt niet overeen met patroon voor KNX individueel adres.\n\"area.line.device", - "invalid_ip_address": "Ongeldig IPv4-adres.", - "invalid_signature": "Het wachtwoord om het `.knxkeys`-bestand te decoderen is verkeerd." + "invalid_ip_address": "Ongeldig IPv4-adres." }, "step": { + "connection_type": { + "data": { + "connection_type": "KNX-verbindingstype" + }, + "title": "KNX-verbinding" + }, "manual_tunnel": { + "data": { + "host": "Host", + "local_ip": "Lokale IP van Home Assistant", + "port": "Poort", + "tunneling_type": "KNX Tunneling Type" + }, "data_description": { + "host": "IP adres van het KNX/IP tunneling apparaat.", "local_ip": "Leeg laten om auto-discovery te gebruiken.", "port": "Poort van het KNX/IP-tunnelapparaat." }, - "description": "Voer de verbindingsinformatie van uw tunneling-apparaat in." + "description": "Voer de verbindingsinformatie van uw tunneling-apparaat in.", + "title": "Tunnelinstellingen" + }, + "options_init": { + "menu_options": { + "communication_settings": "Communicatie-instellingen", + "connection_type": "KNX-interface configureren" + }, + "title": "KNX-instellingen" }, "routing": { "data": { @@ -101,7 +124,8 @@ "knxkeys_filename": "Het bestand zal naar verwachting worden gevonden in uw configuratiemap in '.storage/knx/'.\nIn Home Assistant OS zou dit '/config/.storage/knx/' zijn.\nVoorbeeld: 'my_project.knxkeys'", "knxkeys_password": "Dit werd ingesteld bij het exporteren van het bestand van ETS." }, - "description": "Voer de informatie voor uw `.knxkeys` bestand in." + "description": "Voer de informatie voor uw `.knxkeys` bestand in.", + "title": "Sleutelbestand" }, "secure_routing_manual": { "data_description": { @@ -112,7 +136,8 @@ "data": { "gateway": "KNX Tunnel Connection" }, - "description": "Selecteer een gateway uit de lijst." + "description": "Selecteer een gateway uit de lijst.", + "title": "Tunnel" } } } diff --git a/homeassistant/components/knx/translations/no.json b/homeassistant/components/knx/translations/no.json index 183a2cd18d2..4d0d1ea432b 100644 --- a/homeassistant/components/knx/translations/no.json +++ b/homeassistant/components/knx/translations/no.json @@ -6,11 +6,13 @@ }, "error": { "cannot_connect": "Tilkobling mislyktes", - "file_not_found": "Den angitte `.knxkeys`-filen ble ikke funnet i banen config/.storage/knx/", "invalid_backbone_key": "Ugyldig ryggradsn\u00f8kkel. 32 heksadesimale tall forventet.", "invalid_individual_address": "Verdien samsvarer ikke med m\u00f8nsteret for individuelle KNX-adresser.\n 'area.line.device'", "invalid_ip_address": "Ugyldig IPv4-adresse.", - "invalid_signature": "Passordet for \u00e5 dekryptere `.knxkeys`-filen er feil.", + "keyfile_invalid_signature": "Passordet for \u00e5 dekryptere `.knxkeys`-filen er feil.", + "keyfile_no_backbone_key": "`.knxkeys`-filen inneholder ikke en ryggradsn\u00f8kkel for sikker ruting.", + "keyfile_no_tunnel_for_host": "`.knxkeys`-filen inneholder ikke legitimasjon for vert ` {host} `.", + "keyfile_not_found": "Den angitte `.knxkeys`-filen ble ikke funnet i banen config/.storage/knx/", "no_router_discovered": "Ingen KNXnet/IP-ruter ble oppdaget p\u00e5 nettverket.", "no_tunnel_discovered": "Kunne ikke finne en KNX-tunnelserver p\u00e5 nettverket ditt.", "unsupported_tunnel_type": "Den valgte tunneltypen st\u00f8ttes ikke av gatewayen." @@ -20,7 +22,15 @@ "data": { "connection_type": "KNX tilkoblingstype" }, - "description": "Vennligst skriv inn tilkoblingstypen vi skal bruke for din KNX-tilkobling.\n AUTOMATISK - Integrasjonen tar seg av tilkoblingen til KNX-bussen ved \u00e5 utf\u00f8re en gateway-skanning.\n TUNNELING - Integrasjonen vil kobles til din KNX-bussen via tunnelering.\n ROUTING - Integrasjonen vil koble til din KNX-bussen via ruting." + "description": "Vennligst skriv inn tilkoblingstypen vi skal bruke for din KNX-tilkobling.\n AUTOMATISK - Integrasjonen tar seg av tilkoblingen til KNX-bussen ved \u00e5 utf\u00f8re en gateway-skanning.\n TUNNELING - Integrasjonen vil kobles til din KNX-bussen via tunnelering.\n ROUTING - Integrasjonen vil koble til din KNX-bussen via ruting.", + "title": "KNX-tilkobling" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatisk` vil bruke det f\u00f8rste ledige tunnelendepunktet." + }, + "description": "Velg tunnelen som brukes for tilkobling.", + "title": "Tunnelendepunkt" }, "manual_tunnel": { "data": { @@ -36,7 +46,8 @@ "port": "Port p\u00e5 KNX/IP-tunnelenheten.", "route_back": "Aktiver hvis KNXnet/IP-tunnelserveren din er bak NAT. Gjelder kun for UDP-tilkoblinger." }, - "description": "Vennligst skriv inn tilkoblingsinformasjonen til tunnelenheten din." + "description": "Vennligst skriv inn tilkoblingsinformasjonen til tunnelenheten din.", + "title": "Tunnelinnstillinger" }, "routing": { "data": { @@ -50,7 +61,8 @@ "individual_address": "KNX-adresse som skal brukes av Home Assistant, f.eks. `0.0.4`", "local_ip": "La st\u00e5 tomt for \u00e5 bruke automatisk oppdagelse." }, - "description": "Vennligst konfigurer rutealternativene." + "description": "Vennligst konfigurer rutealternativene.", + "title": "Ruting" }, "secure_key_source": { "description": "Velg hvordan du vil konfigurere KNX/IP Secure.", @@ -58,7 +70,8 @@ "secure_knxkeys": "Bruk en `.knxkeys`-fil som inneholder sikre IP-n\u00f8kler", "secure_routing_manual": "Konfigurer IP sikker ryggradsn\u00f8kkel manuelt", "secure_tunnel_manual": "Konfigurer IP sikker legitimasjon manuelt" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -69,7 +82,8 @@ "knxkeys_filename": "Filen forventes \u00e5 bli funnet i konfigurasjonskatalogen din i `.storage/knx/`.\n I Home Assistant OS vil dette v\u00e6re `/config/.storage/knx/`\n Eksempel: `mitt_prosjekt.knxkeys`", "knxkeys_password": "Dette ble satt ved eksport av filen fra ETS." }, - "description": "Vennligst skriv inn informasjonen for `.knxkeys`-filen." + "description": "Vennligst skriv inn informasjonen for `.knxkeys`-filen.", + "title": "N\u00f8kkelfil" }, "secure_routing_manual": { "data": { @@ -80,7 +94,8 @@ "backbone_key": "Kan sees i 'Sikkerhet'-rapporten til et ETS-prosjekt. F.eks. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Standard er 1000." }, - "description": "Vennligst skriv inn din sikre IP-informasjon." + "description": "Vennligst skriv inn din sikre IP-informasjon.", + "title": "Sikker ruting" }, "secure_tunnel_manual": { "data": { @@ -93,24 +108,28 @@ "user_id": "Dette er ofte tunnelnummer +1. S\u00e5 'Tunnel 2' ville ha bruker-ID '3'.", "user_password": "Passord for den spesifikke tunnelforbindelsen satt i 'Egenskaper'-panelet i tunnelen i ETS." }, - "description": "Vennligst skriv inn din sikre IP-informasjon." + "description": "Vennligst skriv inn din sikre IP-informasjon.", + "title": "Sikker tunneling" }, "tunnel": { "data": { "gateway": "KNX Tunneltilkobling" }, - "description": "Vennligst velg en gateway fra listen." + "description": "Vennligst velg en gateway fra listen.", + "title": "Tunnel" } } }, "options": { "error": { "cannot_connect": "Tilkobling mislyktes", - "file_not_found": "Den angitte `.knxkeys`-filen ble ikke funnet i banen config/.storage/knx/", "invalid_backbone_key": "Ugyldig ryggradsn\u00f8kkel. 32 heksadesimale tall forventet.", "invalid_individual_address": "Verdien samsvarer ikke med m\u00f8nsteret for individuelle KNX-adresser.\n 'area.line.device'", "invalid_ip_address": "Ugyldig IPv4-adresse.", - "invalid_signature": "Passordet for \u00e5 dekryptere `.knxkeys`-filen er feil.", + "keyfile_invalid_signature": "Passordet for \u00e5 dekryptere `.knxkeys`-filen er feil.", + "keyfile_no_backbone_key": "`.knxkeys`-filen inneholder ikke en ryggradsn\u00f8kkel for sikker ruting.", + "keyfile_no_tunnel_for_host": "`.knxkeys`-filen inneholder ikke legitimasjon for vert ` {host} `.", + "keyfile_not_found": "Den angitte `.knxkeys`-filen ble ikke funnet i banen config/.storage/knx/", "no_router_discovered": "Ingen KNXnet/IP-ruter ble oppdaget p\u00e5 nettverket.", "no_tunnel_discovered": "Kunne ikke finne en KNX-tunnelserver p\u00e5 nettverket ditt.", "unsupported_tunnel_type": "Den valgte tunneltypen st\u00f8ttes ikke av gatewayen." @@ -124,13 +143,22 @@ "data_description": { "rate_limit": "Maksimalt utg\u00e5ende telegrammer per sekund.\n `0` for \u00e5 deaktivere grensen. Anbefalt: 0 eller 20 til 40", "state_updater": "Angi standard for lesing av tilstander fra KNX-bussen. N\u00e5r den er deaktivert, vil ikke Home Assistant aktivt hente enhetstilstander fra KNX-bussen. Kan overstyres av entitetsalternativer for \"sync_state\"." - } + }, + "title": "Innstillinger for kommunikasjon" }, "connection_type": { "data": { "connection_type": "KNX tilkoblingstype" }, - "description": "Vennligst skriv inn tilkoblingstypen vi skal bruke for din KNX-tilkobling.\n AUTOMATISK - Integrasjonen tar seg av tilkoblingen til KNX-bussen ved \u00e5 utf\u00f8re en gateway-skanning.\n TUNNELING - Integrasjonen vil kobles til din KNX-bussen via tunnelering.\n ROUTING - Integrasjonen vil koble til din KNX-bussen via ruting." + "description": "Vennligst skriv inn tilkoblingstypen vi skal bruke for din KNX-tilkobling.\n AUTOMATISK - Integrasjonen tar seg av tilkoblingen til KNX-bussen ved \u00e5 utf\u00f8re en gateway-skanning.\n TUNNELING - Integrasjonen vil kobles til din KNX-bussen via tunnelering.\n ROUTING - Integrasjonen vil koble til din KNX-bussen via ruting.", + "title": "KNX-tilkobling" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatisk` vil bruke det f\u00f8rste ledige tunnelendepunktet." + }, + "description": "Velg tunnelen som brukes for tilkobling.", + "title": "Tunnelendepunkt" }, "manual_tunnel": { "data": { @@ -146,13 +174,15 @@ "port": "Port p\u00e5 KNX/IP-tunnelenheten.", "route_back": "Aktiver hvis KNXnet/IP-tunnelserveren din er bak NAT. Gjelder kun for UDP-tilkoblinger." }, - "description": "Vennligst skriv inn tilkoblingsinformasjonen til tunnelenheten din." + "description": "Vennligst skriv inn tilkoblingsinformasjonen til tunnelenheten din.", + "title": "Tunnelinnstillinger" }, "options_init": { "menu_options": { "communication_settings": "Kommunikasjonsinnstillinger", "connection_type": "Konfigurer KNX-grensesnitt" - } + }, + "title": "KNX-innstillinger" }, "routing": { "data": { @@ -166,7 +196,8 @@ "individual_address": "KNX-adresse som skal brukes av Home Assistant, f.eks. `0.0.4`", "local_ip": "La st\u00e5 tomt for \u00e5 bruke automatisk oppdagelse." }, - "description": "Vennligst konfigurer rutealternativene." + "description": "Vennligst konfigurer rutealternativene.", + "title": "Ruting" }, "secure_key_source": { "description": "Velg hvordan du vil konfigurere KNX/IP Secure.", @@ -174,7 +205,8 @@ "secure_knxkeys": "Bruk en `.knxkeys`-fil som inneholder sikre IP-n\u00f8kler", "secure_routing_manual": "Konfigurer IP sikker ryggradsn\u00f8kkel manuelt", "secure_tunnel_manual": "Konfigurer IP sikker legitimasjon manuelt" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +217,8 @@ "knxkeys_filename": "Filen forventes \u00e5 bli funnet i konfigurasjonskatalogen din i `.storage/knx/`.\n I Home Assistant OS vil dette v\u00e6re `/config/.storage/knx/`\n Eksempel: `mitt_prosjekt.knxkeys`", "knxkeys_password": "Dette ble satt ved eksport av filen fra ETS." }, - "description": "Vennligst skriv inn informasjonen for `.knxkeys`-filen." + "description": "Vennligst skriv inn informasjonen for `.knxkeys`-filen.", + "title": "N\u00f8kkelfil" }, "secure_routing_manual": { "data": { @@ -196,7 +229,8 @@ "backbone_key": "Kan sees i 'Sikkerhet'-rapporten til et ETS-prosjekt. F.eks. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Standard er 1000." }, - "description": "Vennligst skriv inn din sikre IP-informasjon." + "description": "Vennligst skriv inn din sikre IP-informasjon.", + "title": "Sikker ruting" }, "secure_tunnel_manual": { "data": { @@ -209,13 +243,15 @@ "user_id": "Dette er ofte tunnelnummer +1. S\u00e5 'Tunnel 2' ville ha bruker-ID '3'.", "user_password": "Passord for den spesifikke tunnelforbindelsen satt i 'Egenskaper'-panelet i tunnelen i ETS." }, - "description": "Vennligst skriv inn din sikre IP-informasjon." + "description": "Vennligst skriv inn din sikre IP-informasjon.", + "title": "Sikker tunneling" }, "tunnel": { "data": { "gateway": "KNX Tunneltilkobling" }, - "description": "Vennligst velg en gateway fra listen." + "description": "Vennligst velg en gateway fra listen.", + "title": "Tunnel" } } } diff --git a/homeassistant/components/knx/translations/pl.json b/homeassistant/components/knx/translations/pl.json index e8fd5c6dba7..4cfa5f5d4bd 100644 --- a/homeassistant/components/knx/translations/pl.json +++ b/homeassistant/components/knx/translations/pl.json @@ -6,11 +6,13 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "file_not_found": "Podany plik '.knxkeys' nie zosta\u0142 znaleziony w \u015bcie\u017cce config/.storage/knx/", "invalid_backbone_key": "Nieprawid\u0142owy klucz szkieletowy. Oczekiwano 32 liczb szesnastkowych.", "invalid_individual_address": "Warto\u015b\u0107 nie pasuje do wzorca dla indywidualnego adresu KNX.\n 'obszar.linia.urz\u0105dzenie'", "invalid_ip_address": "Nieprawid\u0142owy adres IPv4.", - "invalid_signature": "Has\u0142o do odszyfrowania pliku '.knxkeys' jest nieprawid\u0142owe.", + "keyfile_invalid_signature": "Has\u0142o do odszyfrowania pliku '.knxkeys' jest nieprawid\u0142owe.", + "keyfile_no_backbone_key": "Plik `.knxkeys` nie zawiera klucza szkieletowego do bezpiecznego routingu.", + "keyfile_no_tunnel_for_host": "Plik `.knxkeys` nie zawiera po\u015bwiadcze\u0144 dla hosta `{host}`.", + "keyfile_not_found": "Podany plik '.knxkeys' nie zosta\u0142 znaleziony w \u015bcie\u017cce config/.storage/knx/", "no_router_discovered": "Nie wykryto w sieci routera KNXnet/IP.", "no_tunnel_discovered": "Nie mo\u017cna znale\u017a\u0107 serwera tuneluj\u0105cego KNX w Twojej sieci.", "unsupported_tunnel_type": "Wybrany typ tunelowania nie jest obs\u0142ugiwany przez bramk\u0119." @@ -20,7 +22,15 @@ "data": { "connection_type": "Typ po\u0142\u0105czenia KNX" }, - "description": "Prosz\u0119 wprowadzi\u0107 typ po\u0142\u0105czenia, kt\u00f3rego powinni\u015bmy u\u017cy\u0107 dla po\u0142\u0105czenia KNX. \nAUTOMATIC - Integracja sama zadba o po\u0142\u0105czenie z magistral\u0105 KNX poprzez skanowanie bramki. \nTUNNELING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez tunelowanie. \nROUTING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez routing." + "description": "Prosz\u0119 wprowadzi\u0107 typ po\u0142\u0105czenia, kt\u00f3rego powinni\u015bmy u\u017cy\u0107 dla po\u0142\u0105czenia KNX. \nAUTOMATIC - Integracja sama zadba o po\u0142\u0105czenie z magistral\u0105 KNX poprzez skanowanie bramki. \nTUNNELING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez tunelowanie. \nROUTING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez routing.", + "title": "Po\u0142\u0105czenie KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatyczny` u\u017cyje pierwszego wolnego punktu ko\u0144cowego tunelu." + }, + "description": "Wybierz tunel u\u017cywany do po\u0142\u0105czenia.", + "title": "Punkt ko\u0144cowy tunelu" }, "manual_tunnel": { "data": { @@ -36,7 +46,8 @@ "port": "Port urz\u0105dzenia tuneluj\u0105cego KNX/IP.", "route_back": "W\u0142\u0105cz, je\u015bli serwer tuneluj\u0105cy KNXnet/IP znajduje si\u0119 za NAT. Dotyczy tylko po\u0142\u0105cze\u0144 UDP." }, - "description": "Prosz\u0119 wprowadzi\u0107 informacje o po\u0142\u0105czeniu urz\u0105dzenia tuneluj\u0105cego." + "description": "Prosz\u0119 wprowadzi\u0107 informacje o po\u0142\u0105czeniu urz\u0105dzenia tuneluj\u0105cego.", + "title": "Ustawienia tunelowania" }, "routing": { "data": { @@ -50,7 +61,8 @@ "individual_address": "Adres KNX u\u017cywany przez Home Assistanta, np. `0.0.4`", "local_ip": "Pozostaw puste, aby u\u017cy\u0107 automatycznego wykrywania." }, - "description": "Prosz\u0119 skonfigurowa\u0107 opcje routingu." + "description": "Prosz\u0119 skonfigurowa\u0107 opcje routingu.", + "title": "Routing" }, "secure_key_source": { "description": "Wybierz, jak chcesz skonfigurowa\u0107 KNX/IP Secure.", @@ -58,7 +70,8 @@ "secure_knxkeys": "U\u017cyj pliku `.knxkeys` zawieraj\u0105cego klucze IP secure", "secure_routing_manual": "R\u0119czna konfiguracja klucza szkieletowego IP Secure", "secure_tunnel_manual": "R\u0119czna konfiguracja danych uwierzytelniaj\u0105cych IP Secure" - } + }, + "title": "KNX IP Secure" }, "secure_knxkeys": { "data": { @@ -69,7 +82,8 @@ "knxkeys_filename": "Plik powinien znajdowa\u0107 si\u0119 w katalogu konfiguracyjnym w `.storage/knx/`.\nW systemie Home Assistant OS b\u0119dzie to `/config/.storage/knx/`\nPrzyk\u0142ad: `m\u00f3j_projekt.knxkeys`", "knxkeys_password": "Zosta\u0142o to ustawione podczas eksportowania pliku z ETS." }, - "description": "Wprowad\u017a informacje dotycz\u0105ce pliku `.knxkeys`." + "description": "Wprowad\u017a informacje dotycz\u0105ce pliku `.knxkeys`.", + "title": "Plik klucza" }, "secure_routing_manual": { "data": { @@ -80,7 +94,8 @@ "backbone_key": "Mo\u017cna go zobaczy\u0107 w raporcie \u201eBezpiecze\u0144stwo\u201d projektu ETS. Np. \u201e00112233445566778899AABBCCDDEEFF\u201d.", "sync_latency_tolerance": "Warto\u015b\u0107 domy\u015blna to 1000." }, - "description": "Wprowad\u017a informacje o IP Secure." + "description": "Wprowad\u017a informacje o IP Secure.", + "title": "Bezpieczny routing" }, "secure_tunnel_manual": { "data": { @@ -93,24 +108,28 @@ "user_id": "Cz\u0119sto jest to numer tunelu plus 1. Tak wi\u0119c \u201eTunnel 2\u201d mia\u0142by identyfikator u\u017cytkownika \u201e3\u201d.", "user_password": "Has\u0142o dla konkretnego po\u0142\u0105czenia tunelowego ustawione w panelu \u201eW\u0142a\u015bciwo\u015bci\u201d tunelu w ETS." }, - "description": "Wprowad\u017a informacje o IP secure." + "description": "Wprowad\u017a informacje o IP secure.", + "title": "Bezpieczne tunelowanie" }, "tunnel": { "data": { "gateway": "Po\u0142\u0105czenie tunelowe KNX" }, - "description": "Prosz\u0119 wybra\u0107 bramk\u0119 z listy." + "description": "Prosz\u0119 wybra\u0107 bramk\u0119 z listy.", + "title": "Tunelowanie" } } }, "options": { "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "file_not_found": "Podany plik '.knxkeys' nie zosta\u0142 znaleziony w \u015bcie\u017cce config/.storage/knx/", "invalid_backbone_key": "Nieprawid\u0142owy klucz szkieletowy. Oczekiwano 32 liczb szesnastkowych.", "invalid_individual_address": "Warto\u015b\u0107 nie pasuje do wzorca dla indywidualnego adresu KNX.\n 'obszar.linia.urz\u0105dzenie'", "invalid_ip_address": "Nieprawid\u0142owy adres IPv4.", - "invalid_signature": "Has\u0142o do odszyfrowania pliku '.knxkeys' jest nieprawid\u0142owe.", + "keyfile_invalid_signature": "Has\u0142o do odszyfrowania pliku '.knxkeys' jest nieprawid\u0142owe.", + "keyfile_no_backbone_key": "Plik `.knxkeys` nie zawiera klucza szkieletowego do bezpiecznego routingu.", + "keyfile_no_tunnel_for_host": "Plik `.knxkeys` nie zawiera po\u015bwiadcze\u0144 dla hosta `{host}`.", + "keyfile_not_found": "Podany plik '.knxkeys' nie zosta\u0142 znaleziony w \u015bcie\u017cce config/.storage/knx/", "no_router_discovered": "Nie wykryto w sieci routera KNXnet/IP.", "no_tunnel_discovered": "Nie mo\u017cna znale\u017a\u0107 serwera tuneluj\u0105cego KNX w Twojej sieci.", "unsupported_tunnel_type": "Wybrany typ tunelowania nie jest obs\u0142ugiwany przez bramk\u0119." @@ -124,13 +143,22 @@ "data_description": { "rate_limit": "Maksymalna liczba wychodz\u0105cych wiadomo\u015bci na sekund\u0119.\n \u201e0\u201d, aby wy\u0142\u0105czy\u0107 limit. Zalecane: 0 lub 20 do 40", "state_updater": "Ustaw domy\u015blne odczytywanie stan\u00f3w z magistrali KNX. Po wy\u0142\u0105czeniu, Home Assistant nie b\u0119dzie aktywnie pobiera\u0107 stan\u00f3w encji z magistrali KNX. Mo\u017cna to zast\u0105pi\u0107 przez opcj\u0119 encji `sync_state`." - } + }, + "title": "Ustawienia komunikacji" }, "connection_type": { "data": { "connection_type": "Typ po\u0142\u0105czenia KNX" }, - "description": "Prosz\u0119 wprowadzi\u0107 typ po\u0142\u0105czenia, kt\u00f3rego powinni\u015bmy u\u017cy\u0107 dla po\u0142\u0105czenia KNX. \nAUTOMATIC - Integracja sama zadba o po\u0142\u0105czenie z magistral\u0105 KNX poprzez skanowanie bramki. \nTUNNELING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez tunelowanie. \nROUTING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez routing." + "description": "Prosz\u0119 wprowadzi\u0107 typ po\u0142\u0105czenia, kt\u00f3rego powinni\u015bmy u\u017cy\u0107 dla po\u0142\u0105czenia KNX. \nAUTOMATIC - Integracja sama zadba o po\u0142\u0105czenie z magistral\u0105 KNX poprzez skanowanie bramki. \nTUNNELING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez tunelowanie. \nROUTING - Integracja po\u0142\u0105czy si\u0119 z magistral\u0105 KNX poprzez routing.", + "title": "Po\u0142\u0105czenie KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatyczny` u\u017cyje pierwszego wolnego punktu ko\u0144cowego tunelu." + }, + "description": "Wybierz tunel u\u017cywany do po\u0142\u0105czenia.", + "title": "Punkt ko\u0144cowy tunelu" }, "manual_tunnel": { "data": { @@ -146,13 +174,15 @@ "port": "Port urz\u0105dzenia tuneluj\u0105cego KNX/IP.", "route_back": "W\u0142\u0105cz, je\u015bli serwer tuneluj\u0105cy KNXnet/IP znajduje si\u0119 za NAT. Dotyczy tylko po\u0142\u0105cze\u0144 UDP." }, - "description": "Prosz\u0119 wprowadzi\u0107 informacje o po\u0142\u0105czeniu urz\u0105dzenia tuneluj\u0105cego." + "description": "Prosz\u0119 wprowadzi\u0107 informacje o po\u0142\u0105czeniu urz\u0105dzenia tuneluj\u0105cego.", + "title": "Ustawienia tunelowania" }, "options_init": { "menu_options": { "communication_settings": "Ustawienia komunikacji", "connection_type": "Konfiguracja interfejsu KNX" - } + }, + "title": "Ustawienia KNX" }, "routing": { "data": { @@ -166,7 +196,8 @@ "individual_address": "Adres KNX u\u017cywany przez Home Assistanta, np. `0.0.4`", "local_ip": "Pozostaw puste, aby u\u017cy\u0107 automatycznego wykrywania." }, - "description": "Prosz\u0119 skonfigurowa\u0107 opcje routingu." + "description": "Prosz\u0119 skonfigurowa\u0107 opcje routingu.", + "title": "Routing" }, "secure_key_source": { "description": "Wybierz, jak chcesz skonfigurowa\u0107 KNX/IP Secure.", @@ -174,7 +205,8 @@ "secure_knxkeys": "U\u017cyj pliku `.knxkeys` zawieraj\u0105cego klucze IP secure", "secure_routing_manual": "R\u0119czna konfiguracja klucza szkieletowego IP Secure", "secure_tunnel_manual": "R\u0119czna konfiguracja danych uwierzytelniaj\u0105cych IP Secure" - } + }, + "title": "KNX IP Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +217,8 @@ "knxkeys_filename": "Plik powinien znajdowa\u0107 si\u0119 w katalogu konfiguracyjnym w `.storage/knx/`.\nW systemie Home Assistant OS b\u0119dzie to `/config/.storage/knx/`\nPrzyk\u0142ad: `m\u00f3j_projekt.knxkeys`", "knxkeys_password": "Zosta\u0142o to ustawione podczas eksportowania pliku z ETS." }, - "description": "Wprowad\u017a informacje dotycz\u0105ce pliku `.knxkeys`." + "description": "Wprowad\u017a informacje dotycz\u0105ce pliku `.knxkeys`.", + "title": "Plik klucza" }, "secure_routing_manual": { "data": { @@ -196,7 +229,8 @@ "backbone_key": "Mo\u017cna go zobaczy\u0107 w raporcie \u201eBezpiecze\u0144stwo\u201d projektu ETS. Np. \u201e00112233445566778899AABBCCDDEEFF\u201d.", "sync_latency_tolerance": "Warto\u015b\u0107 domy\u015blna to 1000." }, - "description": "Wprowad\u017a informacje o IP Secure." + "description": "Wprowad\u017a informacje o IP Secure.", + "title": "Bezpieczny routing" }, "secure_tunnel_manual": { "data": { @@ -209,13 +243,15 @@ "user_id": "Cz\u0119sto jest to numer tunelu plus 1. Tak wi\u0119c \u201eTunnel 2\u201d mia\u0142by identyfikator u\u017cytkownika \u201e3\u201d.", "user_password": "Has\u0142o dla konkretnego po\u0142\u0105czenia tunelowego ustawione w panelu \u201eW\u0142a\u015bciwo\u015bci\u201d tunelu w ETS." }, - "description": "Wprowad\u017a informacje o IP secure." + "description": "Wprowad\u017a informacje o IP secure.", + "title": "Bezpieczne tunelowanie" }, "tunnel": { "data": { "gateway": "Po\u0142\u0105czenie tunelowe KNX" }, - "description": "Prosz\u0119 wybra\u0107 bramk\u0119 z listy." + "description": "Prosz\u0119 wybra\u0107 bramk\u0119 z listy.", + "title": "Tunelowanie" } } } diff --git a/homeassistant/components/knx/translations/pt-BR.json b/homeassistant/components/knx/translations/pt-BR.json index bc934cf135c..5467c084636 100644 --- a/homeassistant/components/knx/translations/pt-BR.json +++ b/homeassistant/components/knx/translations/pt-BR.json @@ -6,11 +6,13 @@ }, "error": { "cannot_connect": "Falha ao conectar", - "file_not_found": "O arquivo `.knxkeys` especificado n\u00e3o foi encontrado no caminho config/.storage/knx/", "invalid_backbone_key": "Chave de backbone inv\u00e1lida. 32 n\u00fameros hexadecimais esperados.", "invalid_individual_address": "O valor n\u00e3o corresponde ao padr\u00e3o do endere\u00e7o individual KNX.\n '\u00e1rea.linha.dispositivo'", "invalid_ip_address": "Endere\u00e7o IPv4 inv\u00e1lido.", - "invalid_signature": "A senha para descriptografar o arquivo `.knxkeys` est\u00e1 errada.", + "keyfile_invalid_signature": "A senha para descriptografar o arquivo `.knxkeys` est\u00e1 errada.", + "keyfile_no_backbone_key": "O arquivo `.knxkeys` n\u00e3o cont\u00e9m uma chave de backbone para roteamento seguro.", + "keyfile_no_tunnel_for_host": "O arquivo `.knxkeys` n\u00e3o cont\u00e9m credenciais para o host ` {host} `.", + "keyfile_not_found": "O arquivo `.knxkeys` especificado n\u00e3o foi encontrado no caminho config/.storage/knx/", "no_router_discovered": "Nenhum roteador KNXnet/IP foi descoberto na rede.", "no_tunnel_discovered": "N\u00e3o foi poss\u00edvel encontrar um servidor de encapsulamento KNX em sua rede.", "unsupported_tunnel_type": "O tipo de tunelamento selecionado n\u00e3o \u00e9 compat\u00edvel com o gateway." @@ -20,7 +22,15 @@ "data": { "connection_type": "Tipo de conex\u00e3o KNX" }, - "description": "Insira o tipo de conex\u00e3o que devemos usar para sua conex\u00e3o KNX.\n AUTOM\u00c1TICO - A integra\u00e7\u00e3o cuida da conectividade com o seu KNX Bus realizando uma varredura de gateway.\n TUNNELING - A integra\u00e7\u00e3o ser\u00e1 conectada ao seu barramento KNX via tunelamento.\n ROUTING - A integra\u00e7\u00e3o ligar-se-\u00e1 ao seu bus KNX atrav\u00e9s de encaminhamento." + "description": "Insira o tipo de conex\u00e3o que devemos usar para sua conex\u00e3o KNX.\n AUTOM\u00c1TICO - A integra\u00e7\u00e3o cuida da conectividade com o seu KNX Bus realizando uma varredura de gateway.\n TUNNELING - A integra\u00e7\u00e3o ser\u00e1 conectada ao seu barramento KNX via tunelamento.\n ROUTING - A integra\u00e7\u00e3o ligar-se-\u00e1 ao seu bus KNX atrav\u00e9s de encaminhamento.", + "title": "Conex\u00e3o KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Autom\u00e1tico` usar\u00e1 o primeiro endpoint de t\u00fanel livre." + }, + "description": "Selecione o t\u00fanel usado para conex\u00e3o.", + "title": "Endpoint do t\u00fanel" }, "manual_tunnel": { "data": { @@ -36,7 +46,8 @@ "port": "Porta do dispositivo de tunelamento KNX/IP.", "route_back": "Ative se o servidor de encapsulamento KNXnet/IP estiver atr\u00e1s do NAT. Aplica-se apenas a conex\u00f5es UDP." }, - "description": "Por favor, digite as informa\u00e7\u00f5es de conex\u00e3o do seu dispositivo de tunelamento." + "description": "Por favor, digite as informa\u00e7\u00f5es de conex\u00e3o do seu dispositivo de tunelamento.", + "title": "Configura\u00e7\u00f5es do t\u00fanel" }, "routing": { "data": { @@ -50,7 +61,8 @@ "individual_address": "Endere\u00e7o KNX a ser usado pelo Home Assistant, por exemplo, `0.0.4`", "local_ip": "Deixe em branco para usar a descoberta autom\u00e1tica." }, - "description": "Por favor, configure as op\u00e7\u00f5es de roteamento." + "description": "Por favor, configure as op\u00e7\u00f5es de roteamento.", + "title": "Roteamento" }, "secure_key_source": { "description": "Selecione como deseja configurar o KNX/IP Secure.", @@ -58,7 +70,8 @@ "secure_knxkeys": "Use um arquivo `.knxkeys` contendo chaves IP seguras", "secure_routing_manual": "Configure a chave de backbone IP segura manualmente", "secure_tunnel_manual": "Configurar credenciais seguras de IP manualmente" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -69,7 +82,8 @@ "knxkeys_filename": "Espera-se que o arquivo seja encontrado em seu diret\u00f3rio de configura\u00e7\u00e3o em `.storage/knx/`.\n No sistema operacional Home Assistant seria `/config/.storage/knx/`\n Exemplo: `my_project.knxkeys`", "knxkeys_password": "Isso foi definido ao exportar o arquivo do ETS." }, - "description": "Por favor, insira as informa\u00e7\u00f5es para o seu arquivo `.knxkeys`." + "description": "Por favor, insira as informa\u00e7\u00f5es para o seu arquivo `.knxkeys`.", + "title": "arquivo-chave" }, "secure_routing_manual": { "data": { @@ -80,7 +94,8 @@ "backbone_key": "Pode ser visto no relat\u00f3rio 'Seguran\u00e7a' de um projeto ETS. Por exemplo. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "O padr\u00e3o \u00e9 1000." }, - "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP." + "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP.", + "title": "Roteamento seguro" }, "secure_tunnel_manual": { "data": { @@ -93,24 +108,28 @@ "user_id": "Isso geralmente \u00e9 o n\u00famero do t\u00fanel +1. Portanto, 'T\u00fanel 2' teria o ID de usu\u00e1rio '3'.", "user_password": "Senha para a conex\u00e3o de t\u00fanel espec\u00edfica definida no painel 'Propriedades' do t\u00fanel no ETS." }, - "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP." + "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP.", + "title": "Tunelamento seguro" }, "tunnel": { "data": { "gateway": "Conex\u00e3o do t\u00fanel KNX" }, - "description": "Selecione um gateway na lista." + "description": "Selecione um gateway na lista.", + "title": "T\u00fanel" } } }, "options": { "error": { "cannot_connect": "Falha ao conectar", - "file_not_found": "O arquivo `.knxkeys` especificado n\u00e3o foi encontrado no caminho config/.storage/knx/", "invalid_backbone_key": "Chave de backbone inv\u00e1lida. 32 n\u00fameros hexadecimais esperados.", "invalid_individual_address": "O valor n\u00e3o corresponde ao padr\u00e3o do endere\u00e7o individual KNX.\n '\u00e1rea.linha.dispositivo'", "invalid_ip_address": "Endere\u00e7o IPv4 inv\u00e1lido.", - "invalid_signature": "A senha para descriptografar o arquivo `.knxkeys` est\u00e1 errada.", + "keyfile_invalid_signature": "A senha para descriptografar o arquivo `.knxkeys` est\u00e1 errada.", + "keyfile_no_backbone_key": "O arquivo `.knxkeys` n\u00e3o cont\u00e9m uma chave de backbone para roteamento seguro.", + "keyfile_no_tunnel_for_host": "O arquivo `.knxkeys` n\u00e3o cont\u00e9m credenciais para o host ` {host} `.", + "keyfile_not_found": "O arquivo `.knxkeys` especificado n\u00e3o foi encontrado no caminho config/.storage/knx/", "no_router_discovered": "Nenhum roteador KNXnet/IP foi descoberto na rede.", "no_tunnel_discovered": "N\u00e3o foi poss\u00edvel encontrar um servidor de encapsulamento KNX em sua rede.", "unsupported_tunnel_type": "O tipo de tunelamento selecionado n\u00e3o \u00e9 compat\u00edvel com o gateway." @@ -124,13 +143,22 @@ "data_description": { "rate_limit": "M\u00e1ximo de telegramas de sa\u00edda por segundo.\n `0` para desabilitar o limite. Recomendado: 0 ou 20 a 40", "state_updater": "Defina o padr\u00e3o para estados de leitura do barramento KNX. Quando desativado, o Home Assistant n\u00e3o recuperar\u00e1 ativamente os estados de entidade do barramento KNX. Pode ser substitu\u00eddo pelas op\u00e7\u00f5es de entidade `sync_state`." - } + }, + "title": "Configura\u00e7\u00f5es de comunica\u00e7\u00e3o" }, "connection_type": { "data": { "connection_type": "Tipo de conex\u00e3o KNX" }, - "description": "Insira o tipo de conex\u00e3o que devemos usar para sua conex\u00e3o KNX.\n AUTOM\u00c1TICO - A integra\u00e7\u00e3o cuida da conectividade com o seu KNX Bus realizando uma varredura de gateway.\n TUNNELING - A integra\u00e7\u00e3o ser\u00e1 conectada ao seu barramento KNX via tunelamento.\n ROUTING - A integra\u00e7\u00e3o ligar-se-\u00e1 ao seu bus KNX atrav\u00e9s de encaminhamento." + "description": "Insira o tipo de conex\u00e3o que devemos usar para sua conex\u00e3o KNX.\n AUTOM\u00c1TICO - A integra\u00e7\u00e3o cuida da conectividade com o seu KNX Bus realizando uma varredura de gateway.\n TUNNELING - A integra\u00e7\u00e3o ser\u00e1 conectada ao seu barramento KNX via tunelamento.\n ROUTING - A integra\u00e7\u00e3o ligar-se-\u00e1 ao seu bus KNX atrav\u00e9s de encaminhamento.", + "title": "Conex\u00e3o KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Autom\u00e1tico` usar\u00e1 o primeiro endpoint de t\u00fanel livre." + }, + "description": "Selecione o t\u00fanel usado para conex\u00e3o.", + "title": "Endpoint do t\u00fanel" }, "manual_tunnel": { "data": { @@ -146,13 +174,15 @@ "port": "Porta do dispositivo de tunelamento KNX/IP.", "route_back": "Ative se o servidor de encapsulamento KNXnet/IP estiver atr\u00e1s do NAT. Aplica-se apenas a conex\u00f5es UDP." }, - "description": "Por favor, digite as informa\u00e7\u00f5es de conex\u00e3o do seu dispositivo de tunelamento." + "description": "Por favor, digite as informa\u00e7\u00f5es de conex\u00e3o do seu dispositivo de tunelamento.", + "title": "Configura\u00e7\u00f5es do t\u00fanel" }, "options_init": { "menu_options": { "communication_settings": "Configura\u00e7\u00f5es de comunica\u00e7\u00e3o", "connection_type": "Configurar interface KNX" - } + }, + "title": "Configura\u00e7\u00f5es do KNX" }, "routing": { "data": { @@ -166,7 +196,8 @@ "individual_address": "Endere\u00e7o KNX a ser usado pelo Home Assistant, por exemplo, `0.0.4`", "local_ip": "Deixe em branco para usar a descoberta autom\u00e1tica." }, - "description": "Por favor, configure as op\u00e7\u00f5es de roteamento." + "description": "Por favor, configure as op\u00e7\u00f5es de roteamento.", + "title": "Roteamento" }, "secure_key_source": { "description": "Selecione como deseja configurar o KNX/IP Secure.", @@ -174,7 +205,8 @@ "secure_knxkeys": "Use um arquivo `.knxkeys` contendo chaves IP seguras", "secure_routing_manual": "Configure a chave de backbone IP segura manualmente", "secure_tunnel_manual": "Configurar credenciais seguras de IP manualmente" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +217,8 @@ "knxkeys_filename": "Espera-se que o arquivo seja encontrado em seu diret\u00f3rio de configura\u00e7\u00e3o em `.storage/knx/`.\n No sistema operacional Home Assistant seria `/config/.storage/knx/`\n Exemplo: `my_project.knxkeys`", "knxkeys_password": "Isso foi definido ao exportar o arquivo do ETS." }, - "description": "Por favor, insira as informa\u00e7\u00f5es para o seu arquivo `.knxkeys`." + "description": "Por favor, insira as informa\u00e7\u00f5es para o seu arquivo `.knxkeys`.", + "title": "arquivo-chave" }, "secure_routing_manual": { "data": { @@ -196,7 +229,8 @@ "backbone_key": "Pode ser visto no relat\u00f3rio 'Seguran\u00e7a' de um projeto ETS. Por exemplo. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "O padr\u00e3o \u00e9 1000." }, - "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP." + "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP.", + "title": "Roteamento seguro" }, "secure_tunnel_manual": { "data": { @@ -209,13 +243,15 @@ "user_id": "Isso geralmente \u00e9 o n\u00famero do t\u00fanel +1. Portanto, 'T\u00fanel 2' teria o ID de usu\u00e1rio '3'.", "user_password": "Senha para a conex\u00e3o de t\u00fanel espec\u00edfica definida no painel 'Propriedades' do t\u00fanel no ETS." }, - "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP." + "description": "Por favor, insira suas informa\u00e7\u00f5es seguras de IP.", + "title": "Tunelamento seguro" }, "tunnel": { "data": { "gateway": "Conex\u00e3o do t\u00fanel KNX" }, - "description": "Selecione um gateway na lista." + "description": "Selecione um gateway na lista.", + "title": "T\u00fanel" } } } diff --git a/homeassistant/components/knx/translations/ru.json b/homeassistant/components/knx/translations/ru.json index d2503b2eab9..4d94ad8151b 100644 --- a/homeassistant/components/knx/translations/ru.json +++ b/homeassistant/components/knx/translations/ru.json @@ -6,11 +6,13 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "file_not_found": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b `.knxkeys` \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u0432 config/.storage/knx/", "invalid_backbone_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 backbone. \u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 32 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0445 \u0447\u0438\u0441\u043b\u0430.", "invalid_individual_address": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0448\u0430\u0431\u043b\u043e\u043d\u0443 \u0434\u043b\u044f \u0438\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0430\u0434\u0440\u0435\u0441\u0430 KNX 'area.line.device'.", "invalid_ip_address": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 IPv4.", - "invalid_signature": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0438 \u0444\u0430\u0439\u043b\u0430 `.knxkeys`.", + "keyfile_invalid_signature": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0438 \u0444\u0430\u0439\u043b\u0430 `.knxkeys`.", + "keyfile_no_backbone_key": "\u0424\u0430\u0439\u043b `.knxkeys` \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u043c\u0430\u0433\u0438\u0441\u0442\u0440\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043b\u044e\u0447\u0430 \u0434\u043b\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438.", + "keyfile_no_tunnel_for_host": "\u0424\u0430\u0439\u043b `.knxkeys` \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0443\u0447\u0435\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043b\u044f \u0445\u043e\u0441\u0442\u0430 `{host}`.", + "keyfile_not_found": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b `.knxkeys` \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u0432 config/.storage/knx/", "no_router_discovered": "\u0412 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440 KNXnet/IP.", "no_tunnel_discovered": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX \u0432 \u0412\u0430\u0448\u0435\u0439 \u0441\u0435\u0442\u0438.", "unsupported_tunnel_type": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0439 \u0442\u0438\u043f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c." @@ -20,7 +22,15 @@ "data": { "connection_type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f KNX" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.\nAUTOMATIC \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u0438\u043d\u0435 KNX, \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0448\u043b\u044e\u0437\u0430.\nTUNNELING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435.\nROUTING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044e." + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.\nAUTOMATIC \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u0438\u043d\u0435 KNX, \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0448\u043b\u044e\u0437\u0430.\nTUNNELING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435.\nROUTING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044e.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatic` \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u0435\u0440\u0432\u0443\u044e \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u0443\u044e \u043a\u043e\u043d\u0435\u0447\u043d\u0443\u044e \u0442\u043e\u0447\u043a\u0443 \u0442\u0443\u043d\u043d\u0435\u043b\u044f." + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0443\u043d\u043d\u0435\u043b\u044c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "title": "\u041a\u043e\u043d\u0435\u0447\u043d\u0430\u044f \u0442\u043e\u0447\u043a\u0430 \u0442\u0443\u043d\u043d\u0435\u043b\u044f" }, "manual_tunnel": { "data": { @@ -36,7 +46,8 @@ "port": "\u041f\u043e\u0440\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX/IP.", "route_back": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0435, \u0435\u0441\u043b\u0438 \u0412\u0430\u0448 \u0441\u0435\u0440\u0432\u0435\u0440 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNXnet/IP \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0437\u0430 NAT. \u041f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0439 UDP." }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438." + "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.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0442\u0443\u043d\u043d\u0435\u043b\u044f" }, "routing": { "data": { @@ -50,7 +61,8 @@ "individual_address": "\u0410\u0434\u0440\u0435\u0441 KNX, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f Home Assistant, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, `0.0.4`", "local_ip": "\u041e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435." }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438." + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438.", + "title": "\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f" }, "secure_key_source": { "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 KNX/IP Secure.", @@ -58,7 +70,8 @@ "secure_knxkeys": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0444\u0430\u0439\u043b `.knxkeys`, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0438\u0439 \u043a\u043b\u044e\u0447\u0438 IP secure", "secure_routing_manual": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c backbone-\u043a\u043b\u044e\u0447\u0438 IP Secure \u0432\u0440\u0443\u0447\u043d\u0443\u044e", "secure_tunnel_manual": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 IP Secure \u0432\u0440\u0443\u0447\u043d\u0443\u044e" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -69,7 +82,8 @@ "knxkeys_filename": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f, \u0447\u0442\u043e \u0444\u0430\u0439\u043b \u0431\u0443\u0434\u0435\u0442 \u043d\u0430\u0439\u0434\u0435\u043d \u0432 \u043a\u0430\u0442\u0430\u043b\u043e\u0433\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 `.storage/knx/`.\n\u0415\u0441\u043b\u0438 \u0412\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 Home Assistant OS \u044d\u0442\u043e\u0442 \u043f\u0443\u0442\u044c \u0431\u0443\u0434\u0435\u0442 `/config/.storage/knx/`\n\u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: `my_project.knxkeys`", "knxkeys_password": "\u042d\u0442\u043e\u0442 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u0431\u044b\u043b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u043f\u0440\u0438 \u044d\u043a\u0441\u043f\u043e\u0440\u0442\u0435 \u0444\u0430\u0439\u043b\u0430 \u0438\u0437 ETS." }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0444\u0430\u0439\u043b\u0435 `.knxkeys`." + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0444\u0430\u0439\u043b\u0435 `.knxkeys`.", + "title": "Keyfile" }, "secure_routing_manual": { "data": { @@ -80,7 +94,8 @@ "backbone_key": "\u041c\u043e\u0436\u043d\u043e \u0443\u0432\u0438\u0434\u0435\u0442\u044c \u0432 \u043e\u0442\u0447\u0435\u0442\u0435 'Security' \u043f\u0440\u043e\u0435\u043a\u0442\u0430 ETS. Eg. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "\u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e - 1000." }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043f\u043e IP Secure." + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043f\u043e IP Secure.", + "title": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u0430\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f" }, "secure_tunnel_manual": { "data": { @@ -93,24 +108,28 @@ "user_id": "\u0427\u0430\u0441\u0442\u043e \u043d\u043e\u043c\u0435\u0440 \u0442\u0443\u043d\u043d\u0435\u043b\u044f +1. \u0422\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c, 'Tunnel 2' \u0431\u0443\u0434\u0435\u0442 \u0438\u043c\u0435\u0442\u044c User-ID '3'.", "user_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e \u0442\u0443\u043d\u043d\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f, \u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0439 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 'Properties' \u0442\u0443\u043d\u043d\u0435\u043b\u044f \u0432 ETS." }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043f\u043e IP Secure." + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043f\u043e IP Secure.", + "title": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0435 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435" }, "tunnel": { "data": { "gateway": "\u0422\u0443\u043d\u043d\u0435\u043b\u044c\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f KNX" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430." + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430.", + "title": "\u0422\u0443\u043d\u043d\u0435\u043b\u044c" } } }, "options": { "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "file_not_found": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b `.knxkeys` \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u0432 config/.storage/knx/", "invalid_backbone_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 backbone. \u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f 32 \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u0445 \u0447\u0438\u0441\u043b\u0430.", "invalid_individual_address": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0448\u0430\u0431\u043b\u043e\u043d\u0443 \u0434\u043b\u044f \u0438\u043d\u0434\u0438\u0432\u0438\u0434\u0443\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0430\u0434\u0440\u0435\u0441\u0430 KNX 'area.line.device'.", "invalid_ip_address": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0430\u0434\u0440\u0435\u0441 IPv4.", - "invalid_signature": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0438 \u0444\u0430\u0439\u043b\u0430 `.knxkeys`.", + "keyfile_invalid_signature": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0444\u0440\u043e\u0432\u043a\u0438 \u0444\u0430\u0439\u043b\u0430 `.knxkeys`.", + "keyfile_no_backbone_key": "\u0424\u0430\u0439\u043b `.knxkeys` \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u043c\u0430\u0433\u0438\u0441\u0442\u0440\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043b\u044e\u0447\u0430 \u0434\u043b\u044f \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438.", + "keyfile_no_tunnel_for_host": "\u0424\u0430\u0439\u043b `.knxkeys` \u043d\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0443\u0447\u0435\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u0434\u043b\u044f \u0445\u043e\u0441\u0442\u0430 `{host}`.", + "keyfile_not_found": "\u0423\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b `.knxkeys` \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u0432 config/.storage/knx/", "no_router_discovered": "\u0412 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440 KNXnet/IP.", "no_tunnel_discovered": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043d\u0430\u0439\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX \u0432 \u0412\u0430\u0448\u0435\u0439 \u0441\u0435\u0442\u0438.", "unsupported_tunnel_type": "\u0412\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0439 \u0442\u0438\u043f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c." @@ -124,13 +143,22 @@ "data_description": { "rate_limit": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0442\u0435\u043b\u0435\u0433\u0440\u0430\u043c\u043c \u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0443.\n\u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u043c\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435: 0 \u0438\u043b\u0438 \u043e\u0442 20 \u0434\u043e 40 (`0` - \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u0435).", "state_updater": "\u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0434\u043b\u044f \u0447\u0442\u0435\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 \u0438\u0437 \u0448\u0438\u043d\u044b KNX. \u0415\u0441\u043b\u0438 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u043e, Home Assistant \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0430\u043a\u0442\u0438\u0432\u043d\u043e \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0441 \u0448\u0438\u043d\u044b KNX. \u041c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043e\u0442\u043c\u0435\u043d\u0435\u043d \u043e\u043f\u0446\u0438\u044f\u043c\u0438 \u043e\u0431\u044a\u0435\u043a\u0442\u0430 `sync_state`." - } + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0432\u044f\u0437\u0438" }, "connection_type": { "data": { "connection_type": "\u0422\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f KNX" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.\nAUTOMATIC \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u0438\u043d\u0435 KNX, \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0448\u043b\u044e\u0437\u0430.\nTUNNELING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435.\nROUTING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044e." + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0438\u043f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0443\u0436\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.\nAUTOMATIC \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u0438\u043d\u0435 KNX, \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044f \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0448\u043b\u044e\u0437\u0430.\nTUNNELING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435.\nROUTING \u2014 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0441\u044f \u043a \u0448\u0438\u043d\u0435 KNX, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044e.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatic` \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u0435\u0440\u0432\u0443\u044e \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u0443\u044e \u043a\u043e\u043d\u0435\u0447\u043d\u0443\u044e \u0442\u043e\u0447\u043a\u0443 \u0442\u0443\u043d\u043d\u0435\u043b\u044f." + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0443\u043d\u043d\u0435\u043b\u044c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "title": "\u041a\u043e\u043d\u0435\u0447\u043d\u0430\u044f \u0442\u043e\u0447\u043a\u0430 \u0442\u0443\u043d\u043d\u0435\u043b\u044f" }, "manual_tunnel": { "data": { @@ -146,13 +174,15 @@ "port": "\u041f\u043e\u0440\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNX/IP.", "route_back": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0435, \u0435\u0441\u043b\u0438 \u0412\u0430\u0448 \u0441\u0435\u0440\u0432\u0435\u0440 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f KNXnet/IP \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0437\u0430 NAT. \u041f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0439 UDP." }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438." + "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.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0442\u0443\u043d\u043d\u0435\u043b\u044f" }, "options_init": { "menu_options": { "communication_settings": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0441\u0432\u044f\u0437\u0438", "connection_type": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 KNX" - } + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 KNX" }, "routing": { "data": { @@ -166,7 +196,8 @@ "individual_address": "\u0410\u0434\u0440\u0435\u0441 KNX, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f Home Assistant, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, `0.0.4`", "local_ip": "\u041e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435." }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438." + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u0438.", + "title": "\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f" }, "secure_key_source": { "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 KNX/IP Secure.", @@ -174,7 +205,8 @@ "secure_knxkeys": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0444\u0430\u0439\u043b `.knxkeys`, \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0449\u0438\u0439 \u043a\u043b\u044e\u0447\u0438 IP secure", "secure_routing_manual": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c backbone-\u043a\u043b\u044e\u0447\u0438 IP Secure \u0432\u0440\u0443\u0447\u043d\u0443\u044e", "secure_tunnel_manual": "\u041d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 IP Secure \u0432\u0440\u0443\u0447\u043d\u0443\u044e" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +217,8 @@ "knxkeys_filename": "\u041e\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044f, \u0447\u0442\u043e \u0444\u0430\u0439\u043b \u0431\u0443\u0434\u0435\u0442 \u043d\u0430\u0439\u0434\u0435\u043d \u0432 \u043a\u0430\u0442\u0430\u043b\u043e\u0433\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 `.storage/knx/`.\n\u0415\u0441\u043b\u0438 \u0412\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 Home Assistant OS \u044d\u0442\u043e\u0442 \u043f\u0443\u0442\u044c \u0431\u0443\u0434\u0435\u0442 `/config/.storage/knx/`\n\u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440: `my_project.knxkeys`", "knxkeys_password": "\u042d\u0442\u043e\u0442 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u0431\u044b\u043b \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u043f\u0440\u0438 \u044d\u043a\u0441\u043f\u043e\u0440\u0442\u0435 \u0444\u0430\u0439\u043b\u0430 \u0438\u0437 ETS." }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0444\u0430\u0439\u043b\u0435 `.knxkeys`." + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0444\u0430\u0439\u043b\u0435 `.knxkeys`.", + "title": "Keyfile" }, "secure_routing_manual": { "data": { @@ -196,7 +229,8 @@ "backbone_key": "\u041c\u043e\u0436\u043d\u043e \u0443\u0432\u0438\u0434\u0435\u0442\u044c \u0432 \u043e\u0442\u0447\u0435\u0442\u0435 'Security' \u043f\u0440\u043e\u0435\u043a\u0442\u0430 ETS. Eg. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "\u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e - 1000." }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043f\u043e IP Secure." + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043f\u043e IP Secure.", + "title": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u0430\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044f" }, "secure_tunnel_manual": { "data": { @@ -209,13 +243,15 @@ "user_id": "\u0427\u0430\u0441\u0442\u043e \u043d\u043e\u043c\u0435\u0440 \u0442\u0443\u043d\u043d\u0435\u043b\u044f +1. \u0422\u0430\u043a\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c, 'Tunnel 2' \u0431\u0443\u0434\u0435\u0442 \u0438\u043c\u0435\u0442\u044c User-ID '3'.", "user_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e \u0442\u0443\u043d\u043d\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f, \u0437\u0430\u0434\u0430\u043d\u043d\u044b\u0439 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 'Properties' \u0442\u0443\u043d\u043d\u0435\u043b\u044f \u0432 ETS." }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043f\u043e IP Secure." + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043f\u043e IP Secure.", + "title": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0435 \u0442\u0443\u043d\u043d\u0435\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435" }, "tunnel": { "data": { "gateway": "\u0422\u0443\u043d\u043d\u0435\u043b\u044c\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c \u0432\u0437\u0430\u0438\u043c\u043e\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f KNX" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430." + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430.", + "title": "\u0422\u0443\u043d\u043d\u0435\u043b\u044c" } } } diff --git a/homeassistant/components/knx/translations/sk.json b/homeassistant/components/knx/translations/sk.json index 4ff1c07de1d..68e0b1f23cc 100644 --- a/homeassistant/components/knx/translations/sk.json +++ b/homeassistant/components/knx/translations/sk.json @@ -6,11 +6,13 @@ }, "error": { "cannot_connect": "Nepodarilo sa pripoji\u0165", - "file_not_found": "Zadan\u00fd s\u00fabor `.knxkeys` sa nena\u0161iel v ceste config/.storage/knx/", "invalid_backbone_key": "Neplatn\u00fd k\u013e\u00fa\u010d backbone. O\u010dak\u00e1va sa 32 hexadecim\u00e1lnych \u010d\u00edsel.", "invalid_individual_address": "Hodnota sa nezhoduje so vzorom pre individu\u00e1lnu adresu KNX.\n 'area.line.device'", "invalid_ip_address": "Neplatn\u00e1 adresa IPv4.", - "invalid_signature": "Heslo na de\u0161ifrovanie s\u00faboru `.knxkeys` je nespr\u00e1vne.", + "keyfile_invalid_signature": "Heslo na de\u0161ifrovanie s\u00faboru `.knxkeys` je nespr\u00e1vne.", + "keyfile_no_backbone_key": "S\u00fabor `.knxkeys` neobsahuje k\u013e\u00fa\u010d chrbtice pre bezpe\u010dn\u00e9 smerovanie.", + "keyfile_no_tunnel_for_host": "S\u00fabor `.knxkeys` neobsahuje poverenia pre hostite\u013ea `{host}`.", + "keyfile_not_found": "Zadan\u00fd s\u00fabor `.knxkeys` sa nena\u0161iel v ceste config/.storage/knx/", "no_router_discovered": "V sieti nebol n\u00e1jden\u00fd \u017eiadny router KNXnet/IP.", "no_tunnel_discovered": "Vo va\u0161ej sieti sa nepodarilo n\u00e1js\u0165 tunelovac\u00ed server KNX.", "unsupported_tunnel_type": "Vybran\u00fd typ tunelovania nie je podporovan\u00fd br\u00e1nou." @@ -20,7 +22,15 @@ "data": { "connection_type": "Typ pripojenia KNX" }, - "description": "Zadajte typ pripojenia, ktor\u00fd by sme mali pou\u017ei\u0165 pre va\u0161e pripojenie KNX.\n AUTOMATICKY - Integr\u00e1cia sa star\u00e1 o pripojenie k va\u0161ej zbernici KNX vykonan\u00edm skenovania br\u00e1ny.\n TUNNELING - Integr\u00e1cia sa pripoj\u00ed k va\u0161ej KNX zbernici prostredn\u00edctvom tunelovania.\n ROUTOVANIE - Integr\u00e1cia sa pripoj\u00ed k va\u0161ej KNX zbernici cez smerovanie." + "description": "Zadajte typ pripojenia, ktor\u00fd by sme mali pou\u017ei\u0165 pre va\u0161e pripojenie KNX.\n AUTOMATICKY - Integr\u00e1cia sa star\u00e1 o pripojenie k va\u0161ej zbernici KNX vykonan\u00edm skenovania br\u00e1ny.\n TUNNELING - Integr\u00e1cia sa pripoj\u00ed k va\u0161ej KNX zbernici prostredn\u00edctvom tunelovania.\n ROUTOVANIE - Integr\u00e1cia sa pripoj\u00ed k va\u0161ej KNX zbernici cez smerovanie.", + "title": "Pripojenie KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automaticky` pou\u017eije prv\u00fd vo\u013en\u00fd koncov\u00fd bod tunela." + }, + "description": "Vyberte tunel pou\u017eit\u00fd na pripojenie.", + "title": "Koncov\u00fd bod tunela" }, "manual_tunnel": { "data": { @@ -36,7 +46,8 @@ "port": "Port tunelovacieho zariadenia KNX/IP.", "route_back": "Povo\u013ete, ak je v\u00e1\u0161 server tunelovania KNXnet/IP za NAT. Plat\u00ed len pre pripojenia UDP." }, - "description": "Zadajte inform\u00e1cie o pripojen\u00ed v\u00e1\u0161ho tunelovacieho zariadenia." + "description": "Zadajte inform\u00e1cie o pripojen\u00ed v\u00e1\u0161ho tunelovacieho zariadenia.", + "title": "Nastavenia tunela" }, "routing": { "data": { @@ -50,7 +61,8 @@ "individual_address": "Adresa KNX, ktor\u00fa bude pou\u017e\u00edva\u0165 Home Assistant, napr. `0.0.4`", "local_ip": "Ak chcete pou\u017ei\u0165 automatick\u00e9 zis\u0165ovanie, nechajte pole pr\u00e1zdne." }, - "description": "Nakonfigurujte mo\u017enosti smerovania." + "description": "Nakonfigurujte mo\u017enosti smerovania.", + "title": "Routing" }, "secure_key_source": { "description": "Vyberte, ako chcete nakonfigurova\u0165 KNX/IP Secure.", @@ -58,7 +70,8 @@ "secure_knxkeys": "Pou\u017eite s\u00fabor `.knxkeys` obsahuj\u00faci bezpe\u010dnostn\u00e9 k\u013e\u00fa\u010de IP", "secure_routing_manual": "Nakonfigurujte zabezpe\u010den\u00fd k\u013e\u00fa\u010d chrbticovej siete IP manu\u00e1lne", "secure_tunnel_manual": "Manu\u00e1lne nakonfigurujte bezpe\u010dnostn\u00e9 poverenia IP" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -69,7 +82,8 @@ "knxkeys_filename": "O\u010dak\u00e1va sa, \u017ee s\u00fabor n\u00e1jdete vo va\u0161om konfigura\u010dnom adres\u00e1ri v `.storage/knx/`.\n V opera\u010dnom syst\u00e9me Home Assistant to bude `/config/.storage/knx/`\n Pr\u00edklad: `my_project.knxkeys`", "knxkeys_password": "Toto bolo nastaven\u00e9 pri exporte s\u00faboru z ETS." }, - "description": "Zadajte inform\u00e1cie pre v\u00e1\u0161 s\u00fabor `.knxkeys`." + "description": "Zadajte inform\u00e1cie pre v\u00e1\u0161 s\u00fabor `.knxkeys`.", + "title": "Keyfile" }, "secure_routing_manual": { "data": { @@ -77,10 +91,11 @@ "sync_latency_tolerance": "Tolerancia latencie siete" }, "data_description": { - "backbone_key": "Mo\u017eno ho vidie\u0165 v spr\u00e1ve \u201eBezpe\u010dnos\u0165\u201c projektu ETS. Napr. '00112233445566778899AABBCCDDEEFF'", + "backbone_key": "Mo\u017eno ho vidie\u0165 v spr\u00e1ve `Bezpe\u010dnos\u0165` projektu ETS. Napr. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Predvolen\u00e1 hodnota je 1000." }, - "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie." + "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie.", + "title": "Bezpe\u010dn\u00fd routing" }, "secure_tunnel_manual": { "data": { @@ -89,28 +104,32 @@ "user_password": "Pou\u017e\u00edvate\u013esk\u00e9 heslo" }, "data_description": { - "device_authentication": "Toto sa nastavuje na paneli \u201eIP\u201c rozhrania v ETS.", - "user_id": "Toto je \u010dasto \u010d\u00edslo tunela +1. Tak\u017ee \u201eTunnel 2\u201c bude ma\u0165 User-ID \u201e3\u201c.", - "user_password": "Heslo pre \u0161pecifick\u00e9 pripojenie tunela nastaven\u00e9 na paneli \u201eVlastnosti\u201c tunela v ETS." + "device_authentication": "Toto sa nastavuje na paneli `IP` rozhrania v ETS.", + "user_id": "Toto je \u010dasto \u010d\u00edslo tunela +1. Tak\u017ee 'Tunnel 2' bude ma\u0165 User-ID '3'.", + "user_password": "Heslo pre \u0161pecifick\u00e9 pripojenie tunela nastaven\u00e9 na paneli 'Vlastnosti' tunela v ETS." }, - "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie." + "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie.", + "title": "Bezpe\u010dn\u00e9 tuneling" }, "tunnel": { "data": { "gateway": "Pripojenie tunela KNX" }, - "description": "Vyberte br\u00e1nu zo zoznamu." + "description": "Vyberte br\u00e1nu zo zoznamu.", + "title": "Tunel" } } }, "options": { "error": { "cannot_connect": "Nepodarilo sa pripoji\u0165", - "file_not_found": "Zadan\u00fd s\u00fabor `.knxkeys` sa nena\u0161iel v ceste config/.storage/knx/", "invalid_backbone_key": "Neplatn\u00fd k\u013e\u00fa\u010d backbone. O\u010dak\u00e1va sa 32 hexadecim\u00e1lnych \u010d\u00edsel.", "invalid_individual_address": "Hodnota sa nezhoduje so vzorom pre individu\u00e1lnu adresu KNX.\n 'area.line.device'", "invalid_ip_address": "Neplatn\u00e1 adresa IPv4.", - "invalid_signature": "Heslo na de\u0161ifrovanie s\u00faboru `.knxkeys` je nespr\u00e1vne.", + "keyfile_invalid_signature": "Heslo na de\u0161ifrovanie s\u00faboru `.knxkeys` je nespr\u00e1vne.", + "keyfile_no_backbone_key": "S\u00fabor `.knxkeys` neobsahuje k\u013e\u00fa\u010d chrbtice pre bezpe\u010dn\u00e9 smerovanie.", + "keyfile_no_tunnel_for_host": "S\u00fabor `.knxkeys` neobsahuje poverenia pre hostite\u013ea `{host}`.", + "keyfile_not_found": "Zadan\u00fd s\u00fabor `.knxkeys` sa nena\u0161iel v ceste config/.storage/knx/", "no_router_discovered": "V sieti nebol n\u00e1jden\u00fd \u017eiadny router KNXnet/IP.", "no_tunnel_discovered": "Vo va\u0161ej sieti sa nepodarilo n\u00e1js\u0165 tunelovac\u00ed server KNX.", "unsupported_tunnel_type": "Vybran\u00fd typ tunelovania nie je podporovan\u00fd br\u00e1nou." @@ -124,13 +143,22 @@ "data_description": { "rate_limit": "Maxim\u00e1lny po\u010det odch\u00e1dzaj\u00facich telegramov za sekundu.\n `0` pre deaktiv\u00e1ciu limitu. Odpor\u00fa\u010dan\u00e9: 0 alebo 20 a\u017e 40", "state_updater": "Nastavi\u0165 predvolen\u00e9 pre \u010d\u00edtanie stavov zo zbernice KNX. Ke\u010f je vypnut\u00fd, Home Assistant nebude akt\u00edvne z\u00edskava\u0165 stavy ent\u00edt zo zbernice KNX. D\u00e1 sa prep\u00edsa\u0165 mo\u017enos\u0165ami entity `sync_state`." - } + }, + "title": "Nastavenia komunik\u00e1cie" }, "connection_type": { "data": { "connection_type": "Typ pripojenia KNX" }, - "description": "Zadajte typ pripojenia, ktor\u00fd by sme mali pou\u017ei\u0165 pre va\u0161e pripojenie KNX.\n AUTOMATICKY - Integr\u00e1cia sa star\u00e1 o pripojenie k va\u0161ej zbernici KNX vykonan\u00edm skenovania br\u00e1ny.\n TUNNELING - Integr\u00e1cia sa pripoj\u00ed k va\u0161ej KNX zbernici prostredn\u00edctvom tunelovania.\n ROUTOVANIE - Integr\u00e1cia sa pripoj\u00ed k va\u0161ej KNX zbernici cez smerovanie." + "description": "Zadajte typ pripojenia, ktor\u00fd by sme mali pou\u017ei\u0165 pre va\u0161e pripojenie KNX.\n AUTOMATICKY - Integr\u00e1cia sa star\u00e1 o pripojenie k va\u0161ej zbernici KNX vykonan\u00edm skenovania br\u00e1ny.\n TUNNELING - Integr\u00e1cia sa pripoj\u00ed k va\u0161ej KNX zbernici prostredn\u00edctvom tunelovania.\n ROUTOVANIE - Integr\u00e1cia sa pripoj\u00ed k va\u0161ej KNX zbernici cez smerovanie.", + "title": "Pripojenie KNX" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automaticky` pou\u017eije prv\u00fd vo\u013en\u00fd koncov\u00fd bod tunela." + }, + "description": "Vyberte tunel pou\u017eit\u00fd na pripojenie.", + "title": "Koncov\u00fd bod tunela" }, "manual_tunnel": { "data": { @@ -146,13 +174,15 @@ "port": "Port tunelovacieho zariadenia KNX/IP.", "route_back": "Povo\u013ete, ak je v\u00e1\u0161 server tunelovania KNXnet/IP za NAT. Plat\u00ed len pre pripojenia UDP." }, - "description": "Zadajte inform\u00e1cie o pripojen\u00ed v\u00e1\u0161ho tunelovacieho zariadenia." + "description": "Zadajte inform\u00e1cie o pripojen\u00ed v\u00e1\u0161ho tunelovacieho zariadenia.", + "title": "Nastavenia tunela" }, "options_init": { "menu_options": { "communication_settings": "Nastavenia komunik\u00e1cie", "connection_type": "Konfigur\u00e1cia rozhrania KNX" - } + }, + "title": "Nastavenia KNX" }, "routing": { "data": { @@ -166,7 +196,8 @@ "individual_address": "Adresa KNX, ktor\u00fa bude pou\u017e\u00edva\u0165 Home Assistant, napr. `0.0.4`", "local_ip": "Ak chcete pou\u017ei\u0165 automatick\u00e9 zis\u0165ovanie, nechajte pole pr\u00e1zdne." }, - "description": "Nakonfigurujte mo\u017enosti smerovania." + "description": "Nakonfigurujte mo\u017enosti smerovania.", + "title": "Routing" }, "secure_key_source": { "description": "Vyberte, ako chcete nakonfigurova\u0165 KNX/IP Secure.", @@ -174,7 +205,8 @@ "secure_knxkeys": "Pou\u017eite s\u00fabor `.knxkeys` obsahuj\u00faci bezpe\u010dnostn\u00e9 k\u013e\u00fa\u010de IP", "secure_routing_manual": "Nakonfigurujte zabezpe\u010den\u00fd k\u013e\u00fa\u010d chrbticovej siete IP manu\u00e1lne", "secure_tunnel_manual": "Manu\u00e1lne nakonfigurujte bezpe\u010dnostn\u00e9 poverenia IP" - } + }, + "title": "KNX IP-Secure" }, "secure_knxkeys": { "data": { @@ -185,7 +217,8 @@ "knxkeys_filename": "O\u010dak\u00e1va sa, \u017ee s\u00fabor n\u00e1jdete vo va\u0161om konfigura\u010dnom adres\u00e1ri v `.storage/knx/`.\n V opera\u010dnom syst\u00e9me Home Assistant to bude `/config/.storage/knx/`\n Pr\u00edklad: `my_project.knxkeys`", "knxkeys_password": "Toto bolo nastaven\u00e9 pri exporte s\u00faboru z ETS." }, - "description": "Zadajte inform\u00e1cie pre v\u00e1\u0161 s\u00fabor `.knxkeys`." + "description": "Zadajte inform\u00e1cie pre v\u00e1\u0161 s\u00fabor `.knxkeys`.", + "title": "Keyfile" }, "secure_routing_manual": { "data": { @@ -193,10 +226,11 @@ "sync_latency_tolerance": "Tolerancia latencie siete" }, "data_description": { - "backbone_key": "Mo\u017eno ho vidie\u0165 v spr\u00e1ve \u201eBezpe\u010dnos\u0165\u201c projektu ETS. Napr. '00112233445566778899AABBCCDDEEFF'", + "backbone_key": "Mo\u017eno ho vidie\u0165 v spr\u00e1ve `Bezpe\u010dnos\u0165` projektu ETS. Napr. '00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "Predvolen\u00e1 hodnota je 1000." }, - "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie." + "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie.", + "title": "Bezpe\u010dn\u00fd routing" }, "secure_tunnel_manual": { "data": { @@ -205,17 +239,19 @@ "user_password": "Pou\u017e\u00edvate\u013esk\u00e9 heslo" }, "data_description": { - "device_authentication": "Toto sa nastavuje na paneli \u201eIP\u201c rozhrania v ETS.", - "user_id": "Toto je \u010dasto \u010d\u00edslo tunela +1. Tak\u017ee \u201eTunnel 2\u201c bude ma\u0165 User-ID \u201e3\u201c.", - "user_password": "Heslo pre \u0161pecifick\u00e9 pripojenie tunela nastaven\u00e9 na paneli \u201eVlastnosti\u201c tunela v ETS." + "device_authentication": "Toto sa nastavuje na paneli `IP` rozhrania v ETS.", + "user_id": "Toto je \u010dasto \u010d\u00edslo tunela +1. Tak\u017ee 'Tunnel 2' bude ma\u0165 User-ID '3'.", + "user_password": "Heslo pre \u0161pecifick\u00e9 pripojenie tunela nastaven\u00e9 na paneli 'Vlastnosti' tunela v ETS." }, - "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie." + "description": "Pros\u00edm, zadajte svoje IP zabezpe\u010den\u00e9 inform\u00e1cie.", + "title": "Bezpe\u010dn\u00e9 tuneling" }, "tunnel": { "data": { "gateway": "Pripojenie tunela KNX" }, - "description": "Vyberte br\u00e1nu zo zoznamu." + "description": "Vyberte br\u00e1nu zo zoznamu.", + "title": "Tunel" } } } diff --git a/homeassistant/components/knx/translations/sv.json b/homeassistant/components/knx/translations/sv.json index 51917ee9bce..a084632343f 100644 --- a/homeassistant/components/knx/translations/sv.json +++ b/homeassistant/components/knx/translations/sv.json @@ -6,10 +6,8 @@ }, "error": { "cannot_connect": "Det gick inte att ansluta.", - "file_not_found": "Den angivna `.knxkeys`-filen hittades inte i s\u00f6kv\u00e4gen config/.storage/knx/", "invalid_individual_address": "V\u00e4rdet matchar inte m\u00f6nstret f\u00f6r en individuell adress i KNX.\n'area.line.device'", - "invalid_ip_address": "Ogiltig IPv4-adress.", - "invalid_signature": "L\u00f6senordet f\u00f6r att dekryptera `.knxkeys`-filen \u00e4r fel." + "invalid_ip_address": "Ogiltig IPv4-adress." }, "step": { "manual_tunnel": { @@ -57,5 +55,10 @@ "description": "V\u00e4lj en gateway fr\u00e5n listan." } } + }, + "options": { + "error": { + "invalid_ip_address": "Ogiltig IPv4-adress." + } } } \ No newline at end of file diff --git a/homeassistant/components/knx/translations/tr.json b/homeassistant/components/knx/translations/tr.json index 86d654adb09..f89f955bcc0 100644 --- a/homeassistant/components/knx/translations/tr.json +++ b/homeassistant/components/knx/translations/tr.json @@ -6,38 +6,72 @@ }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", - "file_not_found": "Belirtilen `.knxkeys` dosyas\u0131 config/.storage/knx/ yolunda bulunamad\u0131", + "invalid_backbone_key": "Ge\u00e7ersiz omurga anahtar\u0131. 32 onalt\u0131l\u0131k say\u0131 bekleniyor.", "invalid_individual_address": "De\u011fer, KNX bireysel adresi i\u00e7in modelle e\u015fle\u015fmiyor.\n 'alan.hat.cihaz'", "invalid_ip_address": "Ge\u00e7ersiz IPv4 adresi.", - "invalid_signature": "`.knxkeys` dosyas\u0131n\u0131n \u015fifresini \u00e7\u00f6zmek i\u00e7in \u015fifre yanl\u0131\u015f." + "keyfile_invalid_signature": "\".knxkeys\" dosyas\u0131n\u0131n \u015fifresini \u00e7\u00f6zmek i\u00e7in kullan\u0131lan parola yanl\u0131\u015f.", + "keyfile_no_backbone_key": "\".knxkeys\" dosyas\u0131, g\u00fcvenli y\u00f6nlendirme i\u00e7in bir omurga anahtar\u0131 i\u00e7ermez.", + "keyfile_no_tunnel_for_host": "\".knxkeys\" dosyas\u0131, \" {host} \" ana bilgisayar\u0131 i\u00e7in kimlik bilgileri i\u00e7ermiyor.", + "keyfile_not_found": "Belirtilen \".knxkeys\" dosyas\u0131 config/.storage/knx/ yolunda bulunamad\u0131", + "no_router_discovered": "A\u011fda hi\u00e7bir KNXnet/IP y\u00f6nlendirici bulunamad\u0131.", + "no_tunnel_discovered": "A\u011f\u0131n\u0131zda bir KNX t\u00fcnel sunucusu bulunamad\u0131.", + "unsupported_tunnel_type": "Se\u00e7ilen t\u00fcnel tipi a\u011f ge\u00e7idi taraf\u0131ndan desteklenmiyor." }, "step": { + "connection_type": { + "data": { + "connection_type": "KNX Ba\u011flant\u0131 T\u00fcr\u00fc" + }, + "description": "L\u00fctfen KNX ba\u011flant\u0131n\u0131z i\u00e7in kullanmam\u0131z gereken ba\u011flant\u0131 tipini giriniz.\n OTOMAT\u0130K - Entegrasyon, bir a\u011f ge\u00e7idi taramas\u0131 ger\u00e7ekle\u015ftirerek KNX Bus'\u0131n\u0131za olan ba\u011flant\u0131y\u0131 halleder.\n T\u00dcNELLEME - Entegrasyon, t\u00fcnelleme yoluyla KNX veri yolunuza ba\u011flanacakt\u0131r.\n Y\u00d6NLEND\u0130RME - Entegrasyon, y\u00f6nlendirme yoluyla KNX veri yolunuza ba\u011flanacakt\u0131r.", + "title": "KNX ba\u011flant\u0131s\u0131" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "\"Otomatik\", ilk bo\u015f t\u00fcnel u\u00e7 noktas\u0131n\u0131 kullanacakt\u0131r." + }, + "description": "Ba\u011flant\u0131 i\u00e7in kullan\u0131lan t\u00fcneli se\u00e7in.", + "title": "T\u00fcnel biti\u015f noktas\u0131" + }, "manual_tunnel": { "data": { "host": "Sunucu", "local_ip": "Home Asistan\u0131n\u0131n Yerel IP'si", "port": "Port", + "route_back": "Geri y\u00f6nlendirme / NAT modu", "tunneling_type": "KNX T\u00fcnel Tipi" }, "data_description": { "host": "KNX/IP t\u00fcnelleme cihaz\u0131n\u0131n IP adresi.", "local_ip": "Otomatik bulmay\u0131 kullanmak i\u00e7in bo\u015f b\u0131rak\u0131n.", - "port": "KNX/IP t\u00fcnelleme cihaz\u0131n\u0131n ba\u011flant\u0131 noktas\u0131." + "port": "KNX/IP t\u00fcnelleme cihaz\u0131n\u0131n ba\u011flant\u0131 noktas\u0131.", + "route_back": "KNXnet/IP t\u00fcnel sunucunuz NAT'\u0131n arkas\u0131ndaysa etkinle\u015ftirin. Yaln\u0131zca UDP ba\u011flant\u0131lar\u0131 i\u00e7in ge\u00e7erlidir." }, - "description": "L\u00fctfen t\u00fcnel cihaz\u0131n\u0131z\u0131n ba\u011flant\u0131 bilgilerini girin." + "description": "L\u00fctfen t\u00fcnel cihaz\u0131n\u0131z\u0131n ba\u011flant\u0131 bilgilerini girin.", + "title": "T\u00fcnel ayarlar\u0131" }, "routing": { "data": { "individual_address": "Bireysel adres", "local_ip": "Home Asistan\u0131n\u0131n Yerel IP'si", "multicast_group": "\u00c7ok noktaya yay\u0131n grubu", - "multicast_port": "\u00c7ok noktaya yay\u0131n ba\u011flant\u0131 noktas\u0131" + "multicast_port": "\u00c7ok noktaya yay\u0131n ba\u011flant\u0131 noktas\u0131", + "routing_secure": "KNX IP Secure kullan\u0131n" }, "data_description": { "individual_address": "Home Assistant taraf\u0131ndan kullan\u0131lacak KNX adresi, \u00f6r. \"0.0.4\"", "local_ip": "Otomatik bulmay\u0131 kullanmak i\u00e7in bo\u015f b\u0131rak\u0131n." }, - "description": "L\u00fctfen y\u00f6nlendirme se\u00e7eneklerini yap\u0131land\u0131r\u0131n." + "description": "L\u00fctfen y\u00f6nlendirme se\u00e7eneklerini yap\u0131land\u0131r\u0131n.", + "title": "Y\u00f6nlendirme" + }, + "secure_key_source": { + "description": "KNX/IP Secure'u nas\u0131l yap\u0131land\u0131rmak istedi\u011finizi se\u00e7in.", + "menu_options": { + "secure_knxkeys": "IP g\u00fcvenli anahtarlar\u0131 i\u00e7eren bir \".knxkeys\" dosyas\u0131 kullan\u0131n", + "secure_routing_manual": "IP g\u00fcvenli omurga anahtar\u0131n\u0131 manuel olarak yap\u0131land\u0131r\u0131n", + "secure_tunnel_manual": "IP g\u00fcvenli kimlik bilgilerini manuel olarak yap\u0131land\u0131r\u0131n" + }, + "title": "KNX IP G\u00fcvenli" }, "secure_knxkeys": { "data": { @@ -48,13 +82,176 @@ "knxkeys_filename": "Dosyan\u0131n yap\u0131land\u0131rma dizininizde `.storage/knx/` i\u00e7inde bulunmas\u0131 bekleniyor.\n Home Assistant OS'de bu, `/config/.storage/knx/` olacakt\u0131r.\n \u00d6rnek: \"my_project.knxkeys\"", "knxkeys_password": "Bu, dosyay\u0131 ETS'den d\u0131\u015fa aktar\u0131rken ayarland\u0131." }, - "description": "L\u00fctfen `.knxkeys` dosyan\u0131z i\u00e7in bilgileri girin." + "description": "L\u00fctfen `.knxkeys` dosyan\u0131z i\u00e7in bilgileri girin.", + "title": "Anahtar Dosyas\u0131" + }, + "secure_routing_manual": { + "data": { + "backbone_key": "Omurga anahtar\u0131", + "sync_latency_tolerance": "A\u011f gecikme tolerans\u0131" + }, + "data_description": { + "backbone_key": "Bir ETS projesinin 'G\u00fcvenlik' raporunda g\u00f6r\u00fclebilir. \u00d6rne\u011fin. '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "Varsay\u0131lan 1000'dir." + }, + "description": "L\u00fctfen IP g\u00fcvenlik bilgilerinizi giriniz.", + "title": "G\u00fcvenli y\u00f6nlendirme" + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Cihaz do\u011frulama \u015fifresi", + "user_id": "Kullan\u0131c\u0131 Kimli\u011fi", + "user_password": "Kullan\u0131c\u0131 \u015fifresi" + }, + "data_description": { + "device_authentication": "Bu, ETS'deki aray\u00fcz\u00fcn 'IP' panelinde ayarlan\u0131r.", + "user_id": "Bu genellikle t\u00fcnel numaras\u0131 +1'dir. Yani 'T\u00fcnel 2' Kullan\u0131c\u0131 Kimli\u011fi '3' olacakt\u0131r.", + "user_password": "ETS'de t\u00fcnelin '\u00d6zellikler' panelinde ayarlanan belirli t\u00fcnel ba\u011flant\u0131s\u0131 i\u00e7in \u015fifre." + }, + "description": "L\u00fctfen IP g\u00fcvenlik bilgilerinizi giriniz.", + "title": "G\u00fcvenli t\u00fcnelleme" }, "tunnel": { "data": { "gateway": "KNX T\u00fcnel Ba\u011flant\u0131s\u0131" }, - "description": "L\u00fctfen listeden bir a\u011f ge\u00e7idi se\u00e7in." + "description": "L\u00fctfen listeden bir a\u011f ge\u00e7idi se\u00e7in.", + "title": "T\u00fcnel" + } + } + }, + "options": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_backbone_key": "Ge\u00e7ersiz omurga anahtar\u0131. 32 onalt\u0131l\u0131k say\u0131 bekleniyor.", + "invalid_individual_address": "De\u011fer, KNX bireysel adresi i\u00e7in modelle e\u015fle\u015fmiyor.\n 'alan.hat.cihaz'", + "invalid_ip_address": "Ge\u00e7ersiz IPv4 adresi.", + "keyfile_invalid_signature": "\".knxkeys\" dosyas\u0131n\u0131n \u015fifresini \u00e7\u00f6zmek i\u00e7in kullan\u0131lan parola yanl\u0131\u015f.", + "keyfile_no_backbone_key": "\".knxkeys\" dosyas\u0131, g\u00fcvenli y\u00f6nlendirme i\u00e7in bir omurga anahtar\u0131 i\u00e7ermez.", + "keyfile_no_tunnel_for_host": "\".knxkeys\" dosyas\u0131, \" {host} \" ana bilgisayar\u0131 i\u00e7in kimlik bilgileri i\u00e7ermiyor.", + "keyfile_not_found": "Belirtilen \".knxkeys\" dosyas\u0131 config/.storage/knx/ yolunda bulunamad\u0131", + "no_router_discovered": "A\u011fda hi\u00e7bir KNXnet/IP y\u00f6nlendirici bulunamad\u0131.", + "no_tunnel_discovered": "A\u011f\u0131n\u0131zda bir KNX t\u00fcnel sunucusu bulunamad\u0131.", + "unsupported_tunnel_type": "Se\u00e7ilen t\u00fcnel tipi a\u011f ge\u00e7idi taraf\u0131ndan desteklenmiyor." + }, + "step": { + "communication_settings": { + "data": { + "rate_limit": "Saya\u00e7 Limiti", + "state_updater": "Durum g\u00fcncelleyici" + }, + "data_description": { + "rate_limit": "Saniyede maksimum giden telgraf say\u0131s\u0131.\n Limiti devre d\u0131\u015f\u0131 b\u0131rakmak i\u00e7in \"0\". \u00d6nerilen: 0 veya 20 ila 40", + "state_updater": "KNX Bus'tan okuma durumlar\u0131 i\u00e7in varsay\u0131lan\u0131 ayarlay\u0131n. Devre d\u0131\u015f\u0131 b\u0131rak\u0131ld\u0131\u011f\u0131nda, Home Assistant varl\u0131k durumlar\u0131n\u0131 KNX Bus'tan aktif olarak almaz. 'sync_state' varl\u0131k se\u00e7enekleri taraf\u0131ndan ge\u00e7ersiz k\u0131l\u0131nabilir." + }, + "title": "\u0130leti\u015fim ayarlar\u0131" + }, + "connection_type": { + "data": { + "connection_type": "KNX Ba\u011flant\u0131 T\u00fcr\u00fc" + }, + "description": "L\u00fctfen KNX ba\u011flant\u0131n\u0131z i\u00e7in kullanmam\u0131z gereken ba\u011flant\u0131 tipini giriniz.\n OTOMAT\u0130K - Entegrasyon, bir a\u011f ge\u00e7idi taramas\u0131 ger\u00e7ekle\u015ftirerek KNX Bus'\u0131n\u0131za olan ba\u011flant\u0131y\u0131 halleder.\n T\u00dcNELLEME - Entegrasyon, t\u00fcnelleme yoluyla KNX veri yolunuza ba\u011flanacakt\u0131r.\n Y\u00d6NLEND\u0130RME - Entegrasyon, y\u00f6nlendirme yoluyla KNX veri yolunuza ba\u011flanacakt\u0131r.", + "title": "KNX ba\u011flant\u0131s\u0131" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "\"Otomatik\", ilk bo\u015f t\u00fcnel u\u00e7 noktas\u0131n\u0131 kullanacakt\u0131r." + }, + "description": "Ba\u011flant\u0131 i\u00e7in kullan\u0131lan t\u00fcneli se\u00e7in.", + "title": "T\u00fcnel biti\u015f noktas\u0131" + }, + "manual_tunnel": { + "data": { + "host": "Sunucu", + "local_ip": "Home Asistan\u0131n\u0131n Yerel IP'si", + "port": "Port", + "route_back": "Geri y\u00f6nlendirme / NAT modu", + "tunneling_type": "KNX T\u00fcnel Tipi" + }, + "data_description": { + "host": "KNX/IP t\u00fcnelleme cihaz\u0131n\u0131n IP adresi.", + "local_ip": "Otomatik bulmay\u0131 kullanmak i\u00e7in bo\u015f b\u0131rak\u0131n.", + "port": "KNX/IP t\u00fcnelleme cihaz\u0131n\u0131n ba\u011flant\u0131 noktas\u0131.", + "route_back": "KNXnet/IP t\u00fcnel sunucunuz NAT'\u0131n arkas\u0131ndaysa etkinle\u015ftirin. Yaln\u0131zca UDP ba\u011flant\u0131lar\u0131 i\u00e7in ge\u00e7erlidir." + }, + "description": "L\u00fctfen t\u00fcnel cihaz\u0131n\u0131z\u0131n ba\u011flant\u0131 bilgilerini girin.", + "title": "T\u00fcnel ayarlar\u0131" + }, + "options_init": { + "menu_options": { + "communication_settings": "\u0130leti\u015fim ayarlar\u0131", + "connection_type": "KNX aray\u00fcz\u00fcn\u00fc yap\u0131land\u0131r\u0131n" + }, + "title": "KNX Ayarlar\u0131" + }, + "routing": { + "data": { + "individual_address": "Bireysel adres", + "local_ip": "Home Asistan\u0131n\u0131n Yerel IP'si", + "multicast_group": "\u00c7ok noktaya yay\u0131n grubu", + "multicast_port": "\u00c7ok noktaya yay\u0131n ba\u011flant\u0131 noktas\u0131", + "routing_secure": "KNX IP Secure kullan\u0131n" + }, + "data_description": { + "individual_address": "Home Assistant taraf\u0131ndan kullan\u0131lacak KNX adresi, \u00f6r. \"0.0.4\"", + "local_ip": "Otomatik bulmay\u0131 kullanmak i\u00e7in bo\u015f b\u0131rak\u0131n." + }, + "description": "L\u00fctfen y\u00f6nlendirme se\u00e7eneklerini yap\u0131land\u0131r\u0131n.", + "title": "Y\u00f6nlendirme" + }, + "secure_key_source": { + "description": "KNX/IP Secure'u nas\u0131l yap\u0131land\u0131rmak istedi\u011finizi se\u00e7in.", + "menu_options": { + "secure_knxkeys": "IP g\u00fcvenli anahtarlar\u0131 i\u00e7eren bir \".knxkeys\" dosyas\u0131 kullan\u0131n", + "secure_routing_manual": "IP g\u00fcvenli omurga anahtar\u0131n\u0131 manuel olarak yap\u0131land\u0131r\u0131n", + "secure_tunnel_manual": "IP g\u00fcvenli kimlik bilgilerini manuel olarak yap\u0131land\u0131r\u0131n" + }, + "title": "KNX IP G\u00fcvenli" + }, + "secure_knxkeys": { + "data": { + "knxkeys_filename": "`.knxkeys` dosyan\u0131z\u0131n dosya ad\u0131 (uzant\u0131 dahil)", + "knxkeys_password": "`.knxkeys` dosyas\u0131n\u0131n \u015fifresini \u00e7\u00f6zmek i\u00e7in \u015fifre" + }, + "data_description": { + "knxkeys_filename": "Dosyan\u0131n yap\u0131land\u0131rma dizininizde `.storage/knx/` i\u00e7inde bulunmas\u0131 bekleniyor.\n Home Assistant OS'de bu, `/config/.storage/knx/` olacakt\u0131r.\n \u00d6rnek: \"my_project.knxkeys\"", + "knxkeys_password": "Bu, dosyay\u0131 ETS'den d\u0131\u015fa aktar\u0131rken ayarland\u0131." + }, + "description": "L\u00fctfen `.knxkeys` dosyan\u0131z i\u00e7in bilgileri girin.", + "title": "Anahtar Dosyas\u0131" + }, + "secure_routing_manual": { + "data": { + "backbone_key": "Omurga anahtar\u0131", + "sync_latency_tolerance": "A\u011f gecikme tolerans\u0131" + }, + "data_description": { + "backbone_key": "Bir ETS projesinin 'G\u00fcvenlik' raporunda g\u00f6r\u00fclebilir. \u00d6rne\u011fin. '00112233445566778899AABBCCDDEEFF'", + "sync_latency_tolerance": "Varsay\u0131lan 1000'dir." + }, + "description": "L\u00fctfen IP g\u00fcvenlik bilgilerinizi giriniz.", + "title": "G\u00fcvenli y\u00f6nlendirme" + }, + "secure_tunnel_manual": { + "data": { + "device_authentication": "Cihaz do\u011frulama \u015fifresi", + "user_id": "Kullan\u0131c\u0131 Kimli\u011fi", + "user_password": "Kullan\u0131c\u0131 \u015fifresi" + }, + "data_description": { + "device_authentication": "Bu, ETS'deki aray\u00fcz\u00fcn 'IP' panelinde ayarlan\u0131r.", + "user_id": "Bu genellikle t\u00fcnel numaras\u0131 +1'dir. Yani 'T\u00fcnel 2' Kullan\u0131c\u0131 Kimli\u011fi '3' olacakt\u0131r.", + "user_password": "ETS'de t\u00fcnelin '\u00d6zellikler' panelinde ayarlanan belirli t\u00fcnel ba\u011flant\u0131s\u0131 i\u00e7in \u015fifre." + }, + "description": "L\u00fctfen IP g\u00fcvenlik bilgilerinizi giriniz.", + "title": "G\u00fcvenli t\u00fcnelleme" + }, + "tunnel": { + "data": { + "gateway": "KNX T\u00fcnel Ba\u011flant\u0131s\u0131" + }, + "description": "L\u00fctfen listeden bir a\u011f ge\u00e7idi se\u00e7in.", + "title": "T\u00fcnel" } } } diff --git a/homeassistant/components/knx/translations/uk.json b/homeassistant/components/knx/translations/uk.json new file mode 100644 index 00000000000..427a130cabd --- /dev/null +++ b/homeassistant/components/knx/translations/uk.json @@ -0,0 +1,58 @@ +{ + "config": { + "step": { + "manual_tunnel": { + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0442\u0443\u043d\u0435\u043b\u044e" + }, + "routing": { + "data": { + "local_ip": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u0438\u0439 IP Home Assistant" + }, + "title": "\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0456\u044f" + }, + "secure_routing_manual": { + "title": "\u0411\u0435\u0437\u043f\u0435\u0447\u043d\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0456\u044f" + }, + "secure_tunnel_manual": { + "title": "\u0411\u0435\u0437\u043f\u0435\u0447\u043d\u0435 \u0442\u0443\u043d\u0435\u043b\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "tunnel": { + "title": "\u0422\u0443\u043d\u0435\u043b\u044c" + } + } + }, + "options": { + "step": { + "communication_settings": { + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u0432'\u044f\u0437\u043a\u0443" + }, + "connection_type": { + "title": "\u0417'\u0454\u0434\u043d\u0430\u043d\u043d\u044f KNX" + }, + "manual_tunnel": { + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0442\u0443\u043d\u0435\u043b\u044e" + }, + "options_init": { + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f KNX" + }, + "routing": { + "title": "\u041c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0456\u044f" + }, + "secure_key_source": { + "title": "KNX IP-Secure" + }, + "secure_knxkeys": { + "title": "\u041a\u043b\u044e\u0447\u043e\u0432\u0438\u0439 \u0444\u0430\u0439\u043b" + }, + "secure_routing_manual": { + "title": "\u0411\u0435\u0437\u043f\u0435\u0447\u043d\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0456\u044f" + }, + "secure_tunnel_manual": { + "title": "\u0411\u0435\u0437\u043f\u0435\u0447\u043d\u0435 \u0442\u0443\u043d\u0435\u043b\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "tunnel": { + "title": "\u0422\u0443\u043d\u0435\u043b\u044c" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/translations/zh-Hant.json b/homeassistant/components/knx/translations/zh-Hant.json index 99ad9f9b490..7086efeeab3 100644 --- a/homeassistant/components/knx/translations/zh-Hant.json +++ b/homeassistant/components/knx/translations/zh-Hant.json @@ -6,11 +6,13 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "file_not_found": "\u8def\u5f91 config/.storage/knx/ \u5167\u627e\u4e0d\u5230\u6307\u5b9a `.knxkeys` \u6a94\u6848", "invalid_backbone_key": "Backbone \u91d1\u9470\u7121\u6548\u3002\u61c9\u70ba 32 \u500b\u5341\u516d\u9032\u4f4d\u6578\u5b57\u3002", "invalid_individual_address": "\u6578\u503c\u8207 KNX \u500b\u5225\u4f4d\u5740\u4e0d\u76f8\u7b26\u3002\n'area.line.device'", "invalid_ip_address": "IPv4 \u4f4d\u5740\u7121\u6548\u3002", - "invalid_signature": "\u52a0\u5bc6 `.knxkeys` \u6a94\u6848\u5bc6\u78bc\u932f\u8aa4\u3002", + "keyfile_invalid_signature": "\u52a0\u5bc6 `.knxkeys` \u6a94\u6848\u5bc6\u78bc\u932f\u8aa4\u3002", + "keyfile_no_backbone_key": "`.knxkeys` \u6a94\u6848\u672a\u5305\u542b\u7528\u65bc\u52a0\u5bc6\u8def\u7531\u7684\u9aa8\u5e79\u91d1\u9470\u3002", + "keyfile_no_tunnel_for_host": "`.knxkeys` \u6a94\u6848\u672a\u5305\u542b\u4e3b\u6a5f `{host}` \u6191\u8b49\u3002", + "keyfile_not_found": "\u8def\u5f91 config/.storage/knx/ \u5167\u627e\u4e0d\u5230\u6307\u5b9a `.knxkeys` \u6a94\u6848", "no_router_discovered": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 KNXnet/IP \u8def\u7531\u5668\u3002", "no_tunnel_discovered": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 KNX \u901a\u9053\u4f3a\u670d\u5668\u3002", "unsupported_tunnel_type": "\u9598\u9053\u5668\u4e0d\u652f\u63f4\u6240\u9078\u64c7\u7684\u901a\u9053\u985e\u578b\u3002" @@ -20,7 +22,15 @@ "data": { "connection_type": "KNX \u9023\u7dda\u985e\u578b" }, - "description": "\u8acb\u8f38\u5165 KNX \u9023\u7dda\u6240\u4f7f\u7528\u4e4b\u9023\u7dda\u985e\u5225\u3002 \n \u81ea\u52d5\uff08AUTOMATIC\uff09 - \u6574\u5408\u81ea\u52d5\u85c9\u7531\u9598\u9053\u5668\u6383\u63cf\u5f8c\u8655\u7406\u9023\u7dda\u554f\u984c\u3002\n \u901a\u9053\uff08TUNNELING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u901a\u9053\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002\n \u8def\u7531\uff08ROUTING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u8def\u7531\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002" + "description": "\u8acb\u8f38\u5165 KNX \u9023\u7dda\u6240\u4f7f\u7528\u4e4b\u9023\u7dda\u985e\u5225\u3002 \n \u81ea\u52d5\uff08AUTOMATIC\uff09 - \u6574\u5408\u81ea\u52d5\u85c9\u7531\u9598\u9053\u5668\u6383\u63cf\u5f8c\u8655\u7406\u9023\u7dda\u554f\u984c\u3002\n \u901a\u9053\uff08TUNNELING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u901a\u9053\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002\n \u8def\u7531\uff08ROUTING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u8def\u7531\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002", + "title": "KNX \u9023\u7dda" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatic` \u5c07\u6703\u4f7f\u7528\u7b2c\u4e00\u500b\u53ef\u4f7f\u7528\u7684\u901a\u9053\u7aef\u9ede\u3002" + }, + "description": "\u9078\u64c7\u9023\u7dda\u901a\u9053\u3002", + "title": "\u901a\u9053\u7aef\u9ede" }, "manual_tunnel": { "data": { @@ -36,7 +46,8 @@ "port": "KNX/IP \u901a\u9053\u88dd\u7f6e\u901a\u8a0a\u57e0\u3002", "route_back": "\u5047\u5982 KNXnet/IP \u901a\u9053\u4f3a\u670d\u5668\u4f4d\u65bc NAT \u6642\u555f\u7528\u3001\u50c5\u9069\u7528 UDP \u9023\u7dda\u3002" }, - "description": "\u8acb\u8f38\u5165\u901a\u9053\u88dd\u7f6e\u7684\u9023\u7dda\u8cc7\u8a0a\u3002" + "description": "\u8acb\u8f38\u5165\u901a\u9053\u88dd\u7f6e\u7684\u9023\u7dda\u8cc7\u8a0a\u3002", + "title": "\u901a\u9053\u8a2d\u5b9a" }, "routing": { "data": { @@ -50,7 +61,8 @@ "individual_address": "Home Assistant \u6240\u4f7f\u7528\u4e4b KNX \u4f4d\u5740\u3002\u4f8b\u5982\uff1a`0.0.4`", "local_ip": "\u4fdd\u6301\u7a7a\u767d\u4ee5\u4f7f\u7528\u81ea\u52d5\u641c\u7d22\u3002" }, - "description": "\u8acb\u8a2d\u5b9a\u8def\u7531\u9078\u9805\u3002" + "description": "\u8acb\u8a2d\u5b9a\u8def\u7531\u9078\u9805\u3002", + "title": "\u8def\u7531" }, "secure_key_source": { "description": "\u9078\u64c7\u5982\u4f55\u8a2d\u5b9a KNX/IP \u52a0\u5bc6\u3002", @@ -58,7 +70,8 @@ "secure_knxkeys": "\u4f7f\u7528\u5305\u542b IP \u52a0\u5bc6\u91d1\u8000\u7684 knxkeys \u6a94\u6848", "secure_routing_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6 backbone \u91d1\u9470", "secure_tunnel_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6\u6191\u8b49" - } + }, + "title": "KNX IP \u52a0\u5bc6" }, "secure_knxkeys": { "data": { @@ -69,7 +82,8 @@ "knxkeys_filename": "\u6a94\u6848\u61c9\u8a72\u4f4d\u65bc\u8a2d\u5b9a\u8cc7\u6599\u593e `.storage/knx/` \u5167\u3002\n\u82e5\u70ba Home Assistant OS\u3001\u5247\u61c9\u8a72\u70ba `/config/.storage/knx/`\n\u4f8b\u5982\uff1a`my_project.knxkeys`", "knxkeys_password": "\u81ea ETS \u532f\u51fa\u6a94\u6848\u4e2d\u9032\u884c\u8a2d\u5b9a\u3002" }, - "description": "\u8acb\u8f38\u5165 `.knxkeys` \u6a94\u6848\u8cc7\u8a0a\u3002" + "description": "\u8acb\u8f38\u5165 `.knxkeys` \u6a94\u6848\u8cc7\u8a0a\u3002", + "title": "Keyfile" }, "secure_routing_manual": { "data": { @@ -80,7 +94,8 @@ "backbone_key": "\u65bc ETS \u9805\u76ee\u7684 'Security' \u56de\u5831\u4e2d\u767c\u73fe\u3002\u4f8b\u5982\uff1a'00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "\u9810\u8a2d\u503c\u70ba 1000\u3002" }, - "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002" + "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002", + "title": "\u52a0\u5bc6\u8def\u7531" }, "secure_tunnel_manual": { "data": { @@ -93,24 +108,28 @@ "user_id": "\u901a\u5e38\u70ba\u901a\u9053\u6578 +1\u3002\u56e0\u6b64 'Tunnel 2' \u5c07\u5177\u6709\u4f7f\u7528\u8005 ID '3'\u3002", "user_password": "\u65bc ETS \u901a\u9053 'Properties' \u9762\u677f\u53ef\u8a2d\u5b9a\u6307\u5b9a\u901a\u9053\u9023\u7dda\u5bc6\u78bc\u3002" }, - "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002" + "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002", + "title": "\u52a0\u5bc6\u901a\u9053" }, "tunnel": { "data": { "gateway": "KNX \u901a\u9053\u9023\u7dda" }, - "description": "\u8acb\u5f9e\u5217\u8868\u4e2d\u9078\u64c7\u4e00\u7d44\u9598\u9053\u5668\u3002" + "description": "\u8acb\u5f9e\u5217\u8868\u4e2d\u9078\u64c7\u4e00\u7d44\u9598\u9053\u5668\u3002", + "title": "\u901a\u9053" } } }, "options": { "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "file_not_found": "\u8def\u5f91 config/.storage/knx/ \u5167\u627e\u4e0d\u5230\u6307\u5b9a `.knxkeys` \u6a94\u6848", "invalid_backbone_key": "Backbone \u91d1\u9470\u7121\u6548\u3002\u61c9\u70ba 32 \u500b\u5341\u516d\u9032\u4f4d\u6578\u5b57\u3002", "invalid_individual_address": "\u6578\u503c\u8207 KNX \u500b\u5225\u4f4d\u5740\u4e0d\u76f8\u7b26\u3002\n'area.line.device'", "invalid_ip_address": "IPv4 \u4f4d\u5740\u7121\u6548\u3002", - "invalid_signature": "\u52a0\u5bc6 `.knxkeys` \u6a94\u6848\u5bc6\u78bc\u932f\u8aa4\u3002", + "keyfile_invalid_signature": "\u52a0\u5bc6 `.knxkeys` \u6a94\u6848\u5bc6\u78bc\u932f\u8aa4\u3002", + "keyfile_no_backbone_key": "`.knxkeys` \u6a94\u6848\u672a\u5305\u542b\u7528\u65bc\u52a0\u5bc6\u8def\u7531\u7684\u9aa8\u5e79\u91d1\u9470\u3002", + "keyfile_no_tunnel_for_host": "`.knxkeys` \u6a94\u6848\u672a\u5305\u542b\u4e3b\u6a5f `{host}` \u6191\u8b49\u3002", + "keyfile_not_found": "\u8def\u5f91 config/.storage/knx/ \u5167\u627e\u4e0d\u5230\u6307\u5b9a `.knxkeys` \u6a94\u6848", "no_router_discovered": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 KNXnet/IP \u8def\u7531\u5668\u3002", "no_tunnel_discovered": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 KNX \u901a\u9053\u4f3a\u670d\u5668\u3002", "unsupported_tunnel_type": "\u9598\u9053\u5668\u4e0d\u652f\u63f4\u6240\u9078\u64c7\u7684\u901a\u9053\u985e\u578b\u3002" @@ -124,13 +143,22 @@ "data_description": { "rate_limit": "\u6bcf\u79d2\u6700\u5927 Telegram \u767c\u9001\u91cf\u3002\n`0` \u70ba\u95dc\u9589\u9650\u5236\u3001\u5efa\u8b70\uff1a0 \u6216 20 \u81f3 40", "state_updater": "\u8a2d\u5b9a\u9810\u8a2d KNX Bus \u8b80\u53d6\u72c0\u614b\u3002\u7576\u95dc\u9589\u6642\u3001Home Assistant \u5c07\u4e0d\u6703\u4e3b\u52d5\u5f9e KNX Bus \u7372\u53d6\u5be6\u9ad4\u72c0\u614b\uff0c\u53ef\u88ab`sync_state` \u5be6\u9ad4\u9078\u9805\u8986\u84cb\u3002" - } + }, + "title": "\u901a\u8a0a\u8a2d\u5b9a" }, "connection_type": { "data": { "connection_type": "KNX \u9023\u7dda\u985e\u578b" }, - "description": "\u8acb\u8f38\u5165 KNX \u9023\u7dda\u6240\u4f7f\u7528\u4e4b\u9023\u7dda\u985e\u5225\u3002 \n \u81ea\u52d5\uff08AUTOMATIC\uff09 - \u6574\u5408\u81ea\u52d5\u85c9\u7531\u9598\u9053\u5668\u6383\u63cf\u5f8c\u8655\u7406\u9023\u7dda\u554f\u984c\u3002\n \u901a\u9053\uff08TUNNELING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u901a\u9053\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002\n \u8def\u7531\uff08ROUTING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u8def\u7531\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002" + "description": "\u8acb\u8f38\u5165 KNX \u9023\u7dda\u6240\u4f7f\u7528\u4e4b\u9023\u7dda\u985e\u5225\u3002 \n \u81ea\u52d5\uff08AUTOMATIC\uff09 - \u6574\u5408\u81ea\u52d5\u85c9\u7531\u9598\u9053\u5668\u6383\u63cf\u5f8c\u8655\u7406\u9023\u7dda\u554f\u984c\u3002\n \u901a\u9053\uff08TUNNELING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u901a\u9053\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002\n \u8def\u7531\uff08ROUTING\uff09 - \u6574\u5408\u5c07\u6703\u900f\u904e\u8def\u7531\u65b9\u5f0f\u8207 KNX Bus \u9032\u884c\u9023\u7dda\u3002", + "title": "KNX \u9023\u7dda" + }, + "knxkeys_tunnel_select": { + "data": { + "user_id": "`Automatic` \u5c07\u6703\u4f7f\u7528\u7b2c\u4e00\u500b\u53ef\u4f7f\u7528\u7684\u901a\u9053\u7aef\u9ede\u3002" + }, + "description": "\u9078\u64c7\u9023\u7dda\u901a\u9053\u3002", + "title": "\u901a\u9053\u7aef\u9ede" }, "manual_tunnel": { "data": { @@ -146,13 +174,15 @@ "port": "KNX/IP \u901a\u9053\u88dd\u7f6e\u901a\u8a0a\u57e0\u3002", "route_back": "\u5047\u5982 KNXnet/IP \u901a\u9053\u4f3a\u670d\u5668\u4f4d\u65bc NAT \u6642\u555f\u7528\u3001\u50c5\u9069\u7528 UDP \u9023\u7dda\u3002" }, - "description": "\u8acb\u8f38\u5165\u901a\u9053\u88dd\u7f6e\u7684\u9023\u7dda\u8cc7\u8a0a\u3002" + "description": "\u8acb\u8f38\u5165\u901a\u9053\u88dd\u7f6e\u7684\u9023\u7dda\u8cc7\u8a0a\u3002", + "title": "\u901a\u9053\u8a2d\u5b9a" }, "options_init": { "menu_options": { "communication_settings": "\u901a\u8a0a\u8a2d\u5b9a", "connection_type": "\u8a2d\u5b9a KNX \u4ecb\u9762" - } + }, + "title": "KNX \u8a2d\u5b9a" }, "routing": { "data": { @@ -166,7 +196,8 @@ "individual_address": "Home Assistant \u6240\u4f7f\u7528\u4e4b KNX \u4f4d\u5740\u3002\u4f8b\u5982\uff1a`0.0.4`", "local_ip": "\u4fdd\u6301\u7a7a\u767d\u4ee5\u4f7f\u7528\u81ea\u52d5\u641c\u7d22\u3002" }, - "description": "\u8acb\u8a2d\u5b9a\u8def\u7531\u9078\u9805\u3002" + "description": "\u8acb\u8a2d\u5b9a\u8def\u7531\u9078\u9805\u3002", + "title": "\u8def\u7531" }, "secure_key_source": { "description": "\u9078\u64c7\u5982\u4f55\u8a2d\u5b9a KNX/IP \u52a0\u5bc6\u3002", @@ -174,7 +205,8 @@ "secure_knxkeys": "\u4f7f\u7528\u5305\u542b IP \u52a0\u5bc6\u91d1\u8000\u7684 knxkeys \u6a94\u6848", "secure_routing_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6 backbone \u91d1\u9470", "secure_tunnel_manual": "\u624b\u52d5\u8a2d\u5b9a IP \u52a0\u5bc6\u6191\u8b49" - } + }, + "title": "KNX IP \u52a0\u5bc6" }, "secure_knxkeys": { "data": { @@ -185,7 +217,8 @@ "knxkeys_filename": "\u6a94\u6848\u61c9\u8a72\u4f4d\u65bc\u8a2d\u5b9a\u8cc7\u6599\u593e `.storage/knx/` \u5167\u3002\n\u82e5\u70ba Home Assistant OS\u3001\u5247\u61c9\u8a72\u70ba `/config/.storage/knx/`\n\u4f8b\u5982\uff1a`my_project.knxkeys`", "knxkeys_password": "\u81ea ETS \u532f\u51fa\u6a94\u6848\u4e2d\u9032\u884c\u8a2d\u5b9a\u3002" }, - "description": "\u8acb\u8f38\u5165 `.knxkeys` \u6a94\u6848\u8cc7\u8a0a\u3002" + "description": "\u8acb\u8f38\u5165 `.knxkeys` \u6a94\u6848\u8cc7\u8a0a\u3002", + "title": "Keyfile" }, "secure_routing_manual": { "data": { @@ -196,7 +229,8 @@ "backbone_key": "\u65bc ETS \u9805\u76ee\u7684 'Security' \u56de\u5831\u4e2d\u767c\u73fe\u3002\u4f8b\u5982\uff1a'00112233445566778899AABBCCDDEEFF'", "sync_latency_tolerance": "\u9810\u8a2d\u503c\u70ba 1000\u3002" }, - "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002" + "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002", + "title": "\u52a0\u5bc6\u8def\u7531" }, "secure_tunnel_manual": { "data": { @@ -209,13 +243,15 @@ "user_id": "\u901a\u5e38\u70ba\u901a\u9053\u6578 +1\u3002\u56e0\u6b64 'Tunnel 2' \u5c07\u5177\u6709\u4f7f\u7528\u8005 ID '3'\u3002", "user_password": "\u65bc ETS \u901a\u9053 'Properties' \u9762\u677f\u53ef\u8a2d\u5b9a\u6307\u5b9a\u901a\u9053\u9023\u7dda\u5bc6\u78bc\u3002" }, - "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002" + "description": "\u8acb\u8f38\u5165 IP \u52a0\u5bc6\u8cc7\u8a0a\u3002", + "title": "\u52a0\u5bc6\u901a\u9053" }, "tunnel": { "data": { "gateway": "KNX \u901a\u9053\u9023\u7dda" }, - "description": "\u8acb\u5f9e\u5217\u8868\u4e2d\u9078\u64c7\u4e00\u7d44\u9598\u9053\u5668\u3002" + "description": "\u8acb\u5f9e\u5217\u8868\u4e2d\u9078\u64c7\u4e00\u7d44\u9598\u9053\u5668\u3002", + "title": "\u901a\u9053" } } } diff --git a/homeassistant/components/kodi/config_flow.py b/homeassistant/components/kodi/config_flow.py index e39a791a11b..ea1cf91b4c8 100644 --- a/homeassistant/components/kodi/config_flow.py +++ b/homeassistant/components/kodi/config_flow.py @@ -106,7 +106,7 @@ class KodiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle zeroconf discovery.""" self._host = discovery_info.host self._port = discovery_info.port or DEFAULT_PORT - self._name = discovery_info.hostname[: -len(".local.")] + self._name = discovery_info.hostname.removesuffix(".local.") if not (uuid := discovery_info.properties.get("uuid")): return self.async_abort(reason="no_uuid") diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index bdbac455dd1..1ebc5ad6b80 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -6,11 +6,10 @@ from datetime import timedelta from functools import wraps import logging import re -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from jsonrpc_base.jsonrpc import ProtocolError, TransportError from pykodi import CannotConnectError -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.components import media_source diff --git a/homeassistant/components/kodi/translations/sk.json b/homeassistant/components/kodi/translations/sk.json index 1d604a7c425..e788ca1a3eb 100644 --- a/homeassistant/components/kodi/translations/sk.json +++ b/homeassistant/components/kodi/translations/sk.json @@ -31,13 +31,13 @@ "port": "Port", "ssl": "Pou\u017e\u00edva SSL certifik\u00e1t" }, - "description": "Inform\u00e1cie o pripojen\u00ed Kodi. Nezabudnite povoli\u0165 \u201ePovoli\u0165 ovl\u00e1danie Kodi cez HTTP\u201c v \u010dasti Syst\u00e9m/Nastavenia/Sie\u0165/Slu\u017eby." + "description": "Inform\u00e1cie o pripojen\u00ed Kodi. Nezabudnite povoli\u0165 \"Povoli\u0165 ovl\u00e1danie Kodi cez HTTP\" v \u010dasti Syst\u00e9m/Nastavenia/Sie\u0165/Slu\u017eby." }, "ws_port": { "data": { "ws_port": "Port" }, - "description": "Port WebSocket (niekedy naz\u00fdvan\u00fd TCP port v Kodi). Ak sa chcete pripoji\u0165 cez WebSocket, mus\u00edte povoli\u0165 \u201ePovoli\u0165 programom ... ovl\u00e1da\u0165 Kodi\u201c v \u010dasti Syst\u00e9m/Nastavenia/Sie\u0165/Slu\u017eby. Ak WebSocket nie je povolen\u00fd, odstr\u00e1\u0148te port a nechajte ho pr\u00e1zdny." + "description": "Port WebSocket (niekedy naz\u00fdvan\u00fd TCP port v Kodi). Ak sa chcete pripoji\u0165 cez WebSocket, mus\u00edte povoli\u0165 \"Povoli\u0165 programom ... ovl\u00e1da\u0165 Kodi\" v \u010dasti Syst\u00e9m/Nastavenia/Sie\u0165/Slu\u017eby. Ak WebSocket nie je povolen\u00fd, odstr\u00e1\u0148te port a nechajte ho pr\u00e1zdny." } } }, diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 620ed12ac54..f82bb17db62 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -259,7 +259,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # async_connect will handle retries until it establishes a connection await client.async_connect() - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # config entry specific data to enable unload hass.data[DOMAIN][entry.entry_id] = { diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index fcf94a38c18..c9889dd6464 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -191,11 +191,11 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.data[CONF_ID] = status.get("chipId", status["mac"].replace(":", "")) except (CannotConnect, KeyError) as err: raise CannotConnect from err - else: - self.data[CONF_MODEL] = status.get("model", KONN_MODEL) - self.data[CONF_ACCESS_TOKEN] = "".join( - random.choices(f"{string.ascii_uppercase}{string.digits}", k=20) - ) + + self.data[CONF_MODEL] = status.get("model", KONN_MODEL) + self.data[CONF_ACCESS_TOKEN] = "".join( + random.choices(f"{string.ascii_uppercase}{string.digits}", k=20) + ) async def async_step_import(self, device_config): """Import a configuration.yaml config. @@ -282,19 +282,17 @@ class KonnectedFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): status = await get_status(self.hass, netloc[0], int(netloc[1])) except CannotConnect: return self.async_abort(reason="cannot_connect") - else: - self.data[CONF_HOST] = netloc[0] - self.data[CONF_PORT] = int(netloc[1]) - self.data[CONF_ID] = status.get( - "chipId", status["mac"].replace(":", "") - ) - self.data[CONF_MODEL] = status.get("model", KONN_MODEL) - KonnectedFlowHandler.discovered_hosts[self.data[CONF_ID]] = { - CONF_HOST: self.data[CONF_HOST], - CONF_PORT: self.data[CONF_PORT], - } - return await self.async_step_confirm() + self.data[CONF_HOST] = netloc[0] + self.data[CONF_PORT] = int(netloc[1]) + self.data[CONF_ID] = status.get("chipId", status["mac"].replace(":", "")) + self.data[CONF_MODEL] = status.get("model", KONN_MODEL) + + KonnectedFlowHandler.discovered_hosts[self.data[CONF_ID]] = { + CONF_HOST: self.data[CONF_HOST], + CONF_PORT: self.data[CONF_PORT], + } + return await self.async_step_confirm() return self.async_abort(reason="unknown") diff --git a/homeassistant/components/konnected/translations/el.json b/homeassistant/components/konnected/translations/el.json index b75982d16fa..ae384f09d78 100644 --- a/homeassistant/components/konnected/translations/el.json +++ b/homeassistant/components/konnected/translations/el.json @@ -39,7 +39,7 @@ "options_binary": { "data": { "inverse": "\u0391\u03bd\u03c4\u03b9\u03c3\u03c4\u03c1\u03bf\u03c6\u03ae \u03c4\u03b7\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03b1\u03bd\u03bf\u03af\u03b3\u03bc\u03b1\u03c4\u03bf\u03c2/\u03ba\u03bb\u03b5\u03b9\u03c3\u03af\u03bc\u03b1\u03c4\u03bf\u03c2", - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", "type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03b4\u03c5\u03b1\u03b4\u03b9\u03ba\u03bf\u03cd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1" }, "description": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 {zone}", @@ -47,8 +47,8 @@ }, "options_digital": { "data": { - "name": "\u038c\u03bd\u03bf\u03bc\u03b1 (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", - "poll_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b7\u03bc\u03bf\u03c3\u03ba\u03cc\u03c0\u03b7\u03c3\u03b7\u03c2 (\u03bb\u03b5\u03c0\u03c4\u03ac) (\u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03cc)", + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "poll_interval": "\u0394\u03b9\u03ac\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b4\u03b7\u03bc\u03bf\u03c3\u03ba\u03cc\u03c0\u03b7\u03c3\u03b7\u03c2 (\u03bb\u03b5\u03c0\u03c4\u03ac)", "type": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1" }, "description": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 {zone}", @@ -84,7 +84,7 @@ }, "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)", + "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", "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" @@ -95,11 +95,11 @@ "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", - "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)", + "momentary": "\u0394\u03b9\u03ac\u03c1\u03ba\u03b5\u03b9\u03b1 \u03c0\u03b1\u03bb\u03bc\u03bf\u03cd (ms)", "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)" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1", + "pause": "\u03a0\u03b1\u03cd\u03c3\u03b7 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd \u03c0\u03b1\u03bb\u03bc\u03ce\u03bd (ms)", + "repeat": "\u03a6\u03bf\u03c1\u03ad\u03c2 \u03b5\u03c0\u03b1\u03bd\u03ac\u03bb\u03b7\u03c8\u03b7\u03c2 (-1=\u03ac\u03c0\u03b5\u03b9\u03c1\u03b5\u03c2)" }, "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/lv.json b/homeassistant/components/konnected/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/konnected/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kostal_plenticore/__init__.py b/homeassistant/components/kostal_plenticore/__init__.py index 24e8ab9f0d3..b7e4c86f772 100644 --- a/homeassistant/components/kostal_plenticore/__init__.py +++ b/homeassistant/components/kostal_plenticore/__init__.py @@ -1,7 +1,7 @@ """The Kostal Plenticore Solar Inverter integration.""" import logging -from kostal.plenticore import PlenticoreApiException +from pykoplenti import ApiException from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -39,7 +39,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: plenticore = hass.data[DOMAIN].pop(entry.entry_id) try: await plenticore.async_unload() - except PlenticoreApiException as err: + except ApiException as err: _LOGGER.error("Error logging out from inverter: %s", err) return unload_ok diff --git a/homeassistant/components/kostal_plenticore/config_flow.py b/homeassistant/components/kostal_plenticore/config_flow.py index 359efef651a..cbbaeefd85d 100644 --- a/homeassistant/components/kostal_plenticore/config_flow.py +++ b/homeassistant/components/kostal_plenticore/config_flow.py @@ -3,7 +3,7 @@ import asyncio import logging from aiohttp.client_exceptions import ClientError -from kostal.plenticore import PlenticoreApiClient, PlenticoreAuthenticationException +from pykoplenti import ApiClient, AuthenticationException import voluptuous as vol from homeassistant import config_entries @@ -30,7 +30,7 @@ async def test_connection(hass: HomeAssistant, data) -> str: """ session = async_get_clientsession(hass) - async with PlenticoreApiClient(session, data["host"]) as client: + async with ApiClient(session, data["host"]) as client: await client.login(data["password"]) values = await client.get_setting_values("scb:network", "Hostname") @@ -52,7 +52,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: hostname = await test_connection(self.hass, user_input) - except PlenticoreAuthenticationException as ex: + except AuthenticationException as ex: errors[CONF_PASSWORD] = "invalid_auth" _LOGGER.error("Error response: %s", ex) except (ClientError, asyncio.TimeoutError): diff --git a/homeassistant/components/kostal_plenticore/helper.py b/homeassistant/components/kostal_plenticore/helper.py index 52ff3a7480b..cb43486dbe0 100644 --- a/homeassistant/components/kostal_plenticore/helper.py +++ b/homeassistant/components/kostal_plenticore/helper.py @@ -9,11 +9,7 @@ import logging from typing import Any, TypeVar, cast from aiohttp.client_exceptions import ClientError -from kostal.plenticore import ( - PlenticoreApiClient, - PlenticoreApiException, - PlenticoreAuthenticationException, -) +from pykoplenti import ApiClient, ApiException, AuthenticationException from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant @@ -48,18 +44,16 @@ class Plenticore: return self.config_entry.data[CONF_HOST] @property - def client(self) -> PlenticoreApiClient: + def client(self) -> ApiClient: """Return the Plenticore API client.""" return self._client async def async_setup(self) -> bool: """Set up Plenticore API client.""" - self._client = PlenticoreApiClient( - async_get_clientsession(self.hass), host=self.host - ) + self._client = ApiClient(async_get_clientsession(self.hass), host=self.host) try: await self._client.login(self.config_entry.data[CONF_PASSWORD]) - except PlenticoreAuthenticationException as err: + except AuthenticationException as err: _LOGGER.error( "Authentication exception connecting to %s: %s", self.host, err ) @@ -135,7 +129,7 @@ class DataUpdateCoordinatorMixin: try: return await client.get_setting_values(module_id, data_id) - except PlenticoreApiException: + except ApiException: return None async def async_write_data(self, module_id: str, value: dict[str, str]) -> bool: @@ -149,10 +143,10 @@ class DataUpdateCoordinatorMixin: try: await client.set_setting_values(module_id, value) - except PlenticoreApiException: + except ApiException: return False - else: - return True + + return True class PlenticoreUpdateCoordinator(DataUpdateCoordinator[_DataT]): diff --git a/homeassistant/components/kostal_plenticore/manifest.json b/homeassistant/components/kostal_plenticore/manifest.json index 71f71cae993..edbee8f6fbe 100644 --- a/homeassistant/components/kostal_plenticore/manifest.json +++ b/homeassistant/components/kostal_plenticore/manifest.json @@ -3,7 +3,7 @@ "name": "Kostal Plenticore Solar Inverter", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/kostal_plenticore", - "requirements": ["kostal_plenticore==0.2.0"], + "requirements": ["pykoplenti==1.0.0"], "codeowners": ["@stegm"], "iot_class": "local_polling", "loggers": ["kostal"] diff --git a/homeassistant/components/kostal_plenticore/number.py b/homeassistant/components/kostal_plenticore/number.py index 2b0726e6255..3acb80030cf 100644 --- a/homeassistant/components/kostal_plenticore/number.py +++ b/homeassistant/components/kostal_plenticore/number.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from datetime import timedelta import logging -from kostal.plenticore import SettingsData +from pykoplenti import SettingsData from homeassistant.components.number import ( NumberDeviceClass, diff --git a/homeassistant/components/kostal_plenticore/translations/lv.json b/homeassistant/components/kostal_plenticore/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/kostal_plenticore/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kraken/sensor.py b/homeassistant/components/kraken/sensor.py index d37ecfea889..dc86fb73d9b 100644 --- a/homeassistant/components/kraken/sensor.py +++ b/homeassistant/components/kraken/sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import Optional from homeassistant.components.sensor import SensorEntity, SensorStateClass from homeassistant.config_entries import ConfigEntry @@ -90,7 +89,7 @@ async def async_setup_entry( class KrakenSensor( - CoordinatorEntity[DataUpdateCoordinator[Optional[KrakenResponse]]], SensorEntity + CoordinatorEntity[DataUpdateCoordinator[KrakenResponse | None]], SensorEntity ): """Define a Kraken sensor.""" diff --git a/homeassistant/components/kraken/translations/tr.json b/homeassistant/components/kraken/translations/tr.json index 0d54a302a50..f9bc67d2625 100644 --- a/homeassistant/components/kraken/translations/tr.json +++ b/homeassistant/components/kraken/translations/tr.json @@ -13,7 +13,7 @@ "one": "Bo\u015f", "other": "Bo\u015f" }, - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } }, diff --git a/homeassistant/components/kulersky/translations/tr.json b/homeassistant/components/kulersky/translations/tr.json index 3df15466f03..d8dbccfea8a 100644 --- a/homeassistant/components/kulersky/translations/tr.json +++ b/homeassistant/components/kulersky/translations/tr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } } diff --git a/homeassistant/components/lacrosse/sensor.py b/homeassistant/components/lacrosse/sensor.py index a9edea22b0d..fb2c60b32c9 100644 --- a/homeassistant/components/lacrosse/sensor.py +++ b/homeassistant/components/lacrosse/sensor.py @@ -1,8 +1,9 @@ """Support for LaCrosse sensor components.""" from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta import logging +from typing import Any import pylacrosse from serial import SerialException @@ -25,7 +26,7 @@ from homeassistant.const import ( PERCENTAGE, UnitOfTemperature, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -44,7 +45,7 @@ CONF_TOGGLE_INTERVAL = "toggle_interval" CONF_TOGGLE_MASK = "toggle_mask" DEFAULT_DEVICE = "/dev/ttyUSB0" -DEFAULT_BAUD = "57600" +DEFAULT_BAUD = 57600 DEFAULT_EXPIRE_AFTER = 300 TYPES = ["battery", "humidity", "temperature"] @@ -61,7 +62,7 @@ SENSOR_SCHEMA = vol.Schema( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_SENSORS): cv.schema_with_slug_keys(SENSOR_SCHEMA), - vol.Optional(CONF_BAUD, default=DEFAULT_BAUD): cv.string, + vol.Optional(CONF_BAUD, default=DEFAULT_BAUD): cv.positive_int, vol.Optional(CONF_DATARATE): cv.positive_int, vol.Optional(CONF_DEVICE, default=DEFAULT_DEVICE): cv.string, vol.Optional(CONF_FREQUENCY): cv.positive_int, @@ -79,9 +80,9 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the LaCrosse sensors.""" - usb_device = config[CONF_DEVICE] - baud = int(config[CONF_BAUD]) - expire_after = config.get(CONF_EXPIRE_AFTER) + usb_device: str = config[CONF_DEVICE] + baud: int = config[CONF_BAUD] + expire_after: int | None = config.get(CONF_EXPIRE_AFTER) _LOGGER.debug("%s %s", usb_device, baud) @@ -92,7 +93,7 @@ def setup_platform( _LOGGER.warning("Unable to open serial port: %s", exc) return - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: lacrosse.close()) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda event: lacrosse.close()) # type: ignore[no-any-return] if CONF_JEELINK_LED in config: lacrosse.led_mode_state(config.get(CONF_JEELINK_LED)) @@ -107,13 +108,13 @@ def setup_platform( lacrosse.start_scan() - sensors = [] + sensors: list[LaCrosseSensor] = [] for device, device_config in config[CONF_SENSORS].items(): _LOGGER.debug("%s %s", device, device_config) - typ = device_config.get(CONF_TYPE) + typ: str = device_config[CONF_TYPE] sensor_class = TYPE_CLASSES[typ] - name = device_config.get(CONF_NAME, device) + name: str = device_config.get(CONF_NAME, device) sensors.append( sensor_class(hass, lacrosse, device, name, expire_after, device_config) @@ -125,21 +126,28 @@ def setup_platform( class LaCrosseSensor(SensorEntity): """Implementation of a Lacrosse sensor.""" - _temperature = None - _humidity = None - _low_battery = None - _new_battery = None + _temperature: float | None = None + _humidity: int | None = None + _low_battery: bool | None = None + _new_battery: bool | None = None - def __init__(self, hass, lacrosse, device_id, name, expire_after, config): + def __init__( + self, + hass: HomeAssistant, + lacrosse: pylacrosse.LaCrosse, + device_id: str, + name: str, + expire_after: int | None, + config: ConfigType, + ) -> None: """Initialize the sensor.""" self.hass = hass self.entity_id = async_generate_entity_id( ENTITY_ID_FORMAT, device_id, hass=hass ) self._config = config - self._value = None self._expire_after = expire_after - self._expiration_trigger = None + self._expiration_trigger: CALLBACK_TYPE | None = None self._attr_name = name lacrosse.register_callback( @@ -147,14 +155,16 @@ class LaCrosseSensor(SensorEntity): ) @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return { "low_battery": self._low_battery, "new_battery": self._new_battery, } - def _callback_lacrosse(self, lacrosse_sensor, user_data): + def _callback_lacrosse( + self, lacrosse_sensor: pylacrosse.LaCrosseSensor, user_data: None + ) -> None: """Handle a function that is called from pylacrosse with new values.""" if self._expire_after is not None and self._expire_after > 0: # Reset old trigger @@ -175,10 +185,9 @@ class LaCrosseSensor(SensorEntity): self._new_battery = lacrosse_sensor.new_battery @callback - def value_is_expired(self, *_): + def value_is_expired(self, *_: datetime) -> None: """Triggered when value is expired.""" self._expiration_trigger = None - self._value = None self.async_write_ha_state() @@ -190,7 +199,7 @@ class LaCrosseTemperature(LaCrosseSensor): _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS @property - def native_value(self): + def native_value(self) -> float | None: """Return the state of the sensor.""" return self._temperature @@ -203,7 +212,7 @@ class LaCrosseHumidity(LaCrosseSensor): _attr_icon = "mdi:water-percent" @property - def native_value(self): + def native_value(self) -> int | None: """Return the state of the sensor.""" return self._humidity @@ -212,7 +221,7 @@ class LaCrosseBattery(LaCrosseSensor): """Implementation of a Lacrosse battery sensor.""" @property - def native_value(self): + def native_value(self) -> str | None: """Return the state of the sensor.""" if self._low_battery is None: return None @@ -221,7 +230,7 @@ class LaCrosseBattery(LaCrosseSensor): return "ok" @property - def icon(self): + def icon(self) -> str: """Icon to use in the frontend.""" if self._low_battery is None: return "mdi:battery-unknown" @@ -230,7 +239,7 @@ class LaCrosseBattery(LaCrosseSensor): return "mdi:battery" -TYPE_CLASSES = { +TYPE_CLASSES: dict[str, type[LaCrosseSensor]] = { "temperature": LaCrosseTemperature, "humidity": LaCrosseHumidity, "battery": LaCrosseBattery, diff --git a/homeassistant/components/lacrosse_view/sensor.py b/homeassistant/components/lacrosse_view/sensor.py index 7ef154015cb..1c2daa2ba4a 100644 --- a/homeassistant/components/lacrosse_view/sensor.py +++ b/homeassistant/components/lacrosse_view/sensor.py @@ -14,8 +14,10 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + DEGREE, PERCENTAGE, UnitOfPrecipitationDepth, + UnitOfPressure, UnitOfSpeed, UnitOfTemperature, ) @@ -33,7 +35,7 @@ from .const import DOMAIN, LOGGER class LaCrosseSensorEntityDescriptionMixin: """Mixin for required keys.""" - value_fn: Callable[[Sensor, str], float] + value_fn: Callable[[Sensor, str], float | int | str | None] @dataclass @@ -43,9 +45,17 @@ class LaCrosseSensorEntityDescription( """Description for LaCrosse View sensor.""" -def get_value(sensor: Sensor, field: str) -> float: +def get_value(sensor: Sensor, field: str) -> float | int | str | None: """Get the value of a sensor field.""" - return float(sensor.data[field]["values"][-1]["s"]) + field_data = sensor.data.get(field) + if field_data is None: + return None + value = field_data["values"][-1]["s"] + try: + value = float(value) + except ValueError: + return str(value) # handle non-numericals + return int(value) if value.is_integer() else value PARALLEL_UPDATES = 0 @@ -90,6 +100,46 @@ SENSOR_DESCRIPTIONS = { native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES, device_class=SensorDeviceClass.PRECIPITATION, ), + "WindHeading": LaCrosseSensorEntityDescription( + key="WindHeading", + name="Wind heading", + value_fn=get_value, + native_unit_of_measurement=DEGREE, + ), + "WetDry": LaCrosseSensorEntityDescription( + key="WetDry", + name="Wet/Dry", + value_fn=get_value, + ), + "Flex": LaCrosseSensorEntityDescription( + key="Flex", + name="Flex", + value_fn=get_value, + ), + "BarometricPressure": LaCrosseSensorEntityDescription( + key="BarometricPressure", + name="Barometric pressure", + state_class=SensorStateClass.MEASUREMENT, + value_fn=get_value, + device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE, + native_unit_of_measurement=UnitOfPressure.HPA, + ), + "FeelsLike": LaCrosseSensorEntityDescription( + key="FeelsLike", + name="Feels like", + state_class=SensorStateClass.MEASUREMENT, + value_fn=get_value, + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), + "WindChill": LaCrosseSensorEntityDescription( + key="WindChill", + name="Wind chill", + state_class=SensorStateClass.MEASUREMENT, + value_fn=get_value, + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), } @@ -163,8 +213,16 @@ class LaCrosseViewSensor( self.index = index @property - def native_value(self) -> float | str: + def native_value(self) -> int | float | str | None: """Return the sensor value.""" return self.entity_description.value_fn( self.coordinator.data[self.index], self.entity_description.key ) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return ( + super().available + and self.entity_description.key in self.coordinator.data[self.index].data + ) diff --git a/homeassistant/components/lacrosse_view/translations/lv.json b/homeassistant/components/lacrosse_view/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lacrosse_view/translations/uk.json b/homeassistant/components/lacrosse_view/translations/uk.json new file mode 100644 index 00000000000..2aed6be91ba --- /dev/null +++ b/homeassistant/components/lacrosse_view/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/config_flow.py b/homeassistant/components/lametric/config_flow.py index 7496fc51a4e..4d4ebc15850 100644 --- a/homeassistant/components/lametric/config_flow.py +++ b/homeassistant/components/lametric/config_flow.py @@ -116,7 +116,10 @@ class LaMetricFlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN): async def async_step_choice_enter_manual_or_fetch_cloud( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Handle the user's choice of entering the manual credentials or fetching the cloud credentials.""" + """Handle the user's choice. + + Either enter the manual credentials or fetch the cloud credentials. + """ return self.async_show_menu( step_id="choice_enter_manual_or_fetch_cloud", menu_options=["pick_implementation", "manual_entry"], diff --git a/homeassistant/components/lametric/helpers.py b/homeassistant/components/lametric/helpers.py index 6ca3157be0c..884e6c451bc 100644 --- a/homeassistant/components/lametric/helpers.py +++ b/homeassistant/components/lametric/helpers.py @@ -2,10 +2,9 @@ from __future__ import annotations from collections.abc import Callable, Coroutine -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from demetriek import LaMetricConnectionError, LaMetricError -from typing_extensions import Concatenate, ParamSpec from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/lametric/services.yaml b/homeassistant/components/lametric/services.yaml index 5e8db5f7da4..3299245fbc0 100644 --- a/homeassistant/components/lametric/services.yaml +++ b/homeassistant/components/lametric/services.yaml @@ -103,8 +103,8 @@ chart: value: "positive5" - label: "Positive 6" value: "positive6" - - label: "Static" - value: "static" + - label: "Statistic" + value: "statistic" - label: "Thunder" value: "thunder" - label: "Water 1" diff --git a/homeassistant/components/lametric/translations/cs.json b/homeassistant/components/lametric/translations/cs.json index 3b3f849ac95..18920d8f89b 100644 --- a/homeassistant/components/lametric/translations/cs.json +++ b/homeassistant/components/lametric/translations/cs.json @@ -4,7 +4,8 @@ "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", - "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", diff --git a/homeassistant/components/lametric/translations/el.json b/homeassistant/components/lametric/translations/el.json index 24b48466fee..2b569b8d4ae 100644 --- a/homeassistant/components/lametric/translations/el.json +++ b/homeassistant/components/lametric/translations/el.json @@ -56,7 +56,7 @@ }, "issues": { "manual_migration": { - "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 LaMetric \u03ad\u03c7\u03b5\u03b9 \u03b5\u03ba\u03c3\u03c5\u03b3\u03c7\u03c1\u03bf\u03bd\u03b9\u03c3\u03c4\u03b5\u03af: \u03a4\u03ce\u03c1\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af \u03ba\u03b1\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03bc\u03ad\u03c3\u03c9 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03bf\u03b9 \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b5\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03c4\u03bf\u03c0\u03b9\u03ba\u03ad\u03c2. \n\n \u0394\u03c5\u03c3\u03c4\u03c5\u03c7\u03ce\u03c2, \u03b4\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7\u03c2 \u03bc\u03b5\u03c4\u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03ba\u03b1\u03b9, \u03c9\u03c2 \u03b5\u03ba \u03c4\u03bf\u03cd\u03c4\u03bf\u03c5, \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03b1\u03c0\u03cc \u03b5\u03c3\u03ac\u03c2 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf LaMetric \u03c3\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf Home Assistant. \u03a3\u03c5\u03bc\u03b2\u03bf\u03c5\u03bb\u03b5\u03c5\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Home Assistant LaMetric \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03b1\u03bb\u03b9\u03ac \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML LaMetric \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 LaMetric \u03ad\u03c7\u03b5\u03b9 \u03b5\u03ba\u03c3\u03c5\u03b3\u03c7\u03c1\u03bf\u03bd\u03b9\u03c3\u03c4\u03b5\u03af: \u03a4\u03ce\u03c1\u03b1 \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03ba\u03b1\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03bc\u03ad\u03c3\u03c9 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03bf\u03b9 \u03b5\u03c0\u03b9\u03ba\u03bf\u03b9\u03bd\u03c9\u03bd\u03af\u03b5\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03c4\u03bf\u03c0\u03b9\u03ba\u03ad\u03c2. \n\n \u0394\u03c5\u03c3\u03c4\u03c5\u03c7\u03ce\u03c2, \u03b4\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b4\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7\u03c2 \u03bc\u03b5\u03c4\u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03ba\u03b1\u03b9, \u03c9\u03c2 \u03b5\u03ba \u03c4\u03bf\u03cd\u03c4\u03bf\u03c5, \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03b1\u03c0\u03cc \u03b5\u03c3\u03ac\u03c2 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac \u03c4\u03bf LaMetric \u03c3\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf Home Assistant. \u03a3\u03c5\u03bc\u03b2\u03bf\u03c5\u03bb\u03b5\u03c5\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Home Assistant LaMetric \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03b1\u03bb\u03b9\u03ac \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 LaMetric YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", "title": "\u0391\u03c0\u03b1\u03b9\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03c7\u03b5\u03b9\u03c1\u03bf\u03ba\u03af\u03bd\u03b7\u03c4\u03b7 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf LaMetric" } } diff --git a/homeassistant/components/lametric/translations/hu.json b/homeassistant/components/lametric/translations/hu.json index 8506d7fe5df..610b20a0dd6 100644 --- a/homeassistant/components/lametric/translations/hu.json +++ b/homeassistant/components/lametric/translations/hu.json @@ -5,7 +5,7 @@ "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a hiteles\u00edt\u00e9si URL gener\u00e1l\u00e1sa sor\u00e1n.", "invalid_discovery_info": "\u00c9rv\u00e9nytelen felfedez\u00e9si inform\u00e1ci\u00f3 \u00e9rkezett", "link_local_address": "A linklocal c\u00edmek nem t\u00e1mogatottak", - "missing_configuration": "A LaMetric integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", + "missing_configuration": "A LaMetric integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rem, k\u00f6vesse a dokument\u00e1ci\u00f3t.", "no_devices": "A jogosult felhaszn\u00e1l\u00f3 nem rendelkezik LaMetric-eszk\u00f6z\u00f6kkel", "no_url_available": "Nincs el\u00e9rhet\u0151 URL. A hib\u00e1r\u00f3l tov\u00e1bbi inform\u00e1ci\u00f3 [a s\u00fag\u00f3ban]({docs_url}) tal\u00e1lhat\u00f3.", "reauth_device_not_found": "Az \u00fajb\u00f3l hiteles\u00edteni k\u00edv\u00e1nt eszk\u00f6z nem tal\u00e1lhat\u00f3 ebben a LaMetric-fi\u00f3kban", @@ -56,7 +56,7 @@ }, "issues": { "manual_migration": { - "description": "A LaMetric integr\u00e1ci\u00f3 moderniz\u00e1l\u00e1sra ker\u00fclt: A konfigur\u00e1l\u00e1s \u00e9s be\u00e1ll\u00edt\u00e1s mostant\u00f3l a felhaszn\u00e1l\u00f3i fel\u00fcleten kereszt\u00fcl t\u00f6rt\u00e9nik, \u00e9s a kommunik\u00e1ci\u00f3 mostant\u00f3l helyi.\n\nSajnos nincs lehet\u0151s\u00e9g automatikus migr\u00e1ci\u00f3s \u00fatvonalra, \u00edgy a LaMetric-et \u00fajra be kell \u00e1ll\u00edtania a Home Assistant seg\u00edts\u00e9g\u00e9vel. K\u00e9rj\u00fck, tekintse meg a Home Assistant LaMetric integr\u00e1ci\u00f3 dokument\u00e1ci\u00f3j\u00e1t a be\u00e1ll\u00edt\u00e1ssal kapcsolatban.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a r\u00e9gi LaMetric YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistant-ot.", + "description": "A LaMetric integr\u00e1ci\u00f3 moderniz\u00e1l\u00e1sra ker\u00fclt: A konfigur\u00e1l\u00e1s \u00e9s be\u00e1ll\u00edt\u00e1s mostant\u00f3l a felhaszn\u00e1l\u00f3i fel\u00fcleten kereszt\u00fcl t\u00f6rt\u00e9nik, \u00e9s a kommunik\u00e1ci\u00f3 mostant\u00f3l helyi.\n\nSajnos nincs lehet\u0151s\u00e9g automatikus migr\u00e1ci\u00f3s \u00fatvonalra, \u00edgy a LaMetric-et \u00fajra be kell \u00e1ll\u00edtania a Home Assistant seg\u00edts\u00e9g\u00e9vel. K\u00e9rem, tekintse meg a Home Assistant LaMetric integr\u00e1ci\u00f3 dokument\u00e1ci\u00f3j\u00e1t a be\u00e1ll\u00edt\u00e1ssal kapcsolatban.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a r\u00e9gi LaMetric YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistant-ot.", "title": "Manu\u00e1lis migr\u00e1ci\u00f3 sz\u00fcks\u00e9ges a LaMetric eset\u00e9ben" } } diff --git a/homeassistant/components/lametric/translations/lv.json b/homeassistant/components/lametric/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/lametric/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/nl.json b/homeassistant/components/lametric/translations/nl.json index d598679de04..f362eae7d8e 100644 --- a/homeassistant/components/lametric/translations/nl.json +++ b/homeassistant/components/lametric/translations/nl.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Apparaat is al geconfigureerd", "authorize_url_timeout": "Time-out bij het genereren van autorisatie-URL.", + "link_local_address": "Link local adressen worden niet ondersteund", + "missing_configuration": "De LaMetric-integratie is niet geconfigureerd. Volg de documentatie.", + "no_devices": "De geautoriseerde gebruiker heeft geen LaMetric apparaten", "no_url_available": "Geen URL beschikbaar. Voor informatie over deze fout, [raadpleeg de documentatie]({docs_url})", "reauth_successful": "Herauthenticatie geslaagd", "unknown": "Onverwachte fout" diff --git a/homeassistant/components/lametric/translations/select.sv.json b/homeassistant/components/lametric/translations/select.sv.json new file mode 100644 index 00000000000..a23b382dc49 --- /dev/null +++ b/homeassistant/components/lametric/translations/select.sv.json @@ -0,0 +1,8 @@ +{ + "state": { + "lametric__brightness_mode": { + "auto": "Automatisk", + "manual": "Manuell" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lametric/translations/tr.json b/homeassistant/components/lametric/translations/tr.json index 5362e625f83..aa0625c4383 100644 --- a/homeassistant/components/lametric/translations/tr.json +++ b/homeassistant/components/lametric/translations/tr.json @@ -44,9 +44,19 @@ } } }, + "entity": { + "select": { + "brightness_mode": { + "state": { + "auto": "Otomatik", + "manual": "Manuel" + } + } + } + }, "issues": { "manual_migration": { - "description": "LaMetric entegrasyonu modernle\u015ftirildi: Art\u0131k kullan\u0131c\u0131 aray\u00fcz\u00fc \u00fczerinden yap\u0131land\u0131r\u0131ld\u0131 ve kuruldu ve ileti\u015fimler art\u0131k yerel. \n\n Maalesef otomatik ge\u00e7i\u015f yolu m\u00fcmk\u00fcn de\u011fildir ve bu nedenle LaMetric'inizi Home Assistant ile yeniden kurman\u0131z\u0131 gerektirir. L\u00fctfen nas\u0131l kurulaca\u011f\u0131na ili\u015fkin Home Assistant LaMetric entegrasyon belgelerine bak\u0131n. \n\n Bu sorunu d\u00fczeltmek i\u00e7in eski LaMetric YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "description": "LaMetric entegrasyonu modernize edildi: Art\u0131k kullan\u0131c\u0131 arabirimi arac\u0131l\u0131\u011f\u0131yla yap\u0131land\u0131r\u0131lm\u0131\u015f ve ayarlanm\u0131\u015ft\u0131r ve ileti\u015fim art\u0131k yereldir. \n\n Ne yaz\u0131k ki, m\u00fcmk\u00fcn olan bir otomatik ge\u00e7i\u015f yolu yoktur ve bu nedenle LaMetric'inizi Home Assistant ile yeniden kurman\u0131z\u0131 gerektirir. Nas\u0131l kurulaca\u011f\u0131yla ilgili olarak l\u00fctfen Home Assistant LaMetric entegrasyon belgelerine bak\u0131n. \n\n Configuration.yaml dosyan\u0131zdan eski LaMetric YAML yap\u0131land\u0131rmas\u0131n\u0131 kald\u0131r\u0131n ve bu sorunu \u00e7\u00f6zmek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", "title": "LaMetric i\u00e7in manuel ge\u00e7i\u015f gerekli" } } diff --git a/homeassistant/components/lametric/translations/zh-Hant.json b/homeassistant/components/lametric/translations/zh-Hant.json index 6ee5ea2f1b4..bb175735f79 100644 --- a/homeassistant/components/lametric/translations/zh-Hant.json +++ b/homeassistant/components/lametric/translations/zh-Hant.json @@ -21,7 +21,7 @@ "description": "\u6709\u5169\u7a2e\u4e0d\u540c\u65b9\u6cd5\u53ef\u4ee5\u5c07 LaMetric \u88dd\u7f6e\u6574\u5408\u9032 Home Assistant\u3002\n\n\u53ef\u4ee5\u81ea\u884c\u8f38\u5165 API \u6b0a\u6756\u8207\u5168\u90e8\u88dd\u7f6e\u8cc7\u8a0a\uff0c\u6216\u8005 Home Asssistant \u53ef\u4ee5\u7531 LaMetric.com \u5e33\u865f\u9032\u884c\u532f\u5165\u3002", "menu_options": { "manual_entry": "\u624b\u52d5\u8f38\u5165", - "pick_implementation": "\u7531 LaMetric.com \u532f\u5165\uff08\u5efa\u8b70\uff09" + "pick_implementation": "\u7531 LaMetric.com \u532f\u5165\uff08\u63a8\u85a6\uff09" } }, "manual_entry": { diff --git a/homeassistant/components/landisgyr_heat_meter/__init__.py b/homeassistant/components/landisgyr_heat_meter/__init__.py index 34724c07ca9..6ef17cf24da 100644 --- a/homeassistant/components/landisgyr_heat_meter/__init__.py +++ b/homeassistant/components/landisgyr_heat_meter/__init__.py @@ -5,6 +5,7 @@ from datetime import timedelta import logging import ultraheat_api +from ultraheat_api.response import HeatMeterResponse from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_DEVICE, Platform @@ -26,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: reader = ultraheat_api.UltraheatReader(entry.data[CONF_DEVICE]) api = ultraheat_api.HeatMeterService(reader) - async def async_update_data(): + async def async_update_data() -> HeatMeterResponse: """Fetch data from the API.""" _LOGGER.debug("Polling on %s", entry.data[CONF_DEVICE]) return await hass.async_add_executor_job(api.read) @@ -42,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/landisgyr_heat_meter/manifest.json b/homeassistant/components/landisgyr_heat_meter/manifest.json index a20225c88b0..fe873b936d5 100644 --- a/homeassistant/components/landisgyr_heat_meter/manifest.json +++ b/homeassistant/components/landisgyr_heat_meter/manifest.json @@ -7,7 +7,6 @@ "ssdp": [], "zeroconf": [], "homekit": {}, - "dependencies": [], "codeowners": ["@vpathuis"], "dependencies": ["usb"], "iot_class": "local_polling" diff --git a/homeassistant/components/landisgyr_heat_meter/sensor.py b/homeassistant/components/landisgyr_heat_meter/sensor.py index 2b4fc6edea8..284fb5b7f30 100644 --- a/homeassistant/components/landisgyr_heat_meter/sensor.py +++ b/homeassistant/components/landisgyr_heat_meter/sensor.py @@ -4,12 +4,21 @@ from __future__ import annotations from dataclasses import asdict import logging -from homeassistant.components.sensor import RestoreSensor, SensorDeviceClass +from ultraheat_api.response import HeatMeterResponse + +from homeassistant.components.sensor import ( + RestoreSensor, + SensorDeviceClass, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from homeassistant.util import dt as dt_util from . import DOMAIN @@ -23,7 +32,9 @@ async def async_setup_entry( ) -> None: """Set up the sensor platform.""" unique_id = entry.entry_id - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: DataUpdateCoordinator[HeatMeterResponse] = hass.data[DOMAIN][ + entry.entry_id + ] model = entry.data["model"] @@ -42,16 +53,21 @@ async def async_setup_entry( async_add_entities(sensors) -class HeatMeterSensor(CoordinatorEntity, RestoreSensor): +class HeatMeterSensor( + CoordinatorEntity[DataUpdateCoordinator[HeatMeterResponse]], RestoreSensor +): """Representation of a Sensor.""" - def __init__(self, coordinator, description, device): + def __init__( + self, + coordinator: DataUpdateCoordinator[HeatMeterResponse], + description: SensorEntityDescription, + device: DeviceInfo, + ) -> None: """Set up the sensor with the initial values.""" super().__init__(coordinator) self.key = description.key - self._attr_unique_id = ( - f"{coordinator.config_entry.data['device_number']}_{description.key}" - ) + self._attr_unique_id = f"{coordinator.config_entry.data['device_number']}_{description.key}" # type: ignore[union-attr] self._attr_name = f"Heat Meter {description.name}" self.entity_description = description diff --git a/homeassistant/components/landisgyr_heat_meter/translations/lv.json b/homeassistant/components/landisgyr_heat_meter/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/landisgyr_heat_meter/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/laundrify/strings.json b/homeassistant/components/laundrify/strings.json index b2fea9f307f..ae6ec34d264 100644 --- a/homeassistant/components/laundrify/strings.json +++ b/homeassistant/components/laundrify/strings.json @@ -19,6 +19,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } } diff --git a/homeassistant/components/laundrify/translations/bg.json b/homeassistant/components/laundrify/translations/bg.json index 4721ecf584e..a58e092d86f 100644 --- a/homeassistant/components/laundrify/translations/bg.json +++ b/homeassistant/components/laundrify/translations/bg.json @@ -1,6 +1,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", "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, "error": { diff --git a/homeassistant/components/laundrify/translations/ca.json b/homeassistant/components/laundrify/translations/ca.json index 99b94ecfd77..31873336bd0 100644 --- a/homeassistant/components/laundrify/translations/ca.json +++ b/homeassistant/components/laundrify/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El compte ja est\u00e0 configurat", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "error": { diff --git a/homeassistant/components/laundrify/translations/de.json b/homeassistant/components/laundrify/translations/de.json index 5df595b2dd2..1758fa28de5 100644 --- a/homeassistant/components/laundrify/translations/de.json +++ b/homeassistant/components/laundrify/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Konto wurde bereits konfiguriert", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { diff --git a/homeassistant/components/laundrify/translations/en.json b/homeassistant/components/laundrify/translations/en.json index 0c8375746ed..39ab8eefe93 100644 --- a/homeassistant/components/laundrify/translations/en.json +++ b/homeassistant/components/laundrify/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Account is already configured", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { diff --git a/homeassistant/components/laundrify/translations/et.json b/homeassistant/components/laundrify/translations/et.json index cec7d1ec849..a330e8a66f4 100644 --- a/homeassistant/components/laundrify/translations/et.json +++ b/homeassistant/components/laundrify/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Kasutaja on juba seadistatud", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." }, "error": { diff --git a/homeassistant/components/laundrify/translations/no.json b/homeassistant/components/laundrify/translations/no.json index fc2a24414d1..3410bdcda25 100644 --- a/homeassistant/components/laundrify/translations/no.json +++ b/homeassistant/components/laundrify/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Kontoen er allerede konfigurert", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { diff --git a/homeassistant/components/laundrify/translations/ru.json b/homeassistant/components/laundrify/translations/ru.json index cceb04e1601..fe616d901a5 100644 --- a/homeassistant/components/laundrify/translations/ru.json +++ b/homeassistant/components/laundrify/translations/ru.json @@ -1,6 +1,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.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "error": { diff --git a/homeassistant/components/laundrify/translations/zh-Hant.json b/homeassistant/components/laundrify/translations/zh-Hant.json index 834ebb9125b..89d7964b369 100644 --- a/homeassistant/components/laundrify/translations/zh-Hant.json +++ b/homeassistant/components/laundrify/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index e9fb2683f4f..776ad116f4a 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -5,7 +5,7 @@ import asyncio from copy import deepcopy from itertools import chain import re -from typing import Union, cast +from typing import TypeAlias, cast import pypck import voluptuous as vol @@ -60,9 +60,10 @@ from .const import ( # typing AddressType = tuple[int, int, bool] -DeviceConnectionType = Union[ - pypck.module.ModuleConnection, pypck.module.GroupConnection -] +DeviceConnectionType: TypeAlias = ( + pypck.module.ModuleConnection | pypck.module.GroupConnection +) + InputType = type[pypck.inputs.Input] # Regex for address validation diff --git a/homeassistant/components/lcn/translations/uk.json b/homeassistant/components/lcn/translations/uk.json new file mode 100644 index 00000000000..7bdec322735 --- /dev/null +++ b/homeassistant/components/lcn/translations/uk.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "transmitter": "\u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043e \u043a\u043e\u0434 \u043f\u0435\u0440\u0435\u0434\u0430\u0432\u0430\u0447\u0430", + "transponder": "\u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043e \u043a\u043e\u0434 \u0442\u0440\u0430\u043d\u0441\u043f\u043e\u043d\u0434\u0435\u0440\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/__init__.py b/homeassistant/components/ld2410_ble/__init__.py new file mode 100644 index 00000000000..204a5367e0b --- /dev/null +++ b/homeassistant/components/ld2410_ble/__init__.py @@ -0,0 +1,94 @@ +"""The LD2410 BLE integration.""" + +import logging + +from bleak_retry_connector import BleakError, get_device +from ld2410_ble import LD2410BLE + +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ADDRESS, EVENT_HOMEASSISTANT_STOP, Platform +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN +from .coordinator import LD2410BLECoordinator +from .models import LD2410BLEData + +PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up LD2410 BLE from a config entry.""" + address: str = entry.data[CONF_ADDRESS] + ble_device = bluetooth.async_ble_device_from_address( + hass, address.upper(), True + ) or await get_device(address) + if not ble_device: + raise ConfigEntryNotReady( + f"Could not find LD2410B device with address {address}" + ) + ld2410_ble = LD2410BLE(ble_device) + + coordinator = LD2410BLECoordinator(hass, ld2410_ble) + + try: + await ld2410_ble.initialise() + except BleakError as exc: + raise ConfigEntryNotReady( + f"Could not initialise LD2410B device with address {address}" + ) from exc + + @callback + def _async_update_ble( + service_info: bluetooth.BluetoothServiceInfoBleak, + change: bluetooth.BluetoothChange, + ) -> None: + """Update from a ble callback.""" + ld2410_ble.set_ble_device_and_advertisement_data( + service_info.device, service_info.advertisement + ) + + entry.async_on_unload( + bluetooth.async_register_callback( + hass, + _async_update_ble, + BluetoothCallbackMatcher({ADDRESS: address}), + bluetooth.BluetoothScanningMode.ACTIVE, + ) + ) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = LD2410BLEData( + entry.title, ld2410_ble, coordinator + ) + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + + async def _async_stop(event: Event) -> None: + """Close the connection.""" + await ld2410_ble.stop() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop) + ) + return True + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + data: LD2410BLEData = hass.data[DOMAIN][entry.entry_id] + if entry.title != data.title: + await hass.config_entries.async_reload(entry.entry_id) + + +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: LD2410BLEData = hass.data[DOMAIN].pop(entry.entry_id) + await data.device.stop() + + return unload_ok diff --git a/homeassistant/components/ld2410_ble/binary_sensor.py b/homeassistant/components/ld2410_ble/binary_sensor.py new file mode 100644 index 00000000000..ab3c8ddea0b --- /dev/null +++ b/homeassistant/components/ld2410_ble/binary_sensor.py @@ -0,0 +1,83 @@ +"""LD2410 BLE integration binary sensor platform.""" + + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import LD2410BLE, LD2410BLECoordinator +from .const import DOMAIN +from .models import LD2410BLEData + +ENTITY_DESCRIPTIONS = ( + BinarySensorEntityDescription( + key="is_moving", + device_class=BinarySensorDeviceClass.MOTION, + has_entity_name=True, + name="Motion", + ), + BinarySensorEntityDescription( + key="is_static", + device_class=BinarySensorDeviceClass.OCCUPANCY, + has_entity_name=True, + name="Occupancy", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the platform for LD2410BLE.""" + data: LD2410BLEData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + LD2410BLEBinarySensor(data.coordinator, data.device, entry.title, description) + for description in ENTITY_DESCRIPTIONS + ) + + +class LD2410BLEBinarySensor( + CoordinatorEntity[LD2410BLECoordinator], BinarySensorEntity +): + """Moving/static sensor for LD2410BLE.""" + + def __init__( + self, + coordinator: LD2410BLECoordinator, + device: LD2410BLE, + name: str, + description: BinarySensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self._coordinator = coordinator + self._key = description.key + self._device = device + self.entity_description = description + self._attr_unique_id = f"{device.address}_{self._key}" + self._attr_device_info = DeviceInfo( + name=name, + connections={(dr.CONNECTION_BLUETOOTH, device.address)}, + ) + self._attr_is_on = getattr(self._device, self._key) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_is_on = getattr(self._device, self._key) + self.async_write_ha_state() + + @property + def available(self) -> bool: + """Unavailable if coordinator isn't connected.""" + return self._coordinator.connected and super().available diff --git a/homeassistant/components/ld2410_ble/config_flow.py b/homeassistant/components/ld2410_ble/config_flow.py new file mode 100644 index 00000000000..0d441c8647f --- /dev/null +++ b/homeassistant/components/ld2410_ble/config_flow.py @@ -0,0 +1,112 @@ +"""Config flow for LD2410BLE integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from bluetooth_data_tools import human_readable_name +from ld2410_ble import BLEAK_EXCEPTIONS, LD2410BLE +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN, LOCAL_NAMES + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for LD2410 BLE.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_devices: dict[str, BluetoothServiceInfoBleak] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + self._discovery_info = discovery_info + self.context["title_placeholders"] = { + "name": human_readable_name( + None, discovery_info.name, discovery_info.address + ) + } + return await self.async_step_user() + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + errors: dict[str, str] = {} + + if user_input is not None: + address = user_input[CONF_ADDRESS] + discovery_info = self._discovered_devices[address] + local_name = discovery_info.name + await self.async_set_unique_id( + discovery_info.address, raise_on_progress=False + ) + self._abort_if_unique_id_configured() + ld2410_ble = LD2410BLE(discovery_info.device) + try: + await ld2410_ble.initialise() + except BLEAK_EXCEPTIONS: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected error") + errors["base"] = "unknown" + else: + await ld2410_ble.stop() + return self.async_create_entry( + title=local_name, + data={ + CONF_ADDRESS: discovery_info.address, + }, + ) + + if discovery := self._discovery_info: + self._discovered_devices[discovery.address] = discovery + else: + current_addresses = self._async_current_ids() + for discovery in async_discovered_service_info(self.hass): + if ( + discovery.address in current_addresses + or discovery.address in self._discovered_devices + or not any( + discovery.name.startswith(local_name) + for local_name in LOCAL_NAMES + ) + ): + continue + self._discovered_devices[discovery.address] = discovery + + if not self._discovered_devices: + return self.async_abort(reason="no_unconfigured_devices") + + data_schema = vol.Schema( + { + vol.Required(CONF_ADDRESS): vol.In( + { + service_info.address: f"{service_info.name} ({service_info.address})" + for service_info in self._discovered_devices.values() + } + ), + } + ) + return self.async_show_form( + step_id="user", + data_schema=data_schema, + errors=errors, + ) diff --git a/homeassistant/components/ld2410_ble/const.py b/homeassistant/components/ld2410_ble/const.py new file mode 100644 index 00000000000..d5e723dc069 --- /dev/null +++ b/homeassistant/components/ld2410_ble/const.py @@ -0,0 +1,5 @@ +"""Constants for the LD2410 BLE integration.""" + +DOMAIN = "ld2410_ble" + +LOCAL_NAMES = {"HLK-LD2410B"} diff --git a/homeassistant/components/ld2410_ble/coordinator.py b/homeassistant/components/ld2410_ble/coordinator.py new file mode 100644 index 00000000000..6ab25553094 --- /dev/null +++ b/homeassistant/components/ld2410_ble/coordinator.py @@ -0,0 +1,40 @@ +"""Data coordinator for receiving LD2410B updates.""" + +import logging + +from ld2410_ble import LD2410BLE, LD2410BLEState + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class LD2410BLECoordinator(DataUpdateCoordinator[None]): + """Data coordinator for receiving LD2410B updates.""" + + def __init__(self, hass: HomeAssistant, ld2410_ble: LD2410BLE) -> None: + """Initialise the coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + ) + self._ld2410_ble = ld2410_ble + ld2410_ble.register_callback(self._async_handle_update) + ld2410_ble.register_disconnected_callback(self._async_handle_disconnect) + self.connected = False + + @callback + def _async_handle_update(self, state: LD2410BLEState) -> None: + """Just trigger the callbacks.""" + self.connected = True + self.async_set_updated_data(None) + + @callback + def _async_handle_disconnect(self) -> None: + """Trigger the callbacks for disconnected.""" + self.connected = False + self.async_update_listeners() diff --git a/homeassistant/components/ld2410_ble/manifest.json b/homeassistant/components/ld2410_ble/manifest.json new file mode 100644 index 00000000000..df7ddff7018 --- /dev/null +++ b/homeassistant/components/ld2410_ble/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "ld2410_ble", + "name": "LD2410 BLE", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ld2410_ble/", + "requirements": ["bluetooth-data-tools==0.3.1", "ld2410-ble==0.1.1"], + "dependencies": ["bluetooth_adapters"], + "codeowners": ["@930913"], + "bluetooth": [{ "local_name": "HLK-LD2410B_*" }], + "integration_type": "device", + "iot_class": "local_push" +} diff --git a/homeassistant/components/ld2410_ble/models.py b/homeassistant/components/ld2410_ble/models.py new file mode 100644 index 00000000000..e2666277495 --- /dev/null +++ b/homeassistant/components/ld2410_ble/models.py @@ -0,0 +1,17 @@ +"""The ld2410 ble integration models.""" +from __future__ import annotations + +from dataclasses import dataclass + +from ld2410_ble import LD2410BLE + +from .coordinator import LD2410BLECoordinator + + +@dataclass +class LD2410BLEData: + """Data for the ld2410 ble integration.""" + + title: str + device: LD2410BLE + coordinator: LD2410BLECoordinator diff --git a/homeassistant/components/ld2410_ble/sensor.py b/homeassistant/components/ld2410_ble/sensor.py new file mode 100644 index 00000000000..5b3d8ec32b9 --- /dev/null +++ b/homeassistant/components/ld2410_ble/sensor.py @@ -0,0 +1,184 @@ +"""LD2410 BLE integration sensor platform.""" + + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfLength +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo, EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import LD2410BLE, LD2410BLECoordinator +from .const import DOMAIN +from .models import LD2410BLEData + +MOVING_TARGET_DISTANCE_DESCRIPTION = SensorEntityDescription( + key="moving_target_distance", + device_class=SensorDeviceClass.DISTANCE, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + has_entity_name=True, + name="Moving Target Distance", + native_unit_of_measurement=UnitOfLength.CENTIMETERS, + state_class=SensorStateClass.MEASUREMENT, +) + +STATIC_TARGET_DISTANCE_DESCRIPTION = SensorEntityDescription( + key="static_target_distance", + device_class=SensorDeviceClass.DISTANCE, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + has_entity_name=True, + name="Static Target Distance", + native_unit_of_measurement=UnitOfLength.CENTIMETERS, + state_class=SensorStateClass.MEASUREMENT, +) + +DETECTION_DISTANCE_DESCRIPTION = SensorEntityDescription( + key="detection_distance", + device_class=SensorDeviceClass.DISTANCE, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + has_entity_name=True, + name="Detection Distance", + native_unit_of_measurement=UnitOfLength.CENTIMETERS, + state_class=SensorStateClass.MEASUREMENT, +) + +MOVING_TARGET_ENERGY_DESCRIPTION = SensorEntityDescription( + key="moving_target_energy", + device_class=None, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + has_entity_name=True, + name="Moving Target Energy", + native_unit_of_measurement="Target Energy", + state_class=SensorStateClass.MEASUREMENT, +) + +STATIC_TARGET_ENERGY_DESCRIPTION = SensorEntityDescription( + key="static_target_energy", + device_class=None, + entity_registry_enabled_default=False, + entity_registry_visible_default=True, + has_entity_name=True, + name="Static Target Energy", + native_unit_of_measurement="Target Energy", + state_class=SensorStateClass.MEASUREMENT, +) + +MAX_MOTION_GATES_DESCRIPTION = SensorEntityDescription( + key="max_motion_gates", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + has_entity_name=True, + name="Max Motion Gates", + native_unit_of_measurement="Gates", +) + +MAX_STATIC_GATES_DESCRIPTION = SensorEntityDescription( + key="max_static_gates", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + has_entity_name=True, + name="Max Static Gates", + native_unit_of_measurement="Gates", +) + +MOTION_ENERGY_GATES = [ + SensorEntityDescription( + key=f"motion_energy_gate_{i}", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + has_entity_name=True, + name=f"Motion Energy Gate {i}", + native_unit_of_measurement="Target Energy", + ) + for i in range(0, 9) +] + +STATIC_ENERGY_GATES = [ + SensorEntityDescription( + key=f"static_energy_gate_{i}", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + has_entity_name=True, + name=f"Static Energy Gate {i}", + native_unit_of_measurement="Target Energy", + ) + for i in range(0, 9) +] + +SENSOR_DESCRIPTIONS = ( + [ + MOVING_TARGET_DISTANCE_DESCRIPTION, + STATIC_TARGET_DISTANCE_DESCRIPTION, + MOVING_TARGET_ENERGY_DESCRIPTION, + STATIC_TARGET_ENERGY_DESCRIPTION, + DETECTION_DISTANCE_DESCRIPTION, + MAX_MOTION_GATES_DESCRIPTION, + MAX_STATIC_GATES_DESCRIPTION, + ] + + MOTION_ENERGY_GATES + + STATIC_ENERGY_GATES +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the platform for LD2410BLE.""" + data: LD2410BLEData = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + LD2410BLESensor( + data.coordinator, + data.device, + entry.title, + description, + ) + for description in SENSOR_DESCRIPTIONS + ) + + +class LD2410BLESensor(CoordinatorEntity[LD2410BLECoordinator], SensorEntity): + """Generic sensor for LD2410BLE.""" + + def __init__( + self, + coordinator: LD2410BLECoordinator, + device: LD2410BLE, + name: str, + description: SensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self._coordinator = coordinator + self._device = device + self._key = description.key + self.entity_description = description + self._attr_unique_id = f"{device.address}_{self._key}" + self._attr_device_info = DeviceInfo( + name=name, + connections={(dr.CONNECTION_BLUETOOTH, device.address)}, + ) + self._attr_native_value = getattr(self._device, self._key) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_native_value = getattr(self._device, self._key) + self.async_write_ha_state() + + @property + def available(self) -> bool: + """Unavailable if coordinator isn't connected.""" + return self._coordinator.connected and super().available diff --git a/homeassistant/components/ld2410_ble/strings.json b/homeassistant/components/ld2410_ble/strings.json new file mode 100644 index 00000000000..79540552575 --- /dev/null +++ b/homeassistant/components/ld2410_ble/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth address" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "not_supported": "Device not supported", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_unconfigured_devices": "No unconfigured devices found.", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" + } + } +} diff --git a/homeassistant/components/ld2410_ble/translations/bg.json b/homeassistant/components/ld2410_ble/translations/bg.json new file mode 100644 index 00000000000..146600109d3 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/bg.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "no_devices_found": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "no_unconfigured_devices": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u043d\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", + "not_supported": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u043d\u0435 \u0441\u0435 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth \u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/ca.json b/homeassistant/components/ld2410_ble/translations/ca.json new file mode 100644 index 00000000000..8ec41bb97db --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "no_unconfigured_devices": "No s'han trobat dispositius no configurats.", + "not_supported": "Dispositiu no compatible" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Adre\u00e7a Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/cs.json b/homeassistant/components/ld2410_ble/translations/cs.json new file mode 100644 index 00000000000..e1bf8e7f45f --- /dev/null +++ b/homeassistant/components/ld2410_ble/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/ld2410_ble/translations/de.json b/homeassistant/components/ld2410_ble/translations/de.json new file mode 100644 index 00000000000..d7ffdfb67c2 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "no_unconfigured_devices": "Keine unkonfigurierten Ger\u00e4te gefunden.", + "not_supported": "Ger\u00e4t nicht unterst\u00fctzt" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth-Adresse" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/el.json b/homeassistant/components/ld2410_ble/translations/el.json new file mode 100644 index 00000000000..bae0a5c5f8f --- /dev/null +++ b/homeassistant/components/ld2410_ble/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", + "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", + "no_unconfigured_devices": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03bc\u03b7 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2.", + "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" + }, + "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": { + "user": { + "data": { + "address": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/en.json b/homeassistant/components/ld2410_ble/translations/en.json new file mode 100644 index 00000000000..75356b78460 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "no_devices_found": "No devices found on the network", + "no_unconfigured_devices": "No unconfigured devices found.", + "not_supported": "Device not supported" + }, + "error": { + "cannot_connect": "Failed to connect", + "unknown": "Unexpected error" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth address" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/es.json b/homeassistant/components/ld2410_ble/translations/es.json new file mode 100644 index 00000000000..2fc60e86dd5 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", + "no_devices_found": "No se encontraron dispositivos en la red", + "no_unconfigured_devices": "No se encontraron dispositivos no configurados.", + "not_supported": "Dispositivo no compatible" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "unknown": "Error inesperado" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Direcci\u00f3n Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/et.json b/homeassistant/components/ld2410_ble/translations/et.json new file mode 100644 index 00000000000..b9742076dbc --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine juba k\u00e4ib", + "no_devices_found": "V\u00f5rgust seadmeid ei leitud", + "no_unconfigured_devices": "H\u00e4\u00e4lestamata seadmeid ei leitud.", + "not_supported": "Seadet ei toetata" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetoothi aadress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/fr.json b/homeassistant/components/ld2410_ble/translations/fr.json new file mode 100644 index 00000000000..d0e00344152 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/fr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "no_unconfigured_devices": "Aucun appareil non configur\u00e9 n'a \u00e9t\u00e9 trouv\u00e9.", + "not_supported": "Appareil non pris en charge" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Adresse Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/he.json b/homeassistant/components/ld2410_ble/translations/he.json new file mode 100644 index 00000000000..71d1ab81ca2 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/he.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", + "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", + "no_devices_found": "\u05dc\u05d0 \u05e0\u05de\u05e6\u05d0\u05d5 \u05d4\u05ea\u05e7\u05e0\u05d9\u05dd \u05d1\u05e8\u05e9\u05ea", + "not_supported": "\u05d4\u05ea\u05e7\u05df \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da" + }, + "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" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/hu.json b/homeassistant/components/ld2410_ble/translations/hu.json new file mode 100644 index 00000000000..6b6e576abcc --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "no_unconfigured_devices": "Nem tal\u00e1lhat\u00f3 konfigur\u00e1latlan eszk\u00f6z.", + "not_supported": "Eszk\u00f6z nem t\u00e1mogatott" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth-c\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/id.json b/homeassistant/components/ld2410_ble/translations/id.json new file mode 100644 index 00000000000..80afbbcf132 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "already_in_progress": "Alur konfigurasi sedang berlangsung", + "no_devices_found": "Tidak ada perangkat yang ditemukan di jaringan", + "no_unconfigured_devices": "Tidak ditemukan perangkat yang tidak dikonfigurasi.", + "not_supported": "Perangkat tidak didukung" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Alamat Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/it.json b/homeassistant/components/ld2410_ble/translations/it.json new file mode 100644 index 00000000000..ef919547573 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "no_unconfigured_devices": "Non sono stati trovati dispositivi non configurati.", + "not_supported": "Dispositivo non supportato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Indirizzo Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/ja.json b/homeassistant/components/ld2410_ble/translations/ja.json new file mode 100644 index 00000000000..c9a492f9e0c --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/ja.json @@ -0,0 +1,21 @@ +{ + "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", + "no_devices_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u4e0a\u306b\u30c7\u30d0\u30a4\u30b9\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth\u30a2\u30c9\u30ec\u30b9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/lv.json b/homeassistant/components/ld2410_ble/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/nl.json b/homeassistant/components/ld2410_ble/translations/nl.json new file mode 100644 index 00000000000..bf6cfe6f726 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "already_in_progress": "De configuratie is momenteel al bezig", + "no_devices_found": "Geen apparaten gevonden op het netwerk", + "not_supported": "Apparaat wordt niet ondersteund." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "unknown": "Onverwachte fout" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth-adres" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/no.json b/homeassistant/components/ld2410_ble/translations/no.json new file mode 100644 index 00000000000..4ffe814933a --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "no_unconfigured_devices": "Finner ingen enheter som ikke er konfigurert.", + "not_supported": "Enheten st\u00f8ttes ikke" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "unknown": "Uventet feil" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth-adresse" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/pl.json b/homeassistant/components/ld2410_ble/translations/pl.json new file mode 100644 index 00000000000..44100e63777 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "no_unconfigured_devices": "Nie znaleziono nieskonfigurowanych urz\u0105dze\u0144.", + "not_supported": "Urz\u0105dzenie nie jest obs\u0142ugiwane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Adres Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/pt-BR.json b/homeassistant/components/ld2410_ble/translations/pt-BR.json new file mode 100644 index 00000000000..f330267ab3c --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "A configura\u00e7\u00e3o j\u00e1 est\u00e1 em andamento", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "no_unconfigured_devices": "Nenhum dispositivo n\u00e3o configurado encontrado.", + "not_supported": "Dispositivo n\u00e3o suportado" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "unknown": "Erro inesperado" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Endere\u00e7o Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/ru.json b/homeassistant/components/ld2410_ble/translations/ru.json new file mode 100644 index 00000000000..dc0d2db5a6e --- /dev/null +++ b/homeassistant/components/ld2410_ble/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.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "no_unconfigured_devices": "\u041d\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e.", + "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." + }, + "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." + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441 Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/sk.json b/homeassistant/components/ld2410_ble/translations/sk.json new file mode 100644 index 00000000000..994e95cb8cd --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/sk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "already_in_progress": "Konfigur\u00e1cia u\u017e prebieha", + "no_devices_found": "V sieti sa nena\u0161li \u017eiadne zariadenia", + "no_unconfigured_devices": "Nena\u0161li sa \u017eiadne nenakonfigurovan\u00e9 zariadenia.", + "not_supported": "Zariadenie nie je podporovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Adresa Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/tr.json b/homeassistant/components/ld2410_ble/translations/tr.json new file mode 100644 index 00000000000..f9755124974 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "no_unconfigured_devices": "Yap\u0131land\u0131r\u0131lmam\u0131\u015f cihaz bulunamad\u0131.", + "not_supported": "Cihaz desteklenmiyor" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "Bluetooth adresi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/uk.json b/homeassistant/components/ld2410_ble/translations/uk.json new file mode 100644 index 00000000000..2c100e11086 --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e", + "already_in_progress": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454", + "no_devices_found": "\u0423 \u043c\u0435\u0440\u0435\u0436\u0456 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432", + "no_unconfigured_devices": "\u041d\u0435\u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e.", + "not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "{\u0456\u043c'\u044f}", + "step": { + "user": { + "data": { + "address": "\u0430\u0434\u0440\u0435\u0441\u0430 Bluetooth" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ld2410_ble/translations/zh-Hant.json b/homeassistant/components/ld2410_ble/translations/zh-Hant.json new file mode 100644 index 00000000000..ac129f22d4b --- /dev/null +++ b/homeassistant/components/ld2410_ble/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "no_unconfigured_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u8a2d\u5b9a\u88dd\u7f6e\u3002", + "not_supported": "\u88dd\u7f6e\u4e0d\u652f\u63f4" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{name}", + "step": { + "user": { + "data": { + "address": "\u85cd\u7259\u4f4d\u5740" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/manifest.json b/homeassistant/components/led_ble/manifest.json index 9282e0bd8a2..dc40e3855aa 100644 --- a/homeassistant/components/led_ble/manifest.json +++ b/homeassistant/components/led_ble/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/led_ble/", "requirements": ["bluetooth-data-tools==0.3.1", "led-ble==1.0.0"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "bluetooth": [ { "local_name": "LEDnet*" }, diff --git a/homeassistant/components/led_ble/translations/lv.json b/homeassistant/components/led_ble/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/led_ble/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/led_ble/translations/uk.json b/homeassistant/components/led_ble/translations/uk.json new file mode 100644 index 00000000000..e58b49d4c9e --- /dev/null +++ b/homeassistant/components/led_ble/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/__init__.py b/homeassistant/components/lg_soundbar/__init__.py index 75b2109b22a..21d7fa4e773 100644 --- a/homeassistant/components/lg_soundbar/__init__.py +++ b/homeassistant/components/lg_soundbar/__init__.py @@ -26,7 +26,7 @@ async def async_setup_entry( except ConnectionError as err: raise ConfigEntryNotReady from err - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/lg_soundbar/translations/lv.json b/homeassistant/components/lg_soundbar/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/lg_soundbar/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lg_soundbar/translations/tr.json b/homeassistant/components/lg_soundbar/translations/tr.json index 181980a8917..ccade6d4e5b 100644 --- a/homeassistant/components/lg_soundbar/translations/tr.json +++ b/homeassistant/components/lg_soundbar/translations/tr.json @@ -4,7 +4,8 @@ "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "no_data": "Cihaz, bir giri\u015f i\u00e7in gereken herhangi bir veriyi d\u00f6nd\u00fcrmedi." }, "step": { "user": { diff --git a/homeassistant/components/lidarr/coordinator.py b/homeassistant/components/lidarr/coordinator.py index e4554685c8c..9cc606a12b3 100644 --- a/homeassistant/components/lidarr/coordinator.py +++ b/homeassistant/components/lidarr/coordinator.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from datetime import timedelta -from typing import Generic, TypeVar, Union, cast +from typing import Generic, TypeVar, cast from aiopyarr import LidarrAlbum, LidarrQueue, LidarrRootFolder, exceptions from aiopyarr.lidarr_client import LidarrClient @@ -16,7 +16,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DEFAULT_MAX_RECORDS, DOMAIN, LOGGER -T = TypeVar("T", bound=Union[list[LidarrRootFolder], LidarrQueue, str, LidarrAlbum]) +T = TypeVar("T", bound=list[LidarrRootFolder] | LidarrQueue | str | LidarrAlbum) class LidarrDataUpdateCoordinator(DataUpdateCoordinator[T], Generic[T], ABC): diff --git a/homeassistant/components/lidarr/translations/hu.json b/homeassistant/components/lidarr/translations/hu.json index 7981b025ce0..bac9d71334d 100644 --- a/homeassistant/components/lidarr/translations/hu.json +++ b/homeassistant/components/lidarr/translations/hu.json @@ -8,7 +8,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", - "wrong_app": "Helytelen alkalmaz\u00e1s el\u00e9rve. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra", + "wrong_app": "Helytelen alkalmaz\u00e1s el\u00e9rve. K\u00e9rem, pr\u00f3b\u00e1lja \u00fajra", "zeroconf_failed": "Az API-kulcs nem tal\u00e1lhat\u00f3. K\u00e9rem, adja meg" }, "step": { diff --git a/homeassistant/components/life360/__init__.py b/homeassistant/components/life360/__init__.py index d05a9ec400b..271f934e1c7 100644 --- a/homeassistant/components/life360/__init__.py +++ b/homeassistant/components/life360/__init__.py @@ -168,7 +168,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].coordinators[entry.entry_id] = coordinator # Set up components for our platforms. - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/life360/translations/el.json b/homeassistant/components/life360/translations/el.json index b9105c05200..66871ae536a 100644 --- a/homeassistant/components/life360/translations/el.json +++ b/homeassistant/components/life360/translations/el.json @@ -23,7 +23,7 @@ "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": "\u03a0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Life360" + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd Life360" } } }, diff --git a/homeassistant/components/life360/translations/lt.json b/homeassistant/components/life360/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/life360/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/nl.json b/homeassistant/components/life360/translations/nl.json index f818a43a73a..37925bae782 100644 --- a/homeassistant/components/life360/translations/nl.json +++ b/homeassistant/components/life360/translations/nl.json @@ -30,6 +30,13 @@ "options": { "step": { "init": { + "data": { + "driving": "Toon rijden als staat", + "driving_speed": "Rijsnelheid", + "limit_gps_acc": "Beperk de GPS-nauwkeurigheid", + "max_gps_accuracy": "Maximale GPS-nauwkeurigheid (meters)", + "set_drive_speed": "Rijsnelheidsdrempel instellen" + }, "title": "Accountinstellingen" } } diff --git a/homeassistant/components/life360/translations/uk.json b/homeassistant/components/life360/translations/uk.json index 6f74c07cc19..71ec1b1f32f 100644 --- a/homeassistant/components/life360/translations/uk.json +++ b/homeassistant/components/life360/translations/uk.json @@ -9,6 +9,11 @@ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/lifx/coordinator.py b/homeassistant/components/lifx/coordinator.py index 952a4574d2d..038f93c1e88 100644 --- a/homeassistant/components/lifx/coordinator.py +++ b/homeassistant/components/lifx/coordinator.py @@ -358,7 +358,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator[None]): return self.active_effect.value -class LIFXSensorUpdateCoordinator(DataUpdateCoordinator): +class LIFXSensorUpdateCoordinator(DataUpdateCoordinator[None]): """DataUpdateCoordinator to gather data for a specific lifx device.""" def __init__( diff --git a/homeassistant/components/lifx/translations/el.json b/homeassistant/components/lifx/translations/el.json index 51556cc2af2..83611e1ddb9 100644 --- a/homeassistant/components/lifx/translations/el.json +++ b/homeassistant/components/lifx/translations/el.json @@ -8,10 +8,10 @@ "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2" }, - "flow_title": "{label} ({host}) {serial}", + "flow_title": "{label} ({group})", "step": { "discovery_confirm": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {label} ({host}) {serial};" + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {label} ({group});" }, "pick_device": { "data": { diff --git a/homeassistant/components/lifx/translations/lv.json b/homeassistant/components/lifx/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/lifx/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/tr.json b/homeassistant/components/lifx/translations/tr.json index a11798e6513..02d3388ac6e 100644 --- a/homeassistant/components/lifx/translations/tr.json +++ b/homeassistant/components/lifx/translations/tr.json @@ -8,10 +8,10 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, - "flow_title": "{label} ({host}) {serial}", + "flow_title": "{label} ( {group} )", "step": { "discovery_confirm": { - "description": "{label} ( {host} ) {serial} kurmak istiyor musunuz?" + "description": "{label} ( {group} ) kurmak istiyor musunuz?" }, "pick_device": { "data": { diff --git a/homeassistant/components/lifx/translations/uk.json b/homeassistant/components/lifx/translations/uk.json index 1efd10692f9..787e5440af9 100644 --- a/homeassistant/components/lifx/translations/uk.json +++ b/homeassistant/components/lifx/translations/uk.json @@ -2,6 +2,13 @@ "config": { "abort": { "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456." + }, + "step": { + "pick_device": { + "data": { + "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index acc6252d3b3..61582976bc0 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -434,9 +434,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: ): profiles.apply_default(light.entity_id, light.is_on, params) - legacy_supported_color_modes = ( - light._light_internal_supported_color_modes # pylint: disable=protected-access - ) + # pylint: disable-next=protected-access + legacy_supported_color_modes = light._light_internal_supported_color_modes supported_color_modes = light.supported_color_modes # If a color temperature is specified, emulate it if not supported by the light @@ -504,8 +503,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: params[ATTR_RGBW_COLOR] = color_util.color_rgb_to_rgbw(*rgb_color) elif ColorMode.RGBWW in supported_color_modes: # https://github.com/python/mypy/issues/13673 - params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( # type: ignore[call-arg] - *rgb_color, light.min_color_temp_kelvin, light.max_color_temp_kelvin + params[ATTR_RGBWW_COLOR] = color_util.color_rgb_to_rgbww( + *rgb_color, # type: ignore[call-arg] + light.min_color_temp_kelvin, + light.max_color_temp_kelvin, ) elif ColorMode.HS in supported_color_modes: params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) diff --git a/homeassistant/components/light/intent.py b/homeassistant/components/light/intent.py index e85602f763a..7b75821ab43 100644 --- a/homeassistant/components/light/intent.py +++ b/homeassistant/components/light/intent.py @@ -1,12 +1,15 @@ """Intents for the light integration.""" from __future__ import annotations +import asyncio +import logging +from typing import Any + import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON -from homeassistant.core import HomeAssistant, State -from homeassistant.helpers import intent -import homeassistant.helpers.config_validation as cv +from homeassistant.core import HomeAssistant +from homeassistant.helpers import area_registry, config_validation as cv, intent import homeassistant.util.color as color_util from . import ( @@ -18,6 +21,8 @@ from . import ( color_supported, ) +_LOGGER = logging.getLogger(__name__) + INTENT_SET = "HassLightSet" @@ -26,30 +31,14 @@ async def async_setup_intents(hass: HomeAssistant) -> None: intent.async_register(hass, SetIntentHandler()) -def _test_supports_color(state: State) -> None: - """Test if state supports colors.""" - supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) - if not color_supported(supported_color_modes): - raise intent.IntentHandleError( - f"Entity {state.name} does not support changing colors" - ) - - -def _test_supports_brightness(state: State) -> None: - """Test if state supports brightness.""" - supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) - if not brightness_supported(supported_color_modes): - raise intent.IntentHandleError( - f"Entity {state.name} does not support changing brightness" - ) - - class SetIntentHandler(intent.IntentHandler): """Handle set color intents.""" intent_type = INTENT_SET slot_schema = { - vol.Required("name"): cv.string, + vol.Any("name", "area"): cv.string, + vol.Optional("domain"): vol.All(cv.ensure_list, [cv.string]), + vol.Optional("device_class"): vol.All(cv.ensure_list, [cv.string]), vol.Optional("color"): color_util.color_name_to_rgb, vol.Optional("brightness"): vol.All(vol.Coerce(int), vol.Range(0, 100)), } @@ -57,44 +46,102 @@ class SetIntentHandler(intent.IntentHandler): async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: """Handle the hass intent.""" hass = intent_obj.hass + service_data: dict[str, Any] = {} slots = self.async_validate_slots(intent_obj.slots) - state = intent.async_match_state( - hass, slots["name"]["value"], hass.states.async_all(DOMAIN) + + name: str | None = slots.get("name", {}).get("value") + if name == "all": + # Don't match on name if targeting all entities + name = None + + # Look up area first to fail early + area_name = slots.get("area", {}).get("value") + area: area_registry.AreaEntry | None = None + if area_name is not None: + areas = area_registry.async_get(hass) + area = areas.async_get_area(area_name) or areas.async_get_area_by_name( + area_name + ) + if area is None: + raise intent.IntentHandleError(f"No area named {area_name}") + + # Optional domain/device class filters. + # Convert to sets for speed. + domains: set[str] | None = None + device_classes: set[str] | None = None + + if "domain" in slots: + domains = set(slots["domain"]["value"]) + + if "device_class" in slots: + device_classes = set(slots["device_class"]["value"]) + + states = list( + intent.async_match_states( + hass, + name=name, + area=area, + domains=domains, + device_classes=device_classes, + ) ) - service_data = {ATTR_ENTITY_ID: state.entity_id} - speech_parts = [] + if not states: + raise intent.IntentHandleError("No entities matched") if "color" in slots: - _test_supports_color(state) service_data[ATTR_RGB_COLOR] = slots["color"]["value"] - # Use original passed in value of the color because we don't have - # human readable names for that internally. - speech_parts.append(f"the color {intent_obj.slots['color']['value']}") if "brightness" in slots: - _test_supports_brightness(state) service_data[ATTR_BRIGHTNESS_PCT] = slots["brightness"]["value"] - speech_parts.append(f"{slots['brightness']['value']}% brightness") - - await hass.services.async_call( - DOMAIN, SERVICE_TURN_ON, service_data, context=intent_obj.context - ) response = intent_obj.create_response() + needs_brightness = ATTR_BRIGHTNESS_PCT in service_data + needs_color = ATTR_RGB_COLOR in service_data - if not speech_parts: # No attributes changed - speech = f"Turned on {state.name}" - else: - parts = [f"Changed {state.name} to"] - for index, part in enumerate(speech_parts): - if index == 0: - parts.append(f" {part}") - elif index != len(speech_parts) - 1: - parts.append(f", {part}") - else: - parts.append(f" and {part}") - speech = "".join(parts) + success_results: list[intent.IntentResponseTarget] = [] + failed_results: list[intent.IntentResponseTarget] = [] + service_coros = [] + + if area is not None: + success_results.append( + intent.IntentResponseTarget( + type=intent.IntentResponseTargetType.AREA, + name=area.name, + id=area.id, + ) + ) + + for state in states: + target = intent.IntentResponseTarget( + type=intent.IntentResponseTargetType.ENTITY, + name=state.name, + id=state.entity_id, + ) + + # Test brightness/color + supported_color_modes = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) + if (needs_color and not color_supported(supported_color_modes)) or ( + needs_brightness and not brightness_supported(supported_color_modes) + ): + failed_results.append(target) + continue + + service_coros.append( + hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {**service_data, ATTR_ENTITY_ID: state.entity_id}, + context=intent_obj.context, + ) + ) + success_results.append(target) + + # Handle service calls in parallel. + await asyncio.gather(*service_coros) + + response.async_set_results( + success_results=success_results, failed_results=failed_results + ) - response.async_set_speech(speech) return response diff --git a/homeassistant/components/light/translations/lt.json b/homeassistant/components/light/translations/lt.json new file mode 100644 index 00000000000..326b3cf4e20 --- /dev/null +++ b/homeassistant/components/light/translations/lt.json @@ -0,0 +1,27 @@ +{ + "device_automation": { + "action_type": { + "brightness_decrease": "Suma\u017einti {entity_name} ry\u0161kum\u0105", + "brightness_increase": "Padidinti {entity_name} ry\u0161kum\u0105", + "flash": "Blyks\u0117ti {entity_name}", + "toggle": "Perjungti {entity_name}", + "turn_off": "I\u0161jungti", + "turn_on": "\u012ejungti {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} yra i\u0161jungta", + "is_on": "{entity_name} \u012fjungta" + }, + "trigger_type": { + "turned_off": "{entity_name} i\u0161jungta", + "turned_on": "{entity_name} \u012fjungta" + } + }, + "state": { + "_": { + "off": "I\u0161jungta", + "on": "\u012ejungta" + } + }, + "title": "Lempa" +} \ No newline at end of file diff --git a/homeassistant/components/lirc/__init__.py b/homeassistant/components/lirc/__init__.py index cca7593768b..227d1e7d5a9 100644 --- a/homeassistant/components/lirc/__init__.py +++ b/homeassistant/components/lirc/__init__.py @@ -60,7 +60,7 @@ class LircInterface(threading.Thread): def run(self): """Run the loop of the LIRC interface thread.""" _LOGGER.debug("LIRC interface thread started") - while not self.stopped.isSet(): + while not self.stopped.is_set(): try: code = lirc.nextcode() # list; empty if no buttons pressed except lirc.NextCodeError: diff --git a/homeassistant/components/litejet/__init__.py b/homeassistant/components/litejet/__init__.py index 5131ee52e67..040b8688a42 100644 --- a/homeassistant/components/litejet/__init__.py +++ b/homeassistant/components/litejet/__init__.py @@ -2,11 +2,10 @@ import logging import pylitejet -from serial import SerialException import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_PORT +from homeassistant.const import CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv @@ -52,10 +51,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: port = entry.data[CONF_PORT] try: - system = pylitejet.LiteJet(port) - except SerialException as ex: - _LOGGER.error("Error connecting to the LiteJet MCP at %s", port, exc_info=ex) - raise ConfigEntryNotReady from ex + system = await pylitejet.open(port) + except pylitejet.LiteJetError as exc: + raise ConfigEntryNotReady from exc + + def handle_connected_changed(connected: bool, reason: str) -> None: + if connected: + _LOGGER.info("Connected") + else: + _LOGGER.warning("Disconnected %s", reason) + + system.on_connected_changed(handle_connected_changed) + + async def handle_stop(event) -> None: + await system.close() + + entry.async_on_unload( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop) + ) hass.data[DOMAIN] = system @@ -69,7 +82,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].close() + await hass.data[DOMAIN].close() hass.data.pop(DOMAIN) return unload_ok diff --git a/homeassistant/components/litejet/config_flow.py b/homeassistant/components/litejet/config_flow.py index e14eda1b745..25d454071cc 100644 --- a/homeassistant/components/litejet/config_flow.py +++ b/homeassistant/components/litejet/config_flow.py @@ -59,15 +59,12 @@ class LiteJetConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: port = user_input[CONF_PORT] - await self.async_set_unique_id(port) - self._abort_if_unique_id_configured() - try: - system = pylitejet.LiteJet(port) - system.close() + system = await pylitejet.open(port) except SerialException: errors[CONF_PORT] = "open_failed" else: + await system.close() return self.async_create_entry( title=port, data={CONF_PORT: port}, diff --git a/homeassistant/components/litejet/diagnostics.py b/homeassistant/components/litejet/diagnostics.py new file mode 100644 index 00000000000..b996dcc0413 --- /dev/null +++ b/homeassistant/components/litejet/diagnostics.py @@ -0,0 +1,22 @@ +"""Support for LiteJet diagnostics.""" +from typing import Any + +from pylitejet import LiteJet + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for LiteJet config entry.""" + system: LiteJet = hass.data[DOMAIN] + return { + "loads": list(system.loads()), + "button_switches": list(system.button_switches()), + "scenes": list(system.scenes()), + "connected": system.connected, + } diff --git a/homeassistant/components/litejet/light.py b/homeassistant/components/litejet/light.py index a41a34016d9..09855a4d0d5 100644 --- a/homeassistant/components/litejet/light.py +++ b/homeassistant/components/litejet/light.py @@ -1,10 +1,9 @@ """Support for LiteJet lights.""" from __future__ import annotations -import logging from typing import Any -from pylitejet import LiteJet +from pylitejet import LiteJet, LiteJetError from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -15,12 +14,11 @@ from homeassistant.components.light import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import CONF_DEFAULT_TRANSITION, DOMAIN -_LOGGER = logging.getLogger(__name__) - ATTR_NUMBER = "number" @@ -33,14 +31,12 @@ async def async_setup_entry( system: LiteJet = hass.data[DOMAIN] - def get_entities(system: LiteJet) -> list[LiteJetLight]: - entities = [] - for index in system.loads(): - name = system.get_load_name(index) - entities.append(LiteJetLight(config_entry, system, index, name)) - return entities + entities = [] + for index in system.loads(): + name = await system.get_load_name(index) + entities.append(LiteJetLight(config_entry, system, index, name)) - async_add_entities(await hass.async_add_executor_job(get_entities, system), True) + async_add_entities(entities, True) class LiteJetLight(LightEntity): @@ -68,24 +64,32 @@ class LiteJetLight(LightEntity): """Run when this Entity has been added to HA.""" self._lj.on_load_activated(self._index, self._on_load_changed) self._lj.on_load_deactivated(self._index, self._on_load_changed) + self._lj.on_connected_changed(self._on_connected_changed) async def async_will_remove_from_hass(self) -> None: """Entity being removed from hass.""" self._lj.unsubscribe(self._on_load_changed) + self._lj.unsubscribe(self._on_connected_changed) - def _on_load_changed(self) -> None: + def _on_load_changed(self, level) -> None: """Handle state changes.""" - _LOGGER.debug("Updating due to notification for %s", self.name) self.schedule_update_ha_state(True) - def turn_on(self, **kwargs: Any) -> None: + def _on_connected_changed(self, connected: bool, reason: str) -> None: + """Handle connected changes.""" + self.schedule_update_ha_state(True) + + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" # If neither attribute is specified then the simple activate load # LiteJet API will use the per-light default brightness and # transition values programmed in the LiteJet system. if ATTR_BRIGHTNESS not in kwargs and ATTR_TRANSITION not in kwargs: - self._lj.activate_load(self._index) + try: + await self._lj.activate_load(self._index) + except LiteJetError as exc: + raise HomeAssistantError() from exc return # If either attribute is specified then Home Assistant must @@ -94,20 +98,36 @@ class LiteJetLight(LightEntity): transition = kwargs.get(ATTR_TRANSITION, default_transition) brightness = int(kwargs.get(ATTR_BRIGHTNESS, 255) / 255 * 99) - self._lj.activate_load_at(self._index, brightness, int(transition)) + try: + await self._lj.activate_load_at(self._index, brightness, int(transition)) + except LiteJetError as exc: + raise HomeAssistantError() from exc - def turn_off(self, **kwargs: Any) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" if ATTR_TRANSITION in kwargs: - self._lj.activate_load_at(self._index, 0, kwargs[ATTR_TRANSITION]) + try: + await self._lj.activate_load_at(self._index, 0, kwargs[ATTR_TRANSITION]) + except LiteJetError as exc: + raise HomeAssistantError() from exc return # If transition attribute is not specified then the simple # deactivate load LiteJet API will use the per-light default # transition value programmed in the LiteJet system. - self._lj.deactivate_load(self._index) + try: + await self._lj.deactivate_load(self._index) + except LiteJetError as exc: + raise HomeAssistantError() from exc - def update(self) -> None: + async def async_update(self) -> None: """Retrieve the light's brightness from the LiteJet system.""" - self._attr_brightness = int(self._lj.get_load_level(self._index) / 99 * 255) + self._attr_available = self._lj.connected + + if not self.available: + return + + self._attr_brightness = int( + await self._lj.get_load_level(self._index) / 99 * 255 + ) self._attr_is_on = self.brightness != 0 diff --git a/homeassistant/components/litejet/manifest.json b/homeassistant/components/litejet/manifest.json index c6e958d3a10..142e790a12a 100644 --- a/homeassistant/components/litejet/manifest.json +++ b/homeassistant/components/litejet/manifest.json @@ -2,7 +2,7 @@ "domain": "litejet", "name": "LiteJet", "documentation": "https://www.home-assistant.io/integrations/litejet", - "requirements": ["pylitejet==0.3.0"], + "requirements": ["pylitejet==0.5.0"], "codeowners": ["@joncar"], "config_flow": true, "iot_class": "local_push", diff --git a/homeassistant/components/litejet/scene.py b/homeassistant/components/litejet/scene.py index 0a091d7e729..01dfc0a3ccd 100644 --- a/homeassistant/components/litejet/scene.py +++ b/homeassistant/components/litejet/scene.py @@ -1,13 +1,19 @@ """Support for LiteJet scenes.""" +import logging from typing import Any +from pylitejet import LiteJet, LiteJetError + from homeassistant.components.scene import Scene from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) + ATTR_NUMBER = "number" @@ -18,46 +24,49 @@ async def async_setup_entry( ) -> None: """Set up entry.""" - system = hass.data[DOMAIN] + system: LiteJet = hass.data[DOMAIN] - def get_entities(system): - entities = [] - for i in system.scenes(): - name = system.get_scene_name(i) - entities.append(LiteJetScene(config_entry.entry_id, system, i, name)) - return entities + entities = [] + for i in system.scenes(): + name = await system.get_scene_name(i) + entities.append(LiteJetScene(config_entry.entry_id, system, i, name)) - async_add_entities(await hass.async_add_executor_job(get_entities, system), True) + async_add_entities(entities, True) class LiteJetScene(Scene): """Representation of a single LiteJet scene.""" - def __init__(self, entry_id, lj, i, name): # pylint: disable=invalid-name + def __init__(self, entry_id, lj: LiteJet, i, name): # pylint: disable=invalid-name """Initialize the scene.""" - self._entry_id = entry_id self._lj = lj self._index = i - self._name = name + self._attr_unique_id = f"{entry_id}_{i}" + self._attr_name = name - @property - def name(self): - """Return the name of the scene.""" - return self._name + async def async_added_to_hass(self) -> None: + """Run when this Entity has been added to HA.""" + self._lj.on_connected_changed(self._on_connected_changed) - @property - def unique_id(self): - """Return a unique identifier for this scene.""" - return f"{self._entry_id}_{self._index}" + async def async_will_remove_from_hass(self) -> None: + """Entity being removed from hass.""" + self._lj.unsubscribe(self._on_connected_changed) + + def _on_connected_changed(self, connected: bool, reason: str) -> None: + self._attr_available = connected + self.schedule_update_ha_state() @property def extra_state_attributes(self): """Return the device-specific state attributes.""" return {ATTR_NUMBER: self._index} - def activate(self, **kwargs: Any) -> None: + async def async_activate(self, **kwargs: Any) -> None: """Activate the scene.""" - self._lj.activate_scene(self._index) + try: + await self._lj.activate_scene(self._index) + except LiteJetError as exc: + raise HomeAssistantError() from exc @property def entity_registry_enabled_default(self) -> bool: diff --git a/homeassistant/components/litejet/switch.py b/homeassistant/components/litejet/switch.py index 375e3dd9f46..1f010691f02 100644 --- a/homeassistant/components/litejet/switch.py +++ b/homeassistant/components/litejet/switch.py @@ -1,18 +1,18 @@ """Support for LiteJet switch.""" -import logging from typing import Any +from pylitejet import LiteJet, LiteJetError + from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN ATTR_NUMBER = "number" -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, @@ -21,16 +21,14 @@ async def async_setup_entry( ) -> None: """Set up entry.""" - system = hass.data[DOMAIN] + system: LiteJet = hass.data[DOMAIN] - def get_entities(system): - entities = [] - for i in system.button_switches(): - name = system.get_switch_name(i) - entities.append(LiteJetSwitch(config_entry.entry_id, system, i, name)) - return entities + entities = [] + for i in system.button_switches(): + name = await system.get_switch_name(i) + entities.append(LiteJetSwitch(config_entry.entry_id, system, i, name)) - async_add_entities(await hass.async_add_executor_job(get_entities, system), True) + async_add_entities(entities, True) class LiteJetSwitch(SwitchEntity): @@ -43,56 +41,56 @@ class LiteJetSwitch(SwitchEntity): self._entry_id = entry_id self._lj = lj self._index = i - self._state = False - self._name = name + self._attr_is_on = False + self._attr_name = name async def async_added_to_hass(self) -> None: """Run when this Entity has been added to HA.""" self._lj.on_switch_pressed(self._index, self._on_switch_pressed) self._lj.on_switch_released(self._index, self._on_switch_released) + self._lj.on_connected_changed(self._on_connected_changed) async def async_will_remove_from_hass(self) -> None: """Entity being removed from hass.""" self._lj.unsubscribe(self._on_switch_pressed) self._lj.unsubscribe(self._on_switch_released) + self._lj.unsubscribe(self._on_connected_changed) def _on_switch_pressed(self): - _LOGGER.debug("Updating pressed for %s", self._name) - self._state = True + self._attr_is_on = True self.schedule_update_ha_state() def _on_switch_released(self): - _LOGGER.debug("Updating released for %s", self._name) - self._state = False + self._attr_is_on = False self.schedule_update_ha_state() - @property - def name(self): - """Return the name of the switch.""" - return self._name + def _on_connected_changed(self, connected: bool, reason: str) -> None: + self._attr_available = connected + self.schedule_update_ha_state() @property def unique_id(self): """Return a unique identifier for this switch.""" return f"{self._entry_id}_{self._index}" - @property - def is_on(self): - """Return if the switch is pressed.""" - return self._state - @property def extra_state_attributes(self): """Return the device-specific state attributes.""" return {ATTR_NUMBER: self._index} - def turn_on(self, **kwargs: Any) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Press the switch.""" - self._lj.press_switch(self._index) + try: + await self._lj.press_switch(self._index) + except LiteJetError as exc: + raise HomeAssistantError() from exc - def turn_off(self, **kwargs: Any) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Release the switch.""" - self._lj.release_switch(self._index) + try: + await self._lj.release_switch(self._index) + except LiteJetError as exc: + raise HomeAssistantError() from exc @property def entity_registry_enabled_default(self) -> bool: diff --git a/homeassistant/components/litejet/translations/bg.json b/homeassistant/components/litejet/translations/bg.json index c4ccfa52041..7ce743464a8 100644 --- a/homeassistant/components/litejet/translations/bg.json +++ b/homeassistant/components/litejet/translations/bg.json @@ -11,5 +11,14 @@ "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 LiteJet" } } + }, + "options": { + "step": { + "init": { + "data": { + "default_transition": "\u041f\u0440\u0435\u0445\u043e\u0434 \u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435 (\u0441\u0435\u043a\u0443\u043d\u0434\u0438)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/litejet/trigger.py b/homeassistant/components/litejet/trigger.py index a0cdeaf9a01..502d84693c6 100644 --- a/homeassistant/components/litejet/trigger.py +++ b/homeassistant/components/litejet/trigger.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable +from pylitejet import LiteJet import voluptuous as vol from homeassistant.const import CONF_PLATFORM @@ -104,7 +105,7 @@ async def async_attach_trigger( ): hass.add_job(call_action) - system = hass.data[DOMAIN] + system: LiteJet = hass.data[DOMAIN] system.on_switch_pressed(number, pressed) system.on_switch_released(number, released) diff --git a/homeassistant/components/litterrobot/button.py b/homeassistant/components/litterrobot/button.py index 81d9c65927e..e4f806f74fa 100644 --- a/homeassistant/components/litterrobot/button.py +++ b/homeassistant/components/litterrobot/button.py @@ -8,18 +8,14 @@ from typing import Any, Generic from pylitterbot import FeederRobot, LitterRobot3 -from homeassistant.components.button import ( - DOMAIN as PLATFORM, - ButtonEntity, - ButtonEntityDescription, -) +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotEntity, _RobotT, async_update_unique_id +from .entity import LitterRobotEntity, _RobotT from .hub import LitterRobotHub @@ -47,7 +43,6 @@ async def async_setup_entry( ), ) ) - async_update_unique_id(hass, PLATFORM, entities) async_add_entities(entities) @@ -65,7 +60,7 @@ class RobotButtonEntityDescription(ButtonEntityDescription, RequiredKeysMixin[_R LITTER_ROBOT_BUTTON = RobotButtonEntityDescription[LitterRobot3]( key="reset_waste_drawer", - name="Reset Waste Drawer", + name="Reset waste drawer", icon="mdi:delete-variant", entity_category=EntityCategory.CONFIG, press_fn=lambda robot: robot.reset_waste_drawer(), diff --git a/homeassistant/components/litterrobot/entity.py b/homeassistant/components/litterrobot/entity.py index 3ad21b1aeb7..063799868b6 100644 --- a/homeassistant/components/litterrobot/entity.py +++ b/homeassistant/components/litterrobot/entity.py @@ -1,15 +1,12 @@ """Litter-Robot entities for common data and methods.""" from __future__ import annotations -from collections.abc import Iterable from typing import Generic, TypeVar from pylitterbot import Robot from pylitterbot.robot import EVENT_UPDATE -from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo, EntityDescription -import homeassistant.helpers.entity_registry as er from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -37,9 +34,6 @@ class LitterRobotEntity( self.hub = hub self.entity_description = description self._attr_unique_id = f"{self.robot.serial}-{description.key}" - # The following can be removed in 2022.12 after adjusting names in entities appropriately - if description.name is not None: - self._attr_name = description.name.capitalize() @property def device_info(self) -> DeviceInfo: @@ -57,18 +51,3 @@ class LitterRobotEntity( """Set up a listener for the entity.""" await super().async_added_to_hass() self.async_on_remove(self.robot.on(EVENT_UPDATE, self.async_write_ha_state)) - - -def async_update_unique_id( - hass: HomeAssistant, domain: str, entities: Iterable[LitterRobotEntity[_RobotT]] -) -> None: - """Update unique ID to be based on entity description key instead of name. - - Introduced with release 2022.9. - """ - ent_reg = er.async_get(hass) - for entity in entities: - old_unique_id = f"{entity.robot.serial}-{entity.entity_description.name}" - if entity_id := ent_reg.async_get_entity_id(domain, DOMAIN, old_unique_id): - new_unique_id = f"{entity.robot.serial}-{entity.entity_description.key}" - ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) diff --git a/homeassistant/components/litterrobot/select.py b/homeassistant/components/litterrobot/select.py index 0edfd5b0646..a8f1a309f15 100644 --- a/homeassistant/components/litterrobot/select.py +++ b/homeassistant/components/litterrobot/select.py @@ -8,11 +8,7 @@ from typing import Any, Generic, TypeVar from pylitterbot import FeederRobot, LitterRobot -from homeassistant.components.select import ( - DOMAIN as PLATFORM, - SelectEntity, - SelectEntityDescription, -) +from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTime from homeassistant.core import HomeAssistant @@ -20,7 +16,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotEntity, _RobotT, async_update_unique_id +from .entity import LitterRobotEntity, _RobotT from .hub import LitterRobotHub _CastTypeT = TypeVar("_CastTypeT", int, float) @@ -46,7 +42,7 @@ class RobotSelectEntityDescription( LITTER_ROBOT_SELECT = RobotSelectEntityDescription[LitterRobot, int]( key="cycle_delay", - name="Clean Cycle Wait Time Minutes", + name="Clean cycle wait time minutes", icon="mdi:timer-outline", unit_of_measurement=UnitOfTime.MINUTES, current_fn=lambda robot: robot.clean_cycle_wait_time_minutes, @@ -83,7 +79,6 @@ async def async_setup_entry( ), ) ) - async_update_unique_id(hass, PLATFORM, entities) async_add_entities(entities) diff --git a/homeassistant/components/litterrobot/sensor.py b/homeassistant/components/litterrobot/sensor.py index 3b994f4ae9d..1fc99b9495a 100644 --- a/homeassistant/components/litterrobot/sensor.py +++ b/homeassistant/components/litterrobot/sensor.py @@ -4,12 +4,11 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime -from typing import Any, Generic, Union, cast +from typing import Any, Generic, cast from pylitterbot import FeederRobot, LitterRobot, LitterRobot4, Robot from homeassistant.components.sensor import ( - DOMAIN as PLATFORM, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -22,7 +21,7 @@ from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotEntity, _RobotT, async_update_unique_id +from .entity import LitterRobotEntity, _RobotT from .hub import LitterRobotHub @@ -56,7 +55,7 @@ class LitterRobotSensorEntity(LitterRobotEntity[_RobotT], SensorEntity): if self.entity_description.should_report(self.robot): if isinstance(val := getattr(self.robot, self.entity_description.key), str): return val.lower() - return cast(Union[float, datetime, None], val) + return cast(float | datetime | None, val) return None @property @@ -71,32 +70,32 @@ ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = { LitterRobot: [ RobotSensorEntityDescription[LitterRobot]( key="waste_drawer_level", - name="Waste Drawer", + name="Waste drawer", native_unit_of_measurement=PERCENTAGE, icon_fn=lambda state: icon_for_gauge_level(state, 10), state_class=SensorStateClass.MEASUREMENT, ), RobotSensorEntityDescription[LitterRobot]( key="sleep_mode_start_time", - name="Sleep Mode Start Time", + name="Sleep mode start time", device_class=SensorDeviceClass.TIMESTAMP, should_report=lambda robot: robot.sleep_mode_enabled, ), RobotSensorEntityDescription[LitterRobot]( key="sleep_mode_end_time", - name="Sleep Mode End Time", + name="Sleep mode end time", device_class=SensorDeviceClass.TIMESTAMP, should_report=lambda robot: robot.sleep_mode_enabled, ), RobotSensorEntityDescription[LitterRobot]( key="last_seen", - name="Last Seen", + name="Last seen", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, ), RobotSensorEntityDescription[LitterRobot]( key="status_code", - name="Status Code", + name="Status code", translation_key="status_code", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.ENUM, @@ -142,7 +141,7 @@ ROBOT_SENSOR_MAP: dict[type[Robot], list[RobotSensorEntityDescription]] = { name="Pet weight", native_unit_of_measurement=UnitOfMass.POUNDS, device_class=SensorDeviceClass.WEIGHT, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), ], FeederRobot: [ @@ -171,5 +170,4 @@ async def async_setup_entry( if isinstance(robot, robot_type) for description in entity_descriptions ] - async_update_unique_id(hass, PLATFORM, entities) async_add_entities(entities) diff --git a/homeassistant/components/litterrobot/switch.py b/homeassistant/components/litterrobot/switch.py index af690f30501..6199a4d89b9 100644 --- a/homeassistant/components/litterrobot/switch.py +++ b/homeassistant/components/litterrobot/switch.py @@ -3,22 +3,18 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass -from typing import Any, Generic, Union +from typing import Any, Generic from pylitterbot import FeederRobot, LitterRobot -from homeassistant.components.switch import ( - DOMAIN as PLATFORM, - SwitchEntity, - SwitchEntityDescription, -) +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .entity import LitterRobotEntity, _RobotT, async_update_unique_id +from .entity import LitterRobotEntity, _RobotT from .hub import LitterRobotHub @@ -38,15 +34,15 @@ class RobotSwitchEntityDescription(SwitchEntityDescription, RequiredKeysMixin[_R ROBOT_SWITCHES = [ - RobotSwitchEntityDescription[Union[LitterRobot, FeederRobot]]( + RobotSwitchEntityDescription[LitterRobot | FeederRobot]( key="night_light_mode_enabled", - name="Night Light Mode", + name="Night light mode", icons=("mdi:lightbulb-on", "mdi:lightbulb-off"), set_fn=lambda robot, value: robot.set_night_light(value), ), - RobotSwitchEntityDescription[Union[LitterRobot, FeederRobot]]( + RobotSwitchEntityDescription[LitterRobot | FeederRobot]( key="panel_lock_enabled", - name="Panel Lockout", + name="Panel lockout", icons=("mdi:lock", "mdi:lock-open"), set_fn=lambda robot, value: robot.set_panel_lockout(value), ), @@ -91,5 +87,4 @@ async def async_setup_entry( for robot in hub.account.robots if isinstance(robot, (LitterRobot, FeederRobot)) ] - async_update_unique_id(hass, PLATFORM, entities) async_add_entities(entities) diff --git a/homeassistant/components/litterrobot/translations/nl.json b/homeassistant/components/litterrobot/translations/nl.json index 06991532cd9..0ed694bb01a 100644 --- a/homeassistant/components/litterrobot/translations/nl.json +++ b/homeassistant/components/litterrobot/translations/nl.json @@ -29,7 +29,11 @@ "status_code": { "state": { "cd": "Kat gedetecteerd", - "p": "Gepauzeerd" + "off": "Uit", + "offline": "Offline", + "p": "Gepauzeerd", + "pwru": "Opstarten", + "rdy": "Klaar" } } } diff --git a/homeassistant/components/litterrobot/translations/tr.json b/homeassistant/components/litterrobot/translations/tr.json index 70b19443055..673561ed092 100644 --- a/homeassistant/components/litterrobot/translations/tr.json +++ b/homeassistant/components/litterrobot/translations/tr.json @@ -25,6 +25,39 @@ } } }, + "entity": { + "sensor": { + "status_code": { + "state": { + "br": "Kapak \u00c7\u0131kar\u0131ld\u0131", + "ccc": "Temizleme Tamamland\u0131", + "ccp": "Temizleme Devam Ediyor", + "cd": "Kedi Tespit Edildi", + "csf": "Kedi Sens\u00f6r\u00fc Hatas\u0131", + "csi": "Kedi Sens\u00f6r\u00fc Kesildi", + "cst": "Kedi Sens\u00f6r Zamanlamas\u0131", + "df1": "Hazne Neredeyse Dolu - 2 kez daha s\u00fcp\u00fcrebilir", + "df2": "Hazne Neredeyse Dolu - 1 kez daha s\u00fcp\u00fcrebilir", + "dfs": "Hazne Dolu", + "dhf": "Bo\u015faltma + Ana Konum Hatas\u0131", + "dpf": "Bo\u015faltma Konumu Hatas\u0131", + "ec": "Bo\u015f D\u00f6ng\u00fc", + "hpf": "Ev Konumu Hatas\u0131", + "off": "Kapal\u0131", + "offline": "\u00c7evrimd\u0131\u015f\u0131", + "otf": "A\u015f\u0131r\u0131 Tork Ar\u0131zas\u0131", + "p": "Durduruldu", + "pd": "S\u0131k\u0131\u015fma Alg\u0131lama", + "pwrd": "G\u00fcc\u00fc Kapatma", + "pwru": "G\u00fcc\u00fc A\u00e7ma", + "rdy": "Haz\u0131r", + "scf": "Ba\u015flang\u0131\u00e7ta Cat Sens\u00f6r\u00fc Hatas\u0131", + "sdf": "Ba\u015flang\u0131\u00e7ta Hazne Dolu", + "spf": "Ba\u015flang\u0131\u00e7ta S\u0131k\u0131\u015fma Alg\u0131lama" + } + } + } + }, "issues": { "migrated_attributes": { "description": "Vakum varl\u0131k \u00f6znitelikleri art\u0131k tan\u0131 sens\u00f6rleri olarak mevcuttur. \n\n L\u00fctfen bu \u00f6znitelikleri kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131 ayarlay\u0131n.", diff --git a/homeassistant/components/litterrobot/translations/uk.json b/homeassistant/components/litterrobot/translations/uk.json new file mode 100644 index 00000000000..1b198cdf577 --- /dev/null +++ b/homeassistant/components/litterrobot/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041e\u043d\u043e\u0432\u0456\u0442\u044c \u0441\u0432\u0456\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}" + }, + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/litterrobot/update.py b/homeassistant/components/litterrobot/update.py index 845b42efaee..33ca6cd0376 100644 --- a/homeassistant/components/litterrobot/update.py +++ b/homeassistant/components/litterrobot/update.py @@ -36,10 +36,9 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot update platform.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - robots = hub.account.robots entities = [ RobotUpdateEntity(robot=robot, hub=hub, description=FIRMWARE_UPDATE_ENTITY) - for robot in robots + for robot in hub.litter_robots() if isinstance(robot, LitterRobot4) ] async_add_entities(entities, True) diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 55f0a182959..1d2d9df0f14 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -9,7 +9,6 @@ from pylitterbot.enums import LitterBoxStatus import voluptuous as vol from homeassistant.components.vacuum import ( - DOMAIN as PLATFORM, STATE_CLEANING, STATE_DOCKED, STATE_ERROR, @@ -26,7 +25,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util from .const import DOMAIN -from .entity import LitterRobotEntity, async_update_unique_id +from .entity import LitterRobotEntity from .hub import LitterRobotHub SERVICE_SET_SLEEP_MODE = "set_sleep_mode" @@ -43,7 +42,7 @@ LITTER_BOX_STATUS_STATE_MAP = { LitterBoxStatus.OFF: STATE_OFF, } -LITTER_BOX_ENTITY = StateVacuumEntityDescription("litter_box", name="Litter Box") +LITTER_BOX_ENTITY = StateVacuumEntityDescription("litter_box", name="Litter box") async def async_setup_entry( @@ -53,12 +52,10 @@ async def async_setup_entry( ) -> None: """Set up Litter-Robot cleaner using config entry.""" hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id] - entities = [ LitterRobotCleaner(robot=robot, hub=hub, description=LITTER_BOX_ENTITY) for robot in hub.litter_robots() ] - async_update_unique_id(hass, PLATFORM, entities) async_add_entities(entities) platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/livisi/translations/sv.json b/homeassistant/components/livisi/translations/sv.json new file mode 100644 index 00000000000..9011fc8f81a --- /dev/null +++ b/homeassistant/components/livisi/translations/sv.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "wrong_password": "L\u00f6senordet \u00e4r felaktigt." + }, + "step": { + "user": { + "data": { + "host": "IP-adress", + "password": "L\u00f6senord" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/tr.json b/homeassistant/components/livisi/translations/tr.json new file mode 100644 index 00000000000..470f94561ec --- /dev/null +++ b/homeassistant/components/livisi/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "wrong_ip_address": "IP adresi yanl\u0131\u015f veya SHC'ye yerel olarak ula\u015f\u0131lam\u0131yor.", + "wrong_password": "\u015eifre yanl\u0131\u015f." + }, + "step": { + "user": { + "data": { + "host": "IP Adresi", + "password": "Parola" + }, + "description": "SHC'nin IP adresini ve (yerel) parolas\u0131n\u0131 girin." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/livisi/translations/uk.json b/homeassistant/components/livisi/translations/uk.json new file mode 100644 index 00000000000..5c722c2a338 --- /dev/null +++ b/homeassistant/components/livisi/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/local_calendar/translations/ca.json b/homeassistant/components/local_calendar/translations/ca.json index a740c433dbc..09337c555a9 100644 --- a/homeassistant/components/local_calendar/translations/ca.json +++ b/homeassistant/components/local_calendar/translations/ca.json @@ -5,7 +5,7 @@ "data": { "calendar_name": "Nom del calendari" }, - "description": "Trieu un nom per al nou calendari" + "description": "Tria un nom per al nou calendari" } } } diff --git a/homeassistant/components/local_calendar/translations/hu.json b/homeassistant/components/local_calendar/translations/hu.json index d8efec9ec9e..47d640626fa 100644 --- a/homeassistant/components/local_calendar/translations/hu.json +++ b/homeassistant/components/local_calendar/translations/hu.json @@ -5,7 +5,7 @@ "data": { "calendar_name": "Napt\u00e1r elnevez\u00e9se" }, - "description": "K\u00e9rj\u00fck, v\u00e1lasszon nevet az \u00faj napt\u00e1r\u00e1nak" + "description": "Nevezze el az \u00faj napt\u00e1r\u00e1t" } } } diff --git a/homeassistant/components/local_calendar/translations/tr.json b/homeassistant/components/local_calendar/translations/tr.json new file mode 100644 index 00000000000..96f072293a9 --- /dev/null +++ b/homeassistant/components/local_calendar/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "calendar_name": "Takvim Ad\u0131" + }, + "description": "L\u00fctfen yeni takviminiz i\u00e7in bir ad se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/tr.json b/homeassistant/components/local_ip/translations/tr.json index d0540ec7a6e..cb9a6dd6053 100644 --- a/homeassistant/components/local_ip/translations/tr.json +++ b/homeassistant/components/local_ip/translations/tr.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Kuruluma ba\u015flamak ister misiniz?", + "description": "Kurulumu ba\u015flatmak istiyor musunuz?", "title": "Yerel IP Adresi" } } diff --git a/homeassistant/components/locative/translations/el.json b/homeassistant/components/locative/translations/el.json index 0bfb57d149f..d5eddae7b7e 100644 --- a/homeassistant/components/locative/translations/el.json +++ b/homeassistant/components/locative/translations/el.json @@ -6,7 +6,7 @@ "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." + "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\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 \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 \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\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": { diff --git a/homeassistant/components/locative/translations/hu.json b/homeassistant/components/locative/translations/hu.json index b74774c3c21..7bcfeb68c49 100644 --- a/homeassistant/components/locative/translations/hu.json +++ b/homeassistant/components/locative/translations/hu.json @@ -6,7 +6,7 @@ "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." }, "create_entry": { - "default": "Ha helyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Locative alkalmaz\u00e1sban. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1sd a [dokument\u00e1ci\u00f3t]({docs_url})." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni Home Assistantba, be kell \u00e1ll\u00edtania a Locative webhook funkci\u00f3j\u00e1t. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 adatokat: \n\n - URL: `{webhook_url}`\n - Met\u00f3dus: POST\n\nB\u0151vebb inform\u00e1ci\u00f3 [a dokument\u00e1ci\u00f3ban]({docs_url}) olvashat\u00f3." }, "step": { "user": { diff --git a/homeassistant/components/locative/translations/tr.json b/homeassistant/components/locative/translations/tr.json index e48ff5d9b56..c658bd84d3f 100644 --- a/homeassistant/components/locative/translations/tr.json +++ b/homeassistant/components/locative/translations/tr.json @@ -6,11 +6,11 @@ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, "create_entry": { - "default": "Konumlar\u0131 Home Assistant'a g\u00f6ndermek i\u00e7in Locative uygulamas\u0131nda webhook \u00f6zelli\u011fini ayarlaman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url}" + "default": "Konumlar\u0131 Home Assistant'a g\u00f6ndermek i\u00e7in Locative uygulamas\u0131nda webhook \u00f6zelli\u011fini ayarlaman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url} ) bak\u0131n." }, "step": { "user": { - "description": "Kuruluma ba\u015flamak ister misiniz?", + "description": "Kurulumu ba\u015flatmak istiyor musunuz?", "title": "Konum Belirleyici Webhook'u ayarlay\u0131n" } } diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index 5008fa0ca2b..202fe8cff73 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -6,6 +6,7 @@ from datetime import timedelta from enum import IntFlag import functools as ft import logging +import re from typing import Any, final import voluptuous as vol @@ -23,7 +24,7 @@ from homeassistant.const import ( STATE_UNLOCKED, STATE_UNLOCKING, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -72,18 +73,48 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await component.async_setup(config) component.async_register_entity_service( - SERVICE_UNLOCK, LOCK_SERVICE_SCHEMA, "async_unlock" + SERVICE_UNLOCK, LOCK_SERVICE_SCHEMA, _async_unlock ) component.async_register_entity_service( - SERVICE_LOCK, LOCK_SERVICE_SCHEMA, "async_lock" + SERVICE_LOCK, LOCK_SERVICE_SCHEMA, _async_lock ) component.async_register_entity_service( - SERVICE_OPEN, LOCK_SERVICE_SCHEMA, "async_open" + SERVICE_OPEN, LOCK_SERVICE_SCHEMA, _async_open, [LockEntityFeature.OPEN] ) return True +async def _async_lock(entity: LockEntity, service_call: ServiceCall) -> None: + """Lock the lock.""" + code: str = service_call.data.get(ATTR_CODE, "") + if entity.code_format_cmp and not entity.code_format_cmp.match(code): + raise ValueError( + f"Code '{code}' for locking {entity.name} doesn't match pattern {entity.code_format}" + ) + await entity.async_lock(**service_call.data) + + +async def _async_unlock(entity: LockEntity, service_call: ServiceCall) -> None: + """Unlock the lock.""" + code: str = service_call.data.get(ATTR_CODE, "") + if entity.code_format_cmp and not entity.code_format_cmp.match(code): + raise ValueError( + f"Code '{code}' for unlocking {entity.name} doesn't match pattern {entity.code_format}" + ) + await entity.async_unlock(**service_call.data) + + +async def _async_open(entity: LockEntity, service_call: ServiceCall) -> None: + """Open the door latch.""" + code: str = service_call.data.get(ATTR_CODE, "") + if entity.code_format_cmp and not entity.code_format_cmp.match(code): + raise ValueError( + f"Code '{code}' for opening {entity.name} doesn't match pattern {entity.code_format}" + ) + await entity.async_open(**service_call.data) + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" component: EntityComponent[LockEntity] = hass.data[DOMAIN] @@ -113,6 +144,7 @@ class LockEntity(Entity): _attr_is_jammed: bool | None = None _attr_state: None = None _attr_supported_features: LockEntityFeature = LockEntityFeature(0) + __code_format_cmp: re.Pattern[str] | None = None @property def changed_by(self) -> str | None: @@ -124,6 +156,20 @@ class LockEntity(Entity): """Regex for code format or None if no code is required.""" return self._attr_code_format + @property + @final + def code_format_cmp(self) -> re.Pattern[str] | None: + """Return a compiled code_format.""" + if self.code_format is None: + self.__code_format_cmp = None + return None + if ( + not self.__code_format_cmp + or self.code_format != self.__code_format_cmp.pattern + ): + self.__code_format_cmp = re.compile(self.code_format) + return self.__code_format_cmp + @property def is_locked(self) -> bool | None: """Return true if the lock is locked.""" diff --git a/homeassistant/components/lock/translations/it.json b/homeassistant/components/lock/translations/it.json index 28f4476e5b8..e50a379b8ed 100644 --- a/homeassistant/components/lock/translations/it.json +++ b/homeassistant/components/lock/translations/it.json @@ -1,23 +1,23 @@ { "device_automation": { "action_type": { - "lock": "Blocca {entity_name}", + "lock": "Chiudi {entity_name}", "open": "Apri {entity_name}", "unlock": "Sblocca {entity_name}" }, "condition_type": { - "is_locked": "{entity_name} \u00e8 bloccato", - "is_unlocked": "{entity_name} \u00e8 sbloccato" + "is_locked": "{entity_name} \u00e8 chiusa", + "is_unlocked": "{entity_name} \u00e8 aperta" }, "trigger_type": { - "locked": "{entity_name} \u00e8 bloccato", - "unlocked": "{entity_name} \u00e8 sbloccato" + "locked": "{entity_name} \u00e8 chiusa", + "unlocked": "{entity_name} \u00e8 aperta" } }, "state": { "_": { - "locked": "Bloccato", - "unlocked": "Sbloccato" + "locked": "Chiusa", + "unlocked": "Aperta" } }, "title": "Serratura" diff --git a/homeassistant/components/logbook/models.py b/homeassistant/components/logbook/models.py index 591781745f7..0b0a9aeb414 100644 --- a/homeassistant/components/logbook/models.py +++ b/homeassistant/components/logbook/models.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime as dt import json from typing import Any, cast @@ -10,6 +9,7 @@ from sqlalchemy.engine.row import Row from homeassistant.const import ATTR_ICON, EVENT_STATE_CHANGED from homeassistant.core import Context, Event, State, callback +import homeassistant.util.dt as dt_util class LazyEventPartialState: @@ -66,7 +66,7 @@ class EventAsRow: data: dict[str, Any] context: Context context_id: str - time_fired: dt + time_fired_ts: float state_id: int event_data: str | None = None old_format_icon: None = None @@ -92,7 +92,7 @@ def async_event_to_row(event: Event) -> EventAsRow | None: context_id=event.context.id, context_user_id=event.context.user_id, context_parent_id=event.context.parent_id, - time_fired=event.time_fired, + time_fired_ts=dt_util.utc_to_timestamp(event.time_fired), state_id=hash(event), ) # States are prefiltered so we never get states @@ -107,7 +107,7 @@ def async_event_to_row(event: Event) -> EventAsRow | None: context_id=new_state.context.id, context_user_id=new_state.context.user_id, context_parent_id=new_state.context.parent_id, - time_fired=new_state.last_updated, + time_fired_ts=dt_util.utc_to_timestamp(new_state.last_updated), state_id=hash(event), icon=new_state.attributes.get(ATTR_ICON), ) diff --git a/homeassistant/components/logbook/processor.py b/homeassistant/components/logbook/processor.py index 6d491ec2892..d73a852ca1c 100644 --- a/homeassistant/components/logbook/processor.py +++ b/homeassistant/components/logbook/processor.py @@ -56,7 +56,7 @@ from .const import ( from .helpers import is_sensor_continuous from .models import EventAsRow, LazyEventPartialState, async_event_to_row from .queries import statement_for_request -from .queries.common import PSUEDO_EVENT_STATE_CHANGED +from .queries.common import PSEUDO_EVENT_STATE_CHANGED @dataclass @@ -201,7 +201,7 @@ def _humanify( event_type = row.event_type if event_type == EVENT_CALL_SERVICE: continue - if event_type is PSUEDO_EVENT_STATE_CHANGED: + if event_type is PSEUDO_EVENT_STATE_CHANGED: entity_id = row.entity_id assert entity_id is not None # Skip continuous sensors @@ -388,12 +388,14 @@ def _rows_match(row: Row | EventAsRow, other_row: Row | EventAsRow) -> bool: def _row_time_fired_isoformat(row: Row | EventAsRow) -> str: """Convert the row timed_fired to isoformat.""" - return process_timestamp_to_utc_isoformat(row.time_fired or dt_util.utcnow()) + return process_timestamp_to_utc_isoformat( + dt_util.utc_from_timestamp(row.time_fired_ts) or dt_util.utcnow() + ) def _row_time_fired_timestamp(row: Row | EventAsRow) -> float: """Convert the row timed_fired to timestamp.""" - return process_datetime_to_timestamp(row.time_fired or dt_util.utcnow()) + return row.time_fired_ts or process_datetime_to_timestamp(dt_util.utcnow()) class EntityNameCache: diff --git a/homeassistant/components/logbook/queries/__init__.py b/homeassistant/components/logbook/queries/__init__.py index 0c3a63f990e..8a2ee40de4f 100644 --- a/homeassistant/components/logbook/queries/__init__.py +++ b/homeassistant/components/logbook/queries/__init__.py @@ -7,6 +7,7 @@ from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components.recorder.filters import Filters from homeassistant.helpers.json import json_dumps +from homeassistant.util import dt as dt_util from .all import all_stmt from .devices import devices_stmt @@ -15,8 +16,8 @@ from .entities_and_devices import entities_devices_stmt def statement_for_request( - start_day: dt, - end_day: dt, + start_day_dt: dt, + end_day_dt: dt, event_types: tuple[str, ...], entity_ids: list[str] | None = None, device_ids: list[str] | None = None, @@ -24,7 +25,8 @@ def statement_for_request( context_id: str | None = None, ) -> StatementLambdaElement: """Generate the logbook statement for a logbook request.""" - + start_day = dt_util.utc_to_timestamp(start_day_dt) + end_day = dt_util.utc_to_timestamp(end_day_dt) # No entities: logbook sends everything for the timeframe # limited by the context_id and the yaml configured filter if not entity_ids and not device_ids: diff --git a/homeassistant/components/logbook/queries/all.py b/homeassistant/components/logbook/queries/all.py index da05aa02fff..21624181a3b 100644 --- a/homeassistant/components/logbook/queries/all.py +++ b/homeassistant/components/logbook/queries/all.py @@ -1,15 +1,13 @@ """All queries for logbook.""" from __future__ import annotations -from datetime import datetime as dt - from sqlalchemy import lambda_stmt from sqlalchemy.orm import Query from sqlalchemy.sql.elements import ClauseList from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components.recorder.db_schema import ( - LAST_UPDATED_INDEX, + LAST_UPDATED_INDEX_TS, Events, States, ) @@ -23,8 +21,8 @@ from .common import ( def all_stmt( - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], states_entity_filter: ClauseList | None = None, events_entity_filter: ClauseList | None = None, @@ -53,22 +51,24 @@ def all_stmt( else: stmt += lambda s: s.union_all(_states_query_for_all(start_day, end_day)) - stmt += lambda s: s.order_by(Events.time_fired) + stmt += lambda s: s.order_by(Events.time_fired_ts) return stmt -def _states_query_for_all(start_day: dt, end_day: dt) -> Query: +def _states_query_for_all(start_day: float, end_day: float) -> Query: return apply_states_filters(_apply_all_hints(select_states()), start_day, end_day) def _apply_all_hints(query: Query) -> Query: """Force mysql to use the right index on large selects.""" return query.with_hint( - States, f"FORCE INDEX ({LAST_UPDATED_INDEX})", dialect_name="mysql" + States, f"FORCE INDEX ({LAST_UPDATED_INDEX_TS})", dialect_name="mysql" ) -def _states_query_for_context_id(start_day: dt, end_day: dt, context_id: str) -> Query: +def _states_query_for_context_id( + start_day: float, end_day: float, context_id: str +) -> Query: return apply_states_filters(select_states(), start_day, end_day).where( States.context_id == context_id ) diff --git a/homeassistant/components/logbook/queries/common.py b/homeassistant/components/logbook/queries/common.py index 466df668da8..362766504e3 100644 --- a/homeassistant/components/logbook/queries/common.py +++ b/homeassistant/components/logbook/queries/common.py @@ -1,7 +1,7 @@ """Queries for logbook.""" from __future__ import annotations -from datetime import datetime as dt +from typing import Final import sqlalchemy from sqlalchemy import select @@ -35,7 +35,7 @@ ALWAYS_CONTINUOUS_ENTITY_ID_LIKE = like_domain_matchers(ALWAYS_CONTINUOUS_DOMAIN UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%" -PSUEDO_EVENT_STATE_CHANGED = None +PSEUDO_EVENT_STATE_CHANGED: Final = None # Since we don't store event_types and None # and we don't store state_changed in events # we use a NULL for state_changed events @@ -47,7 +47,7 @@ EVENT_COLUMNS = ( Events.event_id.label("event_id"), Events.event_type.label("event_type"), Events.event_data.label("event_data"), - Events.time_fired.label("time_fired"), + Events.time_fired_ts.label("time_fired_ts"), Events.context_id.label("context_id"), Events.context_user_id.label("context_user_id"), Events.context_parent_id.label("context_parent_id"), @@ -71,15 +71,15 @@ STATE_CONTEXT_ONLY_COLUMNS = ( EVENT_COLUMNS_FOR_STATE_SELECT = [ literal(value=None, type_=sqlalchemy.Text).label("event_id"), - # We use PSUEDO_EVENT_STATE_CHANGED aka None for + # We use PSEUDO_EVENT_STATE_CHANGED aka None for # state_changed events since it takes up less # space in the response and every row has to be # marked with the event_type - literal(value=PSUEDO_EVENT_STATE_CHANGED, type_=sqlalchemy.String).label( + literal(value=PSEUDO_EVENT_STATE_CHANGED, type_=sqlalchemy.String).label( "event_type" ), literal(value=None, type_=sqlalchemy.Text).label("event_data"), - States.last_updated.label("time_fired"), + States.last_updated_ts.label("time_fired_ts"), States.context_id.label("context_id"), States.context_user_id.label("context_user_id"), States.context_parent_id.label("context_parent_id"), @@ -108,14 +108,14 @@ NOT_CONTEXT_ONLY = literal(None).label("context_only") def select_events_context_id_subquery( - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], ) -> Select: """Generate the select for a context_id subquery.""" return ( select(Events.context_id) - .where((Events.time_fired > start_day) & (Events.time_fired < end_day)) + .where((Events.time_fired_ts > start_day) & (Events.time_fired_ts < end_day)) .where(Events.event_type.in_(event_types)) .outerjoin(EventData, (Events.data_id == EventData.data_id)) ) @@ -142,12 +142,12 @@ def select_states_context_only() -> Select: def select_events_without_states( - start_day: dt, end_day: dt, event_types: tuple[str, ...] + start_day: float, end_day: float, event_types: tuple[str, ...] ) -> Select: """Generate an events select that does not join states.""" return ( select(*EVENT_ROWS_NO_STATES, NOT_CONTEXT_ONLY) - .where((Events.time_fired > start_day) & (Events.time_fired < end_day)) + .where((Events.time_fired_ts > start_day) & (Events.time_fired_ts < end_day)) .where(Events.event_type.in_(event_types)) .outerjoin(EventData, (Events.data_id == EventData.data_id)) ) @@ -163,7 +163,7 @@ def select_states() -> Select: def legacy_select_events_context_id( - start_day: dt, end_day: dt, context_id: str + start_day: float, end_day: float, context_id: str ) -> Select: """Generate a legacy events context id select that also joins states.""" # This can be removed once we no longer have event_ids in the states table @@ -176,33 +176,35 @@ def legacy_select_events_context_id( ) .outerjoin(States, (Events.event_id == States.event_id)) .where( - (States.last_updated == States.last_changed) | States.last_changed.is_(None) + (States.last_updated_ts == States.last_changed_ts) + | States.last_changed_ts.is_(None) ) .where(_not_continuous_entity_matcher()) .outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) ) - .where((Events.time_fired > start_day) & (Events.time_fired < end_day)) + .where((Events.time_fired_ts > start_day) & (Events.time_fired_ts < end_day)) .where(Events.context_id == context_id) ) -def apply_states_filters(query: Query, start_day: dt, end_day: dt) -> Query: +def apply_states_filters(query: Query, start_day: float, end_day: float) -> Query: """Filter states by time range. Filters states that do not have an old state or new state (added / removed) Filters states that are in a continuous domain with a UOM. - Filters states that do not have matching last_updated and last_changed. + Filters states that do not have matching last_updated_ts and last_changed_ts. """ return ( query.filter( - (States.last_updated > start_day) & (States.last_updated < end_day) + (States.last_updated_ts > start_day) & (States.last_updated_ts < end_day) ) .outerjoin(OLD_STATE, (States.old_state_id == OLD_STATE.state_id)) .where(_missing_state_matcher()) .where(_not_continuous_entity_matcher()) .where( - (States.last_updated == States.last_changed) | States.last_changed.is_(None) + (States.last_updated_ts == States.last_changed_ts) + | States.last_changed_ts.is_(None) ) .outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) diff --git a/homeassistant/components/logbook/queries/devices.py b/homeassistant/components/logbook/queries/devices.py index e268c2d3ac3..a270f1996ce 100644 --- a/homeassistant/components/logbook/queries/devices.py +++ b/homeassistant/components/logbook/queries/devices.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Iterable -from datetime import datetime as dt import sqlalchemy from sqlalchemy import lambda_stmt, select @@ -29,8 +28,8 @@ from .common import ( def _select_device_id_context_ids_sub_query( - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], json_quotable_device_ids: list[str], ) -> CompoundSelect: @@ -43,8 +42,8 @@ def _select_device_id_context_ids_sub_query( def _apply_devices_context_union( query: Query, - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], json_quotable_device_ids: list[str], ) -> CompoundSelect: @@ -70,8 +69,8 @@ def _apply_devices_context_union( def devices_stmt( - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], json_quotable_device_ids: list[str], ) -> StatementLambdaElement: @@ -85,7 +84,7 @@ def devices_stmt( end_day, event_types, json_quotable_device_ids, - ).order_by(Events.time_fired) + ).order_by(Events.time_fired_ts) ) return stmt diff --git a/homeassistant/components/logbook/queries/entities.py b/homeassistant/components/logbook/queries/entities.py index 3803da6f4e8..afe7c7c7c2e 100644 --- a/homeassistant/components/logbook/queries/entities.py +++ b/homeassistant/components/logbook/queries/entities.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Iterable -from datetime import datetime as dt import sqlalchemy from sqlalchemy import lambda_stmt, select, union_all @@ -12,7 +11,7 @@ from sqlalchemy.sql.selectable import CTE, CompoundSelect from homeassistant.components.recorder.db_schema import ( ENTITY_ID_IN_EVENT, - ENTITY_ID_LAST_UPDATED_INDEX, + ENTITY_ID_LAST_UPDATED_INDEX_TS, OLD_ENTITY_ID_IN_EVENT, EventData, Events, @@ -32,8 +31,8 @@ from .common import ( def _select_entities_context_ids_sub_query( - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], entity_ids: list[str], json_quoted_entity_ids: list[str], @@ -44,7 +43,9 @@ def _select_entities_context_ids_sub_query( apply_event_entity_id_matchers(json_quoted_entity_ids) ), apply_entities_hints(select(States.context_id)) - .filter((States.last_updated > start_day) & (States.last_updated < end_day)) + .filter( + (States.last_updated_ts > start_day) & (States.last_updated_ts < end_day) + ) .where(States.entity_id.in_(entity_ids)), ) return select(union.c.context_id).group_by(union.c.context_id) @@ -52,8 +53,8 @@ def _select_entities_context_ids_sub_query( def _apply_entities_context_union( query: Query, - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], entity_ids: list[str], json_quoted_entity_ids: list[str], @@ -87,8 +88,8 @@ def _apply_entities_context_union( def entities_stmt( - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], entity_ids: list[str], json_quoted_entity_ids: list[str], @@ -104,12 +105,12 @@ def entities_stmt( event_types, entity_ids, json_quoted_entity_ids, - ).order_by(Events.time_fired) + ).order_by(Events.time_fired_ts) ) def states_query_for_entity_ids( - start_day: dt, end_day: dt, entity_ids: list[str] + start_day: float, end_day: float, entity_ids: list[str] ) -> Query: """Generate a select for states from the States table for specific entities.""" return apply_states_filters( @@ -136,5 +137,5 @@ def apply_event_entity_id_matchers( def apply_entities_hints(query: Query) -> Query: """Force mysql to use the right index on large selects.""" return query.with_hint( - States, f"FORCE INDEX ({ENTITY_ID_LAST_UPDATED_INDEX})", dialect_name="mysql" + States, f"FORCE INDEX ({ENTITY_ID_LAST_UPDATED_INDEX_TS})", dialect_name="mysql" ) diff --git a/homeassistant/components/logbook/queries/entities_and_devices.py b/homeassistant/components/logbook/queries/entities_and_devices.py index f22a8392e19..94e9afc551d 100644 --- a/homeassistant/components/logbook/queries/entities_and_devices.py +++ b/homeassistant/components/logbook/queries/entities_and_devices.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Iterable -from datetime import datetime as dt import sqlalchemy from sqlalchemy import lambda_stmt, select, union_all @@ -29,8 +28,8 @@ from .entities import ( def _select_entities_device_id_context_ids_sub_query( - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], entity_ids: list[str], json_quoted_entity_ids: list[str], @@ -44,7 +43,9 @@ def _select_entities_device_id_context_ids_sub_query( ) ), apply_entities_hints(select(States.context_id)) - .filter((States.last_updated > start_day) & (States.last_updated < end_day)) + .filter( + (States.last_updated_ts > start_day) & (States.last_updated_ts < end_day) + ) .where(States.entity_id.in_(entity_ids)), ) return select(union.c.context_id).group_by(union.c.context_id) @@ -52,8 +53,8 @@ def _select_entities_device_id_context_ids_sub_query( def _apply_entities_devices_context_union( query: Query, - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], entity_ids: list[str], json_quoted_entity_ids: list[str], @@ -88,8 +89,8 @@ def _apply_entities_devices_context_union( def entities_devices_stmt( - start_day: dt, - end_day: dt, + start_day: float, + end_day: float, event_types: tuple[str, ...], entity_ids: list[str], json_quoted_entity_ids: list[str], @@ -109,7 +110,7 @@ def entities_devices_stmt( entity_ids, json_quoted_entity_ids, json_quoted_device_ids, - ).order_by(Events.time_fired) + ).order_by(Events.time_fired_ts) ) return stmt diff --git a/homeassistant/components/logbook/websocket_api.py b/homeassistant/components/logbook/websocket_api.py index 04b288d523b..dac0da83c36 100644 --- a/homeassistant/components/logbook/websocket_api.py +++ b/homeassistant/components/logbook/websocket_api.py @@ -392,6 +392,10 @@ async def ws_event_stream( force_send=True, ) + if msg_id not in connection.subscriptions: + # Unsubscribe happened while sending historical events + return + live_stream.task = asyncio.create_task( _async_events_consumer( subscriptions_setup_complete_time, @@ -402,10 +406,6 @@ async def ws_event_stream( ) ) - if msg_id not in connection.subscriptions: - # Unsubscribe happened while sending historical events - return - live_stream.wait_sync_task = asyncio.create_task( get_instance(hass).async_block_till_done() ) diff --git a/homeassistant/components/lookin/config_flow.py b/homeassistant/components/lookin/config_flow.py index 2b4df9cc027..895d071ab4e 100644 --- a/homeassistant/components/lookin/config_flow.py +++ b/homeassistant/components/lookin/config_flow.py @@ -31,7 +31,7 @@ class LookinFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self, discovery_info: zeroconf.ZeroconfServiceInfo ) -> FlowResult: """Start a discovery flow from zeroconf.""" - uid: str = discovery_info.hostname[: -len(".local.")] + uid: str = discovery_info.hostname.removesuffix(".local.") host: str = discovery_info.host await self.async_set_unique_id(uid.upper()) self._abort_if_unique_id_configured(updates={CONF_HOST: host}) @@ -43,9 +43,8 @@ class LookinFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except Exception: # pylint: disable=broad-except LOGGER.exception("Unexpected exception") return self.async_abort(reason="unknown") - else: - self._name = device.name + self._name = device.name self._host = host self._set_confirm_only() self.context["title_placeholders"] = {"name": self._name, "host": host} diff --git a/homeassistant/components/lookin/translations/lv.json b/homeassistant/components/lookin/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/lookin/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lookin/translations/tr.json b/homeassistant/components/lookin/translations/tr.json index b1751d5d413..be431e50a33 100644 --- a/homeassistant/components/lookin/translations/tr.json +++ b/homeassistant/components/lookin/translations/tr.json @@ -19,7 +19,7 @@ } }, "discovery_confirm": { - "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?" + "description": "{name} ( {host} ) kurmak istiyor musunuz?" }, "user": { "data": { diff --git a/homeassistant/components/lovelace/dashboard.py b/homeassistant/components/lovelace/dashboard.py index 377ba33f171..ef47ea0b1fc 100644 --- a/homeassistant/components/lovelace/dashboard.py +++ b/homeassistant/components/lovelace/dashboard.py @@ -6,7 +6,7 @@ import logging import os from pathlib import Path import time -from typing import Optional, cast +from typing import cast import voluptuous as vol @@ -234,7 +234,7 @@ class DashboardsCollection(collection.StorageCollection): async def _async_load_data(self) -> dict | None: """Load the data.""" if (data := await self.store.async_load()) is None: - return cast(Optional[dict], data) + return cast(dict | None, data) updated = False @@ -246,7 +246,7 @@ class DashboardsCollection(collection.StorageCollection): if updated: await self.store.async_save(data) - return cast(Optional[dict], data) + return cast(dict | None, data) async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" diff --git a/homeassistant/components/lovelace/resources.py b/homeassistant/components/lovelace/resources.py index 7e7c670baf5..e6c4acfdf69 100644 --- a/homeassistant/components/lovelace/resources.py +++ b/homeassistant/components/lovelace/resources.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Optional, cast +from typing import cast import uuid import voluptuous as vol @@ -71,7 +71,7 @@ class ResourceStorageCollection(collection.StorageCollection): async def _async_load_data(self) -> dict | None: """Load the data.""" if (data := await self.store.async_load()) is not None: - return cast(Optional[dict], data) + return cast(dict | None, data) # Import it from config. try: diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index 76d8ec2a53d..f1403e1b2d7 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -2,7 +2,7 @@ "domain": "lupusec", "name": "Lupus Electronics LUPUSEC", "documentation": "https://www.home-assistant.io/integrations/lupusec", - "requirements": ["lupupy==0.2.4"], + "requirements": ["lupupy==0.2.5"], "codeowners": ["@majuss"], "iot_class": "local_polling", "loggers": ["lupupy"] diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index d65ca852da7..388ef7c1ee0 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -2,7 +2,7 @@ "domain": "lutron_caseta", "name": "Lutron Cas\u00e9ta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", - "requirements": ["pylutron-caseta==0.17.1"], + "requirements": ["pylutron-caseta==0.18.0"], "config_flow": true, "zeroconf": [ { diff --git a/homeassistant/components/lutron_caseta/translations/hu.json b/homeassistant/components/lutron_caseta/translations/hu.json index 5796d1b2816..b3d960e79b3 100644 --- a/homeassistant/components/lutron_caseta/translations/hu.json +++ b/homeassistant/components/lutron_caseta/translations/hu.json @@ -11,7 +11,7 @@ "flow_title": "{name} ({host})", "step": { "import_failed": { - "description": "Nem siker\u00fclt be\u00e1ll\u00edtani a bridge-t ({host}) a configuration.yaml f\u00e1jlb\u00f3l import\u00e1lva.", + "description": "Nem siker\u00fclt be\u00e1ll\u00edtani a configuration.yaml f\u00e1jlb\u00f3l import\u00e1lt bridge-t ({host} c\u00edmen).", "title": "Nem siker\u00fclt import\u00e1lni a Cas\u00e9ta h\u00edd konfigur\u00e1ci\u00f3j\u00e1t." }, "link": { diff --git a/homeassistant/components/lutron_caseta/translations/lv.json b/homeassistant/components/lutron_caseta/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/sk.json b/homeassistant/components/lutron_caseta/translations/sk.json index eac14c88454..e7011fd08e0 100644 --- a/homeassistant/components/lutron_caseta/translations/sk.json +++ b/homeassistant/components/lutron_caseta/translations/sk.json @@ -73,7 +73,7 @@ }, "trigger_type": { "press": "\"{subtype}\" stla\u010den\u00e9", - "release": "\u201c{subtype}\u201c uvo\u013enen\u00e9" + "release": "\"{subtype}\" uvo\u013enen\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/tr.json b/homeassistant/components/lutron_caseta/translations/tr.json index df94879f332..34ac0a40206 100644 --- a/homeassistant/components/lutron_caseta/translations/tr.json +++ b/homeassistant/components/lutron_caseta/translations/tr.json @@ -11,7 +11,7 @@ "flow_title": "{name} ({host})", "step": { "import_failed": { - "description": "Configuration.yaml'den i\u00e7e aktar\u0131lan k\u00f6pr\u00fc (ana bilgisayar: {host}) kurulamad\u0131.", + "description": "Configuration.yaml'den i\u00e7e aktar\u0131lan k\u00f6pr\u00fc (host: {host} ) kurulamad\u0131.", "title": "Cas\u00e9ta k\u00f6pr\u00fc yap\u0131land\u0131rmas\u0131 i\u00e7e aktar\u0131lamad\u0131." }, "link": { diff --git a/homeassistant/components/lyric/translations/uk.json b/homeassistant/components/lyric/translations/uk.json new file mode 100644 index 00000000000..2c71bf8d517 --- /dev/null +++ b/homeassistant/components/lyric/translations/uk.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/sensor.py b/homeassistant/components/magicseaweed/sensor.py index 0fb2df6aaca..3c3eb939181 100644 --- a/homeassistant/components/magicseaweed/sensor.py +++ b/homeassistant/components/magicseaweed/sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS, CONF_NA from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.issue_registry import IssueSeverity, create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import Throttle import homeassistant.util.dt as dt_util @@ -80,6 +81,20 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Magicseaweed sensor.""" + create_issue( + hass, + "magicseaweed", + "pending_removal", + breaks_in_ha_version="2023.3.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="pending_removal", + ) + _LOGGER.warning( + "The Magicseaweed integration is deprecated" + " and will be removed in Home Assistant 2023.3" + ) + name = config.get(CONF_NAME) spot_id = config[CONF_SPOT_ID] api_key = config[CONF_API_KEY] diff --git a/homeassistant/components/magicseaweed/strings.json b/homeassistant/components/magicseaweed/strings.json new file mode 100644 index 00000000000..0aa8a584190 --- /dev/null +++ b/homeassistant/components/magicseaweed/strings.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "title": "The Magicseaweed integration is being removed", + "description": "The Magicseaweed integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2023.3.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/components/magicseaweed/translations/bg.json b/homeassistant/components/magicseaweed/translations/bg.json new file mode 100644 index 00000000000..a61d721ae47 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/bg.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Magicseaweed \u043f\u0440\u0435\u0434\u0441\u0442\u043e\u0438 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430 \u043e\u0442 Home Assistant \u0438 \u0432\u0435\u0447\u0435 \u043d\u044f\u043c\u0430 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043b\u0438\u0447\u043d\u0430 \u043e\u0442 Home Assistant 2023.3.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Magicseaweed \u0441\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u0432\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/ca.json b/homeassistant/components/magicseaweed/translations/ca.json new file mode 100644 index 00000000000..6ca53ac5447 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/ca.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "La integraci\u00f3 Magicseaweed s'eliminar\u00e0 de Home Assistant i deixar\u00e0 d'estar disponible a la versi\u00f3 de Home Assistant 2023.3.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per eliminar aquest error.", + "title": "La integraci\u00f3 Magicseaweed est\u00e0 sent eliminada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/de.json b/homeassistant/components/magicseaweed/translations/de.json new file mode 100644 index 00000000000..39667740be9 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/de.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Die Magicseaweed-Integration wird in K\u00fcrze aus Home Assistant entfernt und wird ab Home Assistant 2023.3 nicht mehr verf\u00fcgbar sein.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Magicseaweed-Integration wird entfernt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/el.json b/homeassistant/components/magicseaweed/translations/el.json new file mode 100644 index 00000000000..c1b3f14917c --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/el.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Magicseaweed \u03b5\u03ba\u03ba\u03c1\u03b5\u03bc\u03b5\u03af \u03ba\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant 2023.3. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Magicseaweed \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/en.json b/homeassistant/components/magicseaweed/translations/en.json new file mode 100644 index 00000000000..20d3c401e5d --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "The Magicseaweed integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2023.3.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Magicseaweed integration is being removed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/es.json b/homeassistant/components/magicseaweed/translations/es.json new file mode 100644 index 00000000000..78a9dd172ec --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/es.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "La integraci\u00f3n Magicseaweed est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2023.3. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la integraci\u00f3n Magicseaweed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/et.json b/homeassistant/components/magicseaweed/translations/et.json new file mode 100644 index 00000000000..98206c35b79 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/et.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Magicseaweedi integratsioon on Home Assistantist eemaldamisel ja see ei ole enam saadaval alates Home Assistant 2023.3.\n\nProbleemi lahendamiseks eemalda YAML-konfiguratsioon failist configuration.yaml ja k\u00e4ivita Home Assistant uuesti.", + "title": "Magicseaweedi integratsioon eemaldatakse" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/hu.json b/homeassistant/components/magicseaweed/translations/hu.json new file mode 100644 index 00000000000..03e588a679e --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/hu.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "A Magicseaweed integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra v\u00e1r a Home Assistantb\u00f3l, \u00e9s a Home Assistant 2023.3-t\u00f3l m\u00e1r nem lesz el\u00e9rhet\u0151.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", + "title": "A Magicseaweed integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/id.json b/homeassistant/components/magicseaweed/translations/id.json new file mode 100644 index 00000000000..216d27f09d6 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/id.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integrasi Magicseaweed sedang menunggu penghapusan dari Home Assistant dan tidak akan lagi tersedia pada Home Assistant 2023.3.\n\nHapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Integrasi Magicseaweed dalam proses penghapusan" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/it.json b/homeassistant/components/magicseaweed/translations/it.json new file mode 100644 index 00000000000..9c5e06f71f5 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/it.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "L'integrazione di Magicseaweed \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2023.3. \n\nRimuovi la configurazione YAML dal tuo file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", + "title": "L'integrazione di Magicseaweed \u00e8 stata rimossa" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/no.json b/homeassistant/components/magicseaweed/translations/no.json new file mode 100644 index 00000000000..3f4d8adc656 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/no.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Magicseaweed-integrasjonen venter p\u00e5 fjerning fra Home Assistant og vil ikke lenger v\u00e6re tilgjengelig fra og med Home Assistant 2023.3. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Magicseaweed-integrasjonen blir fjernet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/pl.json b/homeassistant/components/magicseaweed/translations/pl.json new file mode 100644 index 00000000000..36612d86847 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/pl.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integracja Magicseaweed Local oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2023.3. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Integracja Magicseaweed zostanie usuni\u0119ta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/pt-BR.json b/homeassistant/components/magicseaweed/translations/pt-BR.json new file mode 100644 index 00000000000..ab337f75887 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "A integra\u00e7\u00e3o do Magicseaweed est\u00e1 pendente de remo\u00e7\u00e3o do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2023.3. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A integra\u00e7\u00e3o do Magicseaweed est\u00e1 sendo removida" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/ru.json b/homeassistant/components/magicseaweed/translations/ru.json new file mode 100644 index 00000000000..6a6c44c4e54 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/ru.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Magicseaweed \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2023.3. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Magicseaweed \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/sk.json b/homeassistant/components/magicseaweed/translations/sk.json new file mode 100644 index 00000000000..8d677efcb02 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/sk.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Integr\u00e1cia Magicseaweed \u010dak\u00e1 na odstr\u00e1nenie z Home Assistant a od Home Assistant 2023.3 u\u017e nebude k dispoz\u00edcii. \n\n Ak chcete tento probl\u00e9m vyrie\u0161i\u0165, odstr\u00e1\u0148te konfigur\u00e1ciu YAML zo s\u00faboru configuration.yaml a re\u0161tartujte aplik\u00e1ciu Home Assistant.", + "title": "Integr\u00e1cia Magicseaweed sa odstra\u0148uje" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/tr.json b/homeassistant/components/magicseaweed/translations/tr.json new file mode 100644 index 00000000000..2f7226f5013 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/tr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Magicseaweed entegrasyonu, Ev Asistan\u0131ndan kald\u0131r\u0131lmay\u0131 bekliyor ve Home Asistan\u0131 2023.3'ten itibaren kullan\u0131lamayacak. \n\n YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu \u00e7\u00f6zmek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Magicseaweed entegrasyonu kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/uk.json b/homeassistant/components/magicseaweed/translations/uk.json new file mode 100644 index 00000000000..65fd5c55a56 --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/uk.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f Magicseaweed \u043e\u0447\u0456\u043a\u0443\u0454 \u043d\u0430 \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043d\u044f \u0437 Home Assistant \u0456 \u0431\u0456\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0437 Home Assistant 2023.3.\n\n\u0412\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e YAML \u0456\u0437 \u0444\u0430\u0439\u043b\u0443 configuration.yaml \u0456 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c Home Assistant, \u0449\u043e\u0431 \u0440\u043e\u0437\u0432'\u044f\u0437\u0430\u0442\u0438 \u0446\u044e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f Magicseaweed \u0432\u0438\u0434\u0430\u043b\u044f\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/magicseaweed/translations/zh-Hant.json b/homeassistant/components/magicseaweed/translations/zh-Hant.json new file mode 100644 index 00000000000..517dc67469c --- /dev/null +++ b/homeassistant/components/magicseaweed/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Magicseaweed \u672c\u5730\u7aef\u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2023.3 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Magicseaweed \u6574\u5408\u5373\u5c07\u79fb\u9664" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/el.json b/homeassistant/components/mailgun/translations/el.json index bc2821c0881..62c93625bea 100644 --- a/homeassistant/components/mailgun/translations/el.json +++ b/homeassistant/components/mailgun/translations/el.json @@ -6,7 +6,7 @@ "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." + "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 [Webhooks \u03bc\u03b5 \u03c4\u03bf Mailgun]({mailgun_url}).\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- \u03a4\u03cd\u03c0\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5: application/json\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 \u03c4\u03bf \u03c0\u03ce\u03c2 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03c5\u03c2 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03b5\u03b9\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c4\u03b1 \u03b5\u03b9\u03c3\u03b5\u03c1\u03c7\u03cc\u03bc\u03b5\u03bd\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/translations/hu.json b/homeassistant/components/mailgun/translations/hu.json index 17b45578ba3..19b31e49a8e 100644 --- a/homeassistant/components/mailgun/translations/hu.json +++ b/homeassistant/components/mailgun/translations/hu.json @@ -6,11 +6,11 @@ "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." }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant programnak, be kell \u00e1ll\u00edtania a [Webhooks with Mailgun]({mailgun_url}) alkalmaz\u00e1st. \n\nT\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/json \n\nL\u00e1sd [a dokument\u00e1ci\u00f3]({docs_url}), hogyan konfigur\u00e1lhatja az automatizmusokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni Home Assistantba, be kell \u00e1ll\u00edtania a [Webhooks with Mailgun]({mailgun_url}) funkci\u00f3t. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 adatokat: \n\n - URL: `{webhook_url}`\n - Met\u00f3dus: POST\n - Tartalomt\u00edpus: alkalmaz\u00e1s/json \n\nB\u0151vebb inform\u00e1ci\u00f3 [a dokument\u00e1ci\u00f3ban]({docs_url}) olvashat\u00f3, hogyan konfigur\u00e1lhatja az automatizmusokat a be\u00e9rkez\u0151 adatok kezel\u00e9s\u00e9re." }, "step": { "user": { - "description": "Biztos benne, hogy be szeretn\u00e9 \u00e1ll\u00edtani a Mailgunt?", + "description": "Biztos benne, hogy be szeretn\u00e9 \u00e1ll\u00edtani Mailgunt?", "title": "Mailgun Webhook be\u00e1ll\u00edt\u00e1sa" } } diff --git a/homeassistant/components/mailgun/translations/tr.json b/homeassistant/components/mailgun/translations/tr.json index 6f7efc7d8b3..42edf684395 100644 --- a/homeassistant/components/mailgun/translations/tr.json +++ b/homeassistant/components/mailgun/translations/tr.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, "create_entry": { - "default": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in [Webhooks with Mailgun]( {mailgun_url} ) kurman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST\n - \u0130\u00e7erik T\u00fcr\u00fc: uygulama/json \n\n Gelen verileri i\u015flemek i\u00e7in otomasyonlar\u0131n nas\u0131l yap\u0131land\u0131r\u0131laca\u011f\u0131 hakk\u0131nda [belgelere]( {docs_url}" + "default": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in [Mailgun ile Webhook'lar]( {mailgun_url} ) kurman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST\n - \u0130\u00e7erik T\u00fcr\u00fc: uygulama/json \n\n Gelen verileri i\u015flemek i\u00e7in otomasyonlar\u0131n nas\u0131l yap\u0131land\u0131r\u0131laca\u011f\u0131na ili\u015fkin [belgelere]( {docs_url} ) bak\u0131n." }, "step": { "user": { diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index e3d7b275de7..01f59c34fd1 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -3,7 +3,7 @@ "name": "Matrix", "documentation": "https://www.home-assistant.io/integrations/matrix", "requirements": ["matrix-client==0.4.0"], - "codeowners": ["@tinloaf"], + "codeowners": [], "iot_class": "cloud_push", "loggers": ["matrix_client"] } diff --git a/homeassistant/components/matter/__init__.py b/homeassistant/components/matter/__init__.py index b1470ecc422..a0e4dcf7483 100644 --- a/homeassistant/components/matter/__init__.py +++ b/homeassistant/components/matter/__init__.py @@ -69,8 +69,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady( "Unknown error connecting to the Matter server" ) from err - else: - async_delete_issue(hass, DOMAIN, "invalid_server_version") + + async_delete_issue(hass, DOMAIN, "invalid_server_version") async def on_hass_stop(event: Event) -> None: """Handle incoming stop event from Home Assistant.""" diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index 08763e38327..9f8d1fe6c58 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -71,7 +71,7 @@ class MatterAdapter: bridge_unique_id: str | None = None if node.aggregator_device_type_instance is not None and ( - node.root_device_type_instance.get_cluster(all_clusters.Basic) + node.root_device_type_instance.get_cluster(all_clusters.BasicInformation) ): # create virtual (parent) device for bridge node device bridge_device = MatterBridgedNodeDevice( diff --git a/homeassistant/components/matter/diagnostics.py b/homeassistant/components/matter/diagnostics.py new file mode 100644 index 00000000000..77234ab48b5 --- /dev/null +++ b/homeassistant/components/matter/diagnostics.py @@ -0,0 +1,80 @@ +"""Provide diagnostics for Matter.""" +from __future__ import annotations + +from copy import deepcopy +from typing import Any + +from matter_server.common.helpers.util import dataclass_to_dict + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .const import DOMAIN, ID_TYPE_DEVICE_ID +from .helpers import get_device_id, get_matter + +ATTRIBUTES_TO_REDACT = {"chip.clusters.Objects.BasicInformation.Attributes.Location"} + + +def redact_matter_attributes(node_data: dict[str, Any]) -> dict[str, Any]: + """Redact Matter cluster attribute.""" + redacted = deepcopy(node_data) + for attribute_to_redact in ATTRIBUTES_TO_REDACT: + for value in redacted["attributes"].values(): + if value["attribute_type"] == attribute_to_redact: + value["value"] = REDACTED + + return redacted + + +def remove_serialization_type(data: dict[str, Any]) -> dict[str, Any]: + """Remove serialization type from data.""" + if "_type" in data: + data.pop("_type") + return data + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + matter = get_matter(hass) + server_diagnostics = await matter.matter_client.get_diagnostics() + data = remove_serialization_type(dataclass_to_dict(server_diagnostics)) + nodes = [redact_matter_attributes(node_data) for node_data in data["nodes"]] + data["nodes"] = nodes + + return {"server": data} + + +async def async_get_device_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry +) -> dict[str, Any]: + """Return diagnostics for a device.""" + matter = get_matter(hass) + device_id_type_prefix = f"{ID_TYPE_DEVICE_ID}_" + device_id_full = next( + identifier[1] + for identifier in device.identifiers + if identifier[0] == DOMAIN and identifier[1].startswith(device_id_type_prefix) + ) + device_id = device_id_full.lstrip(device_id_type_prefix) + + server_diagnostics = await matter.matter_client.get_diagnostics() + + node = next( + node + for node in await matter.matter_client.get_nodes() + for node_device in node.node_devices + if get_device_id(server_diagnostics.info, node_device) == device_id + ) + + return { + "server_info": remove_serialization_type( + dataclass_to_dict(server_diagnostics.info) + ), + "node": redact_matter_attributes( + remove_serialization_type(dataclass_to_dict(node)) + ), + } diff --git a/homeassistant/components/matter/manifest.json b/homeassistant/components/matter/manifest.json index 129110ba519..46fe45873b4 100644 --- a/homeassistant/components/matter/manifest.json +++ b/homeassistant/components/matter/manifest.json @@ -3,7 +3,7 @@ "name": "Matter (BETA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/matter", - "requirements": ["python-matter-server==1.0.8"], + "requirements": ["python-matter-server==2.0.2"], "dependencies": ["websocket_api"], "codeowners": ["@home-assistant/matter"], "iot_class": "local_push" diff --git a/homeassistant/components/matter/translations/hu.json b/homeassistant/components/matter/translations/hu.json index d343c5f41d9..93e2b2f1b53 100644 --- a/homeassistant/components/matter/translations/hu.json +++ b/homeassistant/components/matter/translations/hu.json @@ -17,8 +17,8 @@ }, "flow_title": "{name}", "progress": { - "install_addon": "K\u00e9rj\u00fck, v\u00e1rjon, am\u00edg a Matter Server b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat.", - "start_addon": "K\u00e9rj\u00fck, v\u00e1rjon, am\u00edg a Matter Server b\u0151v\u00edtm\u00e9ny elindul. Ez a kieg\u00e9sz\u00edt\u0151 az, ami a Matter-t m\u0171k\u00f6dteti a Home Assistant-ben. Ez eltarthat n\u00e9h\u00e1ny egy kis ideig." + "install_addon": "K\u00e9rem, v\u00e1rjon, am\u00edg a Matter Server b\u0151v\u00edtm\u00e9ny telep\u00edt\u00e9se befejez\u0151dik. Ez t\u00f6bb percig is eltarthat.", + "start_addon": "K\u00e9rem, v\u00e1rjon, am\u00edg a Matter Server b\u0151v\u00edtm\u00e9ny elindul. Ez a kieg\u00e9sz\u00edt\u0151 az, ami a Matter-t m\u0171k\u00f6dteti a Home Assistant-ben. Ez eltarthat n\u00e9h\u00e1ny egy kis ideig." }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/matter/translations/lt.json b/homeassistant/components/matter/translations/lt.json new file mode 100644 index 00000000000..c82e4b7b7e6 --- /dev/null +++ b/homeassistant/components/matter/translations/lt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u012erenginys jau sukonfig\u016bruotas" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/matter/translations/lv.json b/homeassistant/components/matter/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/matter/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/matter/translations/tr.json b/homeassistant/components/matter/translations/tr.json new file mode 100644 index 00000000000..78f744034cd --- /dev/null +++ b/homeassistant/components/matter/translations/tr.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "Matter Server eklenti ke\u015fif bilgisi al\u0131namad\u0131.", + "addon_info_failed": "Matter Server eklenti bilgisi al\u0131namad\u0131.", + "addon_install_failed": "Matter Server eklentisi y\u00fcklenemedi.", + "addon_start_failed": "Matter Server eklentisi ba\u015flat\u0131lamad\u0131.", + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "not_matter_addon": "Ke\u015ffedilen eklenti, resmi Matter Server eklentisi de\u011fildir.", + "reconfiguration_successful": "Matter entegrasyonu ba\u015far\u0131yla yeniden yap\u0131land\u0131r\u0131ld\u0131." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_server_version": "Matter sunucusu do\u011fru s\u00fcr\u00fcm de\u011fil", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "{name}", + "progress": { + "install_addon": "Matter Server eklenti kurulumu tamamlanana kadar l\u00fctfen bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir.", + "start_addon": "Matter Server eklentisi ba\u015flarken l\u00fctfen bekleyin. Bu eklenti, Home Assistant'ta Matter'a g\u00fc\u00e7 veren \u015feydir. Bu birka\u00e7 saniye s\u00fcrebilir." + }, + "step": { + "hassio_confirm": { + "title": "Matter Server eklentisi ile Matter entegrasyonunu kurun" + }, + "install_addon": { + "title": "Eklenti kurulumu ba\u015flad\u0131" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Resmi Matter Server Supervisor eklentisini kullan\u0131n" + }, + "description": "Resmi Matter Server Supervisor eklentisini kullanmak istiyor musunuz? \n\n Matter Server'\u0131 zaten ba\u015fka bir eklentide, \u00f6zel bir kapsay\u0131c\u0131da, yerel olarak vb. \u00e7al\u0131\u015ft\u0131r\u0131yorsan\u0131z, bu se\u00e7ene\u011fi se\u00e7meyin.", + "title": "Ba\u011flant\u0131 y\u00f6ntemini se\u00e7in" + }, + "start_addon": { + "title": "Eklenti ba\u015flat\u0131l\u0131yor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mazda/climate.py b/homeassistant/components/mazda/climate.py index e2028885c34..02c4e7ce923 100644 --- a/homeassistant/components/mazda/climate.py +++ b/homeassistant/components/mazda/climate.py @@ -20,7 +20,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.util.unit_conversion import TemperatureConverter from . import MazdaEntity from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_REGION, DOMAIN @@ -129,7 +129,7 @@ class MazdaClimateEntity(MazdaEntity, ClimateEntity): "interiorTemperatureCelsius" ] if self.data["hvacSetting"]["temperatureUnit"] == "F": - self._attr_current_temperature = convert_temperature( + self._attr_current_temperature = TemperatureConverter.convert( current_temperature_celsius, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT, diff --git a/homeassistant/components/mazda/sensor.py b/homeassistant/components/mazda/sensor.py index 7ae59572b30..e50292d773f 100644 --- a/homeassistant/components/mazda/sensor.py +++ b/homeassistant/components/mazda/sensor.py @@ -16,7 +16,6 @@ from homeassistant.const import PERCENTAGE, UnitOfLength, UnitOfPressure from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM, UnitSystem from . import MazdaEntity from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN @@ -27,7 +26,7 @@ class MazdaSensorRequiredKeysMixin: """Mixin for required keys.""" # Function to determine the value for this sensor, given the coordinator data and the configured unit system - value: Callable[[dict[str, Any], UnitSystem], StateType] + value: Callable[[dict[str, Any]], StateType] @dataclass @@ -39,17 +38,6 @@ class MazdaSensorEntityDescription( # Function to determine whether the vehicle supports this sensor, given the coordinator data is_supported: Callable[[dict[str, Any]], bool] = lambda data: True - # Function to determine the unit of measurement for this sensor, given the configured unit system - # Falls back to description.native_unit_of_measurement if it is not provided - unit: Callable[[UnitSystem], str | None] | None = None - - -def _get_distance_unit(unit_system: UnitSystem) -> str: - """Return the distance unit for the given unit system.""" - if unit_system is US_CUSTOMARY_SYSTEM: - return UnitOfLength.MILES - return UnitOfLength.KILOMETERS - def _fuel_remaining_percentage_supported(data): """Determine if fuel remaining percentage is supported.""" @@ -101,55 +89,45 @@ def _ev_remaining_range_supported(data): ) -def _fuel_distance_remaining_value(data, unit_system): +def _fuel_distance_remaining_value(data): """Get the fuel distance remaining value.""" - return round( - unit_system.length( - data["status"]["fuelDistanceRemainingKm"], UnitOfLength.KILOMETERS - ) - ) + return round(data["status"]["fuelDistanceRemainingKm"]) -def _odometer_value(data, unit_system): +def _odometer_value(data): """Get the odometer value.""" # In order to match the behavior of the Mazda mobile app, we always round down - return int( - unit_system.length(data["status"]["odometerKm"], UnitOfLength.KILOMETERS) - ) + return int(data["status"]["odometerKm"]) -def _front_left_tire_pressure_value(data, unit_system): +def _front_left_tire_pressure_value(data): """Get the front left tire pressure value.""" return round(data["status"]["tirePressure"]["frontLeftTirePressurePsi"]) -def _front_right_tire_pressure_value(data, unit_system): +def _front_right_tire_pressure_value(data): """Get the front right tire pressure value.""" return round(data["status"]["tirePressure"]["frontRightTirePressurePsi"]) -def _rear_left_tire_pressure_value(data, unit_system): +def _rear_left_tire_pressure_value(data): """Get the rear left tire pressure value.""" return round(data["status"]["tirePressure"]["rearLeftTirePressurePsi"]) -def _rear_right_tire_pressure_value(data, unit_system): +def _rear_right_tire_pressure_value(data): """Get the rear right tire pressure value.""" return round(data["status"]["tirePressure"]["rearRightTirePressurePsi"]) -def _ev_charge_level_value(data, unit_system): +def _ev_charge_level_value(data): """Get the charge level value.""" return round(data["evStatus"]["chargeInfo"]["batteryLevelPercentage"]) -def _ev_remaining_range_value(data, unit_system): +def _ev_remaining_range_value(data): """Get the remaining range value.""" - return round( - unit_system.length( - data["evStatus"]["chargeInfo"]["drivingRangeKm"], UnitOfLength.KILOMETERS - ) - ) + return round(data["evStatus"]["chargeInfo"]["drivingRangeKm"]) SENSOR_ENTITIES = [ @@ -160,13 +138,14 @@ SENSOR_ENTITIES = [ native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, is_supported=_fuel_remaining_percentage_supported, - value=lambda data, unit_system: data["status"]["fuelRemainingPercent"], + value=lambda data: data["status"]["fuelRemainingPercent"], ), MazdaSensorEntityDescription( key="fuel_distance_remaining", name="Fuel distance remaining", icon="mdi:gas-station", - unit=_get_distance_unit, + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.KILOMETERS, state_class=SensorStateClass.MEASUREMENT, is_supported=_fuel_distance_remaining_supported, value=_fuel_distance_remaining_value, @@ -175,7 +154,8 @@ SENSOR_ENTITIES = [ key="odometer", name="Odometer", icon="mdi:speedometer", - unit=_get_distance_unit, + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.KILOMETERS, state_class=SensorStateClass.TOTAL_INCREASING, is_supported=lambda data: data["status"]["odometerKm"] is not None, value=_odometer_value, @@ -233,7 +213,8 @@ SENSOR_ENTITIES = [ key="ev_remaining_range", name="Remaining range", icon="mdi:ev-station", - unit=_get_distance_unit, + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.KILOMETERS, state_class=SensorStateClass.MEASUREMENT, is_supported=_ev_remaining_range_supported, value=_ev_remaining_range_value, @@ -275,13 +256,6 @@ class MazdaSensorEntity(MazdaEntity, SensorEntity): self._attr_unique_id = f"{self.vin}_{description.key}" @property - def native_unit_of_measurement(self): - """Return the unit of measurement for the sensor, according to the configured unit system.""" - if unit_fn := self.entity_description.unit: - return unit_fn(self.hass.config.units) - return self.entity_description.native_unit_of_measurement - - @property - def native_value(self): + def native_value(self) -> StateType: """Return the state of the sensor.""" - return self.entity_description.value(self.data, self.hass.config.units) + return self.entity_description.value(self.data) diff --git a/homeassistant/components/mazda/translations/uk.json b/homeassistant/components/mazda/translations/uk.json new file mode 100644 index 00000000000..60e922897a5 --- /dev/null +++ b/homeassistant/components/mazda/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0430\u0434\u0440\u0435\u0441\u0443 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c, \u044f\u043a\u0456 \u0432\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u0435 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u0443 \u0432 \u043c\u043e\u0431\u0456\u043b\u044c\u043d\u0438\u0439 \u0434\u043e\u0434\u0430\u0442\u043e\u043a MyMazda." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/strings.json b/homeassistant/components/meater/strings.json index 635c71d324c..7f4a97a5b19 100644 --- a/homeassistant/components/meater/strings.json +++ b/homeassistant/components/meater/strings.json @@ -18,6 +18,9 @@ } } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown_auth_error": "[%key:common::config_flow::error::unknown%]", diff --git a/homeassistant/components/meater/translations/bg.json b/homeassistant/components/meater/translations/bg.json index cb1a84abf51..d343304dff4 100644 --- a/homeassistant/components/meater/translations/bg.json +++ b/homeassistant/components/meater/translations/bg.json @@ -1,5 +1,8 @@ { "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": { "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", "unknown_auth_error": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" diff --git a/homeassistant/components/meater/translations/ca.json b/homeassistant/components/meater/translations/ca.json index cd2c626ef8a..f4317b2d198 100644 --- a/homeassistant/components/meater/translations/ca.json +++ b/homeassistant/components/meater/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat" + }, "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "service_unavailable_error": "L'API no est\u00e0 disponible actualment, torna-ho a provar m\u00e9s tard.", diff --git a/homeassistant/components/meater/translations/de.json b/homeassistant/components/meater/translations/de.json index 9e85c5e56f8..80198e111e8 100644 --- a/homeassistant/components/meater/translations/de.json +++ b/homeassistant/components/meater/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, "error": { "invalid_auth": "Ung\u00fcltige Authentifizierung", "service_unavailable_error": "Die API ist derzeit nicht verf\u00fcgbar. Bitte versuche es sp\u00e4ter erneut.", diff --git a/homeassistant/components/meater/translations/en.json b/homeassistant/components/meater/translations/en.json index 707c6dc6ed6..a49d241607d 100644 --- a/homeassistant/components/meater/translations/en.json +++ b/homeassistant/components/meater/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Account is already configured" + }, "error": { "invalid_auth": "Invalid authentication", "service_unavailable_error": "The API is currently unavailable, please try again later.", diff --git a/homeassistant/components/meater/translations/et.json b/homeassistant/components/meater/translations/et.json index dc5f6f9102f..dbc2b465bb9 100644 --- a/homeassistant/components/meater/translations/et.json +++ b/homeassistant/components/meater/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud" + }, "error": { "invalid_auth": "Tuvastamine nurjus", "service_unavailable_error": "API pole praegu saadaval, proovi hiljem uuesti.", diff --git a/homeassistant/components/meater/translations/no.json b/homeassistant/components/meater/translations/no.json index f97f8ce5f8f..040f6dd19d1 100644 --- a/homeassistant/components/meater/translations/no.json +++ b/homeassistant/components/meater/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, "error": { "invalid_auth": "Ugyldig godkjenning", "service_unavailable_error": "API-en er for \u00f8yeblikket utilgjengelig, pr\u00f8v igjen senere.", diff --git a/homeassistant/components/meater/translations/ru.json b/homeassistant/components/meater/translations/ru.json index 182e2d03e33..a02db6e0b9f 100644 --- a/homeassistant/components/meater/translations/ru.json +++ b/homeassistant/components/meater/translations/ru.json @@ -1,5 +1,8 @@ { "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": { "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", "service_unavailable_error": "\u0412 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f API \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", diff --git a/homeassistant/components/meater/translations/uk.json b/homeassistant/components/meater/translations/uk.json new file mode 100644 index 00000000000..e6fa4937968 --- /dev/null +++ b/homeassistant/components/meater/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u044c\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Meater Cloud {username}." + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "data_description": { + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 Meater Cloud, \u044f\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u043e, \u0430\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meater/translations/zh-Hant.json b/homeassistant/components/meater/translations/zh-Hant.json index 1033f5c993a..01175192a4e 100644 --- a/homeassistant/components/meater/translations/zh-Hant.json +++ b/homeassistant/components/meater/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "service_unavailable_error": "API \u76ee\u524d\u7121\u6cd5\u4f7f\u7528\uff0c\u8acb\u7a0d\u5019\u518d\u8a66\u3002", diff --git a/homeassistant/components/media_extractor/__init__.py b/homeassistant/components/media_extractor/__init__.py index 081375e8e08..a0c542d72a5 100644 --- a/homeassistant/components/media_extractor/__init__.py +++ b/homeassistant/components/media_extractor/__init__.py @@ -140,18 +140,16 @@ class MediaExtractor: except MEQueryException: _LOGGER.error("Wrong query format: %s", stream_query) return - else: - data = {k: v for k, v in self.call_data.items() if k != ATTR_ENTITY_ID} - data[ATTR_MEDIA_CONTENT_ID] = stream_url - if entity_id: - data[ATTR_ENTITY_ID] = entity_id + data = {k: v for k, v in self.call_data.items() if k != ATTR_ENTITY_ID} + data[ATTR_MEDIA_CONTENT_ID] = stream_url - self.hass.async_create_task( - self.hass.services.async_call( - MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, data - ) - ) + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + self.hass.async_create_task( + self.hass.services.async_call(MEDIA_PLAYER_DOMAIN, SERVICE_PLAY_MEDIA, data) + ) def get_stream_query_for_entity(self, entity_id): """Get stream format query for entity.""" diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index fa2c5465443..341c9468d11 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -1201,7 +1201,8 @@ async def websocket_browse_media( """ Browse media available to the media_player entity. - To use, media_player integrations can implement MediaPlayerEntity.async_browse_media() + To use, media_player integrations can implement + MediaPlayerEntity.async_browse_media() """ component: EntityComponent[MediaPlayerEntity] = hass.data[DOMAIN] player = component.get_entity(msg["entity_id"]) diff --git a/homeassistant/components/media_player/browse_media.py b/homeassistant/components/media_player/browse_media.py index 81ded203e75..d1328a851d2 100644 --- a/homeassistant/components/media_player/browse_media.py +++ b/homeassistant/components/media_player/browse_media.py @@ -51,8 +51,9 @@ def async_process_play_media_url( "Not signing path for content with query param" ) elif parsed.path.startswith(PATHS_WITHOUT_AUTH): - # We don't sign this path if it doesn't need auth. Although signing itself can't hurt, - # some devices are unable to handle long URLs and the auth signature might push it over. + # We don't sign this path if it doesn't need auth. Although signing itself can't + # hurt, some devices are unable to handle long URLs and the auth signature might + # push it over. pass else: signed_path = async_sign_path( diff --git a/homeassistant/components/media_player/translations/lt.json b/homeassistant/components/media_player/translations/lt.json index b9ad676cc08..db65add77ab 100644 --- a/homeassistant/components/media_player/translations/lt.json +++ b/homeassistant/components/media_player/translations/lt.json @@ -1,6 +1,7 @@ { "state": { "_": { + "idle": "Laukiama", "on": "\u012ejungta" } } diff --git a/homeassistant/components/media_player/translations/uk.json b/homeassistant/components/media_player/translations/uk.json index 21c7f2897a3..04d6b0e8bb3 100644 --- a/homeassistant/components/media_player/translations/uk.json +++ b/homeassistant/components/media_player/translations/uk.json @@ -6,6 +6,9 @@ "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", "is_paused": "{entity_name} \u043d\u0430 \u043f\u0430\u0443\u0437\u0456", "is_playing": "{entity_name} \u0432\u0456\u0434\u0442\u0432\u043e\u0440\u044e\u0454 \u043c\u0435\u0434\u0456\u0430" + }, + "trigger_type": { + "idle": "{entity_name} \u0441\u0442\u0430\u0454 \u043d\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u0438\u043c" } }, "state": { diff --git a/homeassistant/components/melcloud/translations/lt.json b/homeassistant/components/melcloud/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/melcloud/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/__init__.py b/homeassistant/components/melnor/__init__.py index 93b8d11ab24..3cd9fee4fe7 100644 --- a/homeassistant/components/melnor/__init__.py +++ b/homeassistant/components/melnor/__init__.py @@ -60,7 +60,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_config_entry_first_refresh() hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/melnor/manifest.json b/homeassistant/components/melnor/manifest.json index c57549aa647..45d7f82c55b 100644 --- a/homeassistant/components/melnor/manifest.json +++ b/homeassistant/components/melnor/manifest.json @@ -1,11 +1,11 @@ { - "after_dependencies": ["bluetooth"], "bluetooth": [ { "manufacturer_data_start": [89], "manufacturer_id": 13 } ], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@vanstinator"], "config_flow": true, "domain": "melnor", diff --git a/homeassistant/components/melnor/strings.json b/homeassistant/components/melnor/strings.json index 42309c3bf72..2fefa32b6bc 100644 --- a/homeassistant/components/melnor/strings.json +++ b/homeassistant/components/melnor/strings.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "no_devices_found": "There aren't any Melnor Bluetooth devices nearby." }, "step": { diff --git a/homeassistant/components/melnor/translations/bg.json b/homeassistant/components/melnor/translations/bg.json new file mode 100644 index 00000000000..37b6f40c82e --- /dev/null +++ b/homeassistant/components/melnor/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melnor/translations/ca.json b/homeassistant/components/melnor/translations/ca.json index 3aa06dfddc6..0f6cb27b6f1 100644 --- a/homeassistant/components/melnor/translations/ca.json +++ b/homeassistant/components/melnor/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", "no_devices_found": "No hi ha cap dispositiu Melnor Bluetooth a prop." }, "step": { diff --git a/homeassistant/components/melnor/translations/de.json b/homeassistant/components/melnor/translations/de.json index 5792062dd87..94d5822847d 100644 --- a/homeassistant/components/melnor/translations/de.json +++ b/homeassistant/components/melnor/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "no_devices_found": "In der N\u00e4he gibt es keine Melnor Bluetooth-Ger\u00e4te." }, "step": { diff --git a/homeassistant/components/melnor/translations/en.json b/homeassistant/components/melnor/translations/en.json index c179e46a070..57a756c1ef9 100644 --- a/homeassistant/components/melnor/translations/en.json +++ b/homeassistant/components/melnor/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Device is already configured", "no_devices_found": "There aren't any Melnor Bluetooth devices nearby." }, "step": { diff --git a/homeassistant/components/melnor/translations/et.json b/homeassistant/components/melnor/translations/et.json index 12a75835d26..6fb9a498ea8 100644 --- a/homeassistant/components/melnor/translations/et.json +++ b/homeassistant/components/melnor/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "no_devices_found": "L\u00e4heduses pole \u00fchtegi Melnor Bluetooth-seadet." }, "step": { diff --git a/homeassistant/components/melnor/translations/no.json b/homeassistant/components/melnor/translations/no.json index 98c873ad9fe..14c341a83e1 100644 --- a/homeassistant/components/melnor/translations/no.json +++ b/homeassistant/components/melnor/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", "no_devices_found": "Det er ingen Melnor Bluetooth-enheter i n\u00e6rheten." }, "step": { diff --git a/homeassistant/components/melnor/translations/ru.json b/homeassistant/components/melnor/translations/ru.json index b6e0488ce38..6dbcac424d7 100644 --- a/homeassistant/components/melnor/translations/ru.json +++ b/homeassistant/components/melnor/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "no_devices_found": "\u041f\u043e\u0431\u043b\u0438\u0437\u043e\u0441\u0442\u0438 \u043d\u0435\u0442 \u043d\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Melnor Bluetooth." }, "step": { diff --git a/homeassistant/components/melnor/translations/zh-Hant.json b/homeassistant/components/melnor/translations/zh-Hant.json index 0374906bb4b..f82926dc5fd 100644 --- a/homeassistant/components/melnor/translations/zh-Hant.json +++ b/homeassistant/components/melnor/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_devices_found": "\u9644\u8fd1\u6c92\u6709\u4efb\u4f55 Melnor \u85cd\u7259\u88dd\u7f6e\u3002" }, "step": { diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 22a9b195d2d..c87aea05260 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, TypeVar, Union +from typing import Any, TypeVar from meteofrance_api.helpers import ( get_warning_text_status_from_indice_color, @@ -49,7 +49,7 @@ from .const import ( MODEL, ) -_DataT = TypeVar("_DataT", bound=Union[Rain, Forecast, CurrentPhenomenons]) +_DataT = TypeVar("_DataT", bound=Rain | Forecast | CurrentPhenomenons) @dataclass diff --git a/homeassistant/components/meteoclimatic/translations/lv.json b/homeassistant/components/meteoclimatic/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/meteoclimatic/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/metoffice/helpers.py b/homeassistant/components/metoffice/helpers.py index ecef7e5ddcb..cdd506790ef 100644 --- a/homeassistant/components/metoffice/helpers.py +++ b/homeassistant/components/metoffice/helpers.py @@ -35,18 +35,18 @@ def fetch_data(connection: datapoint.Manager, site: Site, mode: str) -> MetOffic except (ValueError, datapoint.exceptions.APIException) as err: _LOGGER.error("Check Met Office connection: %s", err.args) raise UpdateFailed from err - else: - time_now = utcnow() - return MetOfficeData( - now=forecast.now(), - forecast=[ - timestep - for day in forecast.days - for timestep in day.timesteps - if timestep.date > time_now - and ( - mode == MODE_3HOURLY or timestep.date.hour > 6 - ) # ensures only one result per day in MODE_DAILY - ], - site=site, - ) + + time_now = utcnow() + return MetOfficeData( + now=forecast.now(), + forecast=[ + timestep + for day in forecast.days + for timestep in day.timesteps + if timestep.date > time_now + and ( + mode == MODE_3HOURLY or timestep.date.hour > 6 + ) # ensures only one result per day in MODE_DAILY + ], + site=site, + ) diff --git a/homeassistant/components/mfi/sensor.py b/homeassistant/components/mfi/sensor.py index 38f6d973b9d..84f4abdaead 100644 --- a/homeassistant/components/mfi/sensor.py +++ b/homeassistant/components/mfi/sensor.py @@ -133,14 +133,14 @@ class MfiSensor(SensorEntity): try: tag = self._port.tag except ValueError: - return "State" + return None if tag == "temperature": return UnitOfTemperature.CELSIUS if tag == "active_pwr": return "Watts" if self._port.model == "Input Digital": - return "State" + return None return tag def update(self) -> None: diff --git a/homeassistant/components/mijndomein_energie/__init__.py b/homeassistant/components/mijndomein_energie/__init__.py new file mode 100644 index 00000000000..a7b649c7c81 --- /dev/null +++ b/homeassistant/components/mijndomein_energie/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Mijndomein Energie.""" diff --git a/homeassistant/components/mijndomein_energie/manifest.json b/homeassistant/components/mijndomein_energie/manifest.json new file mode 100644 index 00000000000..970d333eae9 --- /dev/null +++ b/homeassistant/components/mijndomein_energie/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "mijndomein_energie", + "name": "Mijndomein Energie", + "integration_type": "virtual", + "supported_by": "energyzero" +} diff --git a/homeassistant/components/mikrotik/translations/lt.json b/homeassistant/components/mikrotik/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/mikrotik/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/lv.json b/homeassistant/components/mikrotik/translations/lv.json index d4fa954b407..2f962691003 100644 --- a/homeassistant/components/mikrotik/translations/lv.json +++ b/homeassistant/components/mikrotik/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/mikrotik/translations/uk.json b/homeassistant/components/mikrotik/translations/uk.json index b44d5979d13..53cca3884e4 100644 --- a/homeassistant/components/mikrotik/translations/uk.json +++ b/homeassistant/components/mikrotik/translations/uk.json @@ -9,6 +9,12 @@ "name_exists": "\u0426\u044f \u043d\u0430\u0437\u0432\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username} \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439." + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/mill/sensor.py b/homeassistant/components/mill/sensor.py index 776ba1dc632..50db999186f 100644 --- a/homeassistant/components/mill/sensor.py +++ b/homeassistant/components/mill/sensor.py @@ -86,6 +86,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.CO2, native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, name="Estimated CO2", + state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( key=TVOC, diff --git a/homeassistant/components/mill/translations/uk.json b/homeassistant/components/mill/translations/uk.json index ceee910d966..e66dac9b6f2 100644 --- a/homeassistant/components/mill/translations/uk.json +++ b/homeassistant/components/mill/translations/uk.json @@ -5,6 +5,13 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "cloud": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/min_max/config_flow.py b/homeassistant/components/min_max/config_flow.py index b515608042f..f449c98819e 100644 --- a/homeassistant/components/min_max/config_flow.py +++ b/homeassistant/components/min_max/config_flow.py @@ -19,13 +19,13 @@ from homeassistant.helpers.schema_config_entry_flow import ( from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN _STATISTIC_MEASURES = [ - selector.SelectOptionDict(value="min", label="Minimum"), - selector.SelectOptionDict(value="max", label="Maximum"), - selector.SelectOptionDict(value="mean", label="Arithmetic mean"), - selector.SelectOptionDict(value="median", label="Median"), - selector.SelectOptionDict(value="last", label="Most recently updated"), - selector.SelectOptionDict(value="range", label="Statistical range"), - selector.SelectOptionDict(value="sum", label="Sum"), + "min", + "max", + "mean", + "median", + "last", + "range", + "sum", ] @@ -38,7 +38,9 @@ OPTIONS_SCHEMA = vol.Schema( ), ), vol.Required(CONF_TYPE): selector.SelectSelector( - selector.SelectSelectorConfig(options=_STATISTIC_MEASURES), + selector.SelectSelectorConfig( + options=_STATISTIC_MEASURES, translation_key=CONF_TYPE + ), ), vol.Required(CONF_ROUND_DIGITS, default=2): selector.NumberSelector( selector.NumberSelectorConfig( diff --git a/homeassistant/components/min_max/strings.json b/homeassistant/components/min_max/strings.json index 67e8416bc2c..c76a6faf2f5 100644 --- a/homeassistant/components/min_max/strings.json +++ b/homeassistant/components/min_max/strings.json @@ -30,5 +30,18 @@ } } } + }, + "selector": { + "type": { + "options": { + "min": "Minimum", + "max": "Maximum", + "mean": "Arithmetic mean", + "median": "Median", + "last": "Most recently updated", + "range": "Statistical range", + "sum": "Sum" + } + } } } diff --git a/homeassistant/components/min_max/translations/ca.json b/homeassistant/components/min_max/translations/ca.json index b5218d0265b..d6d3a22bdbe 100644 --- a/homeassistant/components/min_max/translations/ca.json +++ b/homeassistant/components/min_max/translations/ca.json @@ -30,5 +30,18 @@ } } }, + "selector": { + "type": { + "options": { + "last": "Actualitzat m\u00e9s recentment", + "max": "M\u00e0xim", + "mean": "Mitjana aritm\u00e8tica", + "median": "Mitjana", + "min": "M\u00ednim", + "range": "Rang estad\u00edstic", + "sum": "Suma" + } + } + }, "title": "Combina l'estat de diversos sensors" } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/de.json b/homeassistant/components/min_max/translations/de.json index 81003aed00e..c963863d73f 100644 --- a/homeassistant/components/min_max/translations/de.json +++ b/homeassistant/components/min_max/translations/de.json @@ -30,5 +30,18 @@ } } }, + "selector": { + "type": { + "options": { + "last": "Zuletzt aktualisiert", + "max": "Maximum", + "mean": "Arithmetisches Mittel", + "median": "Median", + "min": "Minimum", + "range": "Statistischer Bereich", + "sum": "Summe" + } + } + }, "title": "Kombiniere den Zustand mehrerer Sensoren" } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/en.json b/homeassistant/components/min_max/translations/en.json index 6c661c980c0..127799d032c 100644 --- a/homeassistant/components/min_max/translations/en.json +++ b/homeassistant/components/min_max/translations/en.json @@ -30,5 +30,18 @@ } } }, + "selector": { + "type": { + "options": { + "last": "Most recently updated", + "max": "Maximum", + "mean": "Arithmetic mean", + "median": "Median", + "min": "Minimum", + "range": "Statistical range", + "sum": "Sum" + } + } + }, "title": "Combine the state of several sensors" } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/et.json b/homeassistant/components/min_max/translations/et.json index e0b9e561aaf..d462f90d36d 100644 --- a/homeassistant/components/min_max/translations/et.json +++ b/homeassistant/components/min_max/translations/et.json @@ -30,5 +30,18 @@ } } }, + "selector": { + "type": { + "options": { + "last": "Viimati uuendatud", + "max": "Maksimaalne", + "mean": "Aritmeetiline keskmine", + "median": "Mediaan", + "min": "Minimaalne", + "range": "Statistiline vahemik", + "sum": "Summa" + } + } + }, "title": "\u00dchenda mitme anduri olek" } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/nl.json b/homeassistant/components/min_max/translations/nl.json index 84b033d8715..0911bcaddc1 100644 --- a/homeassistant/components/min_max/translations/nl.json +++ b/homeassistant/components/min_max/translations/nl.json @@ -6,7 +6,7 @@ "entity_ids": "Invoerentiteiten", "name": "Naam", "round_digits": "Precisie", - "type": "statistisch kenmerk" + "type": "Statistisch kenmerk" }, "data_description": { "round_digits": "Regelt het aantal decimale cijfers in de uitvoer wanneer de statistische eigenschap gemiddelde of mediaan is." @@ -22,7 +22,7 @@ "data": { "entity_ids": "Invoerentiteiten", "round_digits": "Precisie", - "type": "statistisch kenmerk" + "type": "Statistisch kenmerk" }, "data_description": { "round_digits": "Regelt het aantal decimale cijfers in de uitvoer wanneer de statistische eigenschap gemiddelde of mediaan is." diff --git a/homeassistant/components/min_max/translations/pl.json b/homeassistant/components/min_max/translations/pl.json index 40b8cecd421..4dc8f529e3a 100644 --- a/homeassistant/components/min_max/translations/pl.json +++ b/homeassistant/components/min_max/translations/pl.json @@ -30,5 +30,18 @@ } } }, + "selector": { + "type": { + "options": { + "last": "ostatnio zaktualizowane", + "max": "maksimum", + "mean": "\u015brednia arytmetyczna", + "median": "mediana", + "min": "minimum", + "range": "zakres statystyczny", + "sum": "suma" + } + } + }, "title": "Po\u0142\u0105czenie stanu kilku sensor\u00f3w" } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/ru.json b/homeassistant/components/min_max/translations/ru.json index dab8747e1ae..e81661c5844 100644 --- a/homeassistant/components/min_max/translations/ru.json +++ b/homeassistant/components/min_max/translations/ru.json @@ -12,7 +12,7 @@ "round_digits": "\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u0442 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0437\u043d\u0430\u043a\u043e\u0432 \u043f\u043e\u0441\u043b\u0435 \u0437\u0430\u043f\u044f\u0442\u043e\u0439, \u043a\u043e\u0433\u0434\u0430 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043a\u0430 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0441\u0440\u0435\u0434\u043d\u0435\u0439, \u043c\u0435\u0434\u0438\u0430\u043d\u043d\u043e\u0439 \u0438\u043b\u0438 \u0441\u0443\u043c\u043c\u043e\u0439." }, "description": "\u0412\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442 \u0441\u0443\u043c\u043c\u0443, \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435, \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435, \u0441\u0440\u0435\u0434\u043d\u0435\u0435 \u0438\u043b\u0438 \u043c\u0435\u0434\u0438\u0430\u043d\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430 \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432.", - "title": "\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432" + "title": "\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432" } } }, @@ -30,5 +30,18 @@ } } }, - "title": "\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u0438\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432" + "selector": { + "type": { + "options": { + "last": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435", + "max": "\u041c\u0430\u043a\u0441\u0438\u043c\u0443\u043c", + "mean": "\u0421\u0440\u0435\u0434\u043d\u0435\u0435 \u0430\u0440\u0438\u0444\u043c\u0435\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435", + "median": "\u041c\u0435\u0434\u0438\u0430\u043d\u0430", + "min": "\u041c\u0438\u043d\u0438\u043c\u0443\u043c", + "range": "\u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d", + "sum": "\u0421\u0443\u043c\u043c\u0430" + } + } + }, + "title": "\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0439 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432" } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/tr.json b/homeassistant/components/min_max/translations/tr.json index ab826ed913f..f216dff824e 100644 --- a/homeassistant/components/min_max/translations/tr.json +++ b/homeassistant/components/min_max/translations/tr.json @@ -9,10 +9,10 @@ "type": "\u0130statistik \u00f6zelli\u011fi" }, "data_description": { - "round_digits": "\u0130statistik \u00f6zelli\u011fi ortalama veya medyan oldu\u011funda \u00e7\u0131kt\u0131daki ondal\u0131k basamak say\u0131s\u0131n\u0131 kontrol eder." + "round_digits": "\u0130statistik \u00f6zelli\u011fi ortalama, medyan veya toplam oldu\u011funda \u00e7\u0131kt\u0131daki ondal\u0131k basamak say\u0131s\u0131n\u0131 kontrol eder." }, - "description": "Giri\u015f sens\u00f6rleri listesinden minimum, maksimum, ortalama veya medyan de\u011feri hesaplayan bir sens\u00f6r olu\u015fturun.", - "title": "Min / maks / ortalama / medyan sens\u00f6r\u00fc ekle" + "description": "Giri\u015f sens\u00f6rleri listesinden minimum, maksimum, ortalama, medyan veya toplam\u0131 hesaplayan bir sens\u00f6r olu\u015fturun.", + "title": "Birka\u00e7 sens\u00f6r\u00fcn durumunu birle\u015ftirin" } } }, @@ -25,10 +25,10 @@ "type": "\u0130statistik \u00f6zelli\u011fi" }, "data_description": { - "round_digits": "\u0130statistik \u00f6zelli\u011fi ortalama veya medyan oldu\u011funda \u00e7\u0131kt\u0131daki ondal\u0131k basamak say\u0131s\u0131n\u0131 kontrol eder." + "round_digits": "\u0130statistik \u00f6zelli\u011fi ortalama, medyan veya toplam oldu\u011funda \u00e7\u0131kt\u0131daki ondal\u0131k basamak say\u0131s\u0131n\u0131 kontrol eder." } } } }, - "title": "Min / maks / ortalama / medyan sens\u00f6r" + "title": "Birka\u00e7 sens\u00f6r\u00fcn durumunu birle\u015ftirin" } \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/uk.json b/homeassistant/components/min_max/translations/uk.json new file mode 100644 index 00000000000..fe3fc997183 --- /dev/null +++ b/homeassistant/components/min_max/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/min_max/translations/zh-Hant.json b/homeassistant/components/min_max/translations/zh-Hant.json index 628e2458c65..a2f1ff12b32 100644 --- a/homeassistant/components/min_max/translations/zh-Hant.json +++ b/homeassistant/components/min_max/translations/zh-Hant.json @@ -30,5 +30,18 @@ } } }, + "selector": { + "type": { + "options": { + "last": "\u6700\u8fd1\u66f4\u65b0", + "max": "\u6700\u5927\u503c", + "mean": "\u7b97\u8853\u5e73\u5747\u503c", + "median": "\u4e2d\u9593\u503c", + "min": "\u6700\u5c0f\u503c", + "range": "\u7d71\u8a08\u7bc4\u570d", + "sum": "\u7e3d\u548c" + } + } + }, "title": "\u7d50\u5408\u591a\u500b\u611f\u6e2c\u5668\u72c0\u614b" } \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/const.py b/homeassistant/components/minecraft_server/const.py index ab5d67dc426..8fe7c9b2791 100644 --- a/homeassistant/components/minecraft_server/const.py +++ b/homeassistant/components/minecraft_server/const.py @@ -36,6 +36,3 @@ SRV_RECORD_PREFIX = "_minecraft._tcp" UNIT_PLAYERS_MAX = "players" UNIT_PLAYERS_ONLINE = "players" -UNIT_PROTOCOL_VERSION = None -UNIT_VERSION = None -UNIT_MOTD = None diff --git a/homeassistant/components/minecraft_server/sensor.py b/homeassistant/components/minecraft_server/sensor.py index 36d78520565..2499dd8b75b 100644 --- a/homeassistant/components/minecraft_server/sensor.py +++ b/homeassistant/components/minecraft_server/sensor.py @@ -23,11 +23,8 @@ from .const import ( NAME_PLAYERS_ONLINE, NAME_PROTOCOL_VERSION, NAME_VERSION, - UNIT_MOTD, UNIT_PLAYERS_MAX, UNIT_PLAYERS_ONLINE, - UNIT_PROTOCOL_VERSION, - UNIT_VERSION, ) @@ -61,7 +58,7 @@ class MinecraftServerSensorEntity(MinecraftServerEntity, SensorEntity): server: MinecraftServer, type_name: str, icon: str, - unit: str | None, + unit: str | None = None, device_class: str | None = None, ) -> None: """Initialize sensor base entity.""" @@ -79,9 +76,7 @@ class MinecraftServerVersionSensor(MinecraftServerSensorEntity): def __init__(self, server: MinecraftServer) -> None: """Initialize version sensor.""" - super().__init__( - server=server, type_name=NAME_VERSION, icon=ICON_VERSION, unit=UNIT_VERSION - ) + super().__init__(server=server, type_name=NAME_VERSION, icon=ICON_VERSION) async def async_update(self) -> None: """Update version.""" @@ -97,7 +92,6 @@ class MinecraftServerProtocolVersionSensor(MinecraftServerSensorEntity): server=server, type_name=NAME_PROTOCOL_VERSION, icon=ICON_PROTOCOL_VERSION, - unit=UNIT_PROTOCOL_VERSION, ) async def async_update(self) -> None: @@ -173,7 +167,6 @@ class MinecraftServerMOTDSensor(MinecraftServerSensorEntity): server=server, type_name=NAME_MOTD, icon=ICON_MOTD, - unit=UNIT_MOTD, ) async def async_update(self) -> None: diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py index 75a8d003aeb..379ff41ae51 100644 --- a/homeassistant/components/minio/minio_helper.py +++ b/homeassistant/components/minio/minio_helper.py @@ -44,7 +44,7 @@ def get_minio_notification_response( ): """Start listening to minio events. Copied from minio-py.""" query = {"prefix": prefix, "suffix": suffix, "events": events} - # pylint: disable=protected-access + # pylint: disable-next=protected-access return minio_client._url_open( "GET", bucket_name=bucket_name, query=query, preload_content=False ) @@ -159,7 +159,7 @@ class MinioEventThread(threading.Thread): presigned_url = minio_client.presigned_get_object(bucket, key) # Fail gracefully. If for whatever reason this stops working, # it shouldn't prevent it from firing events. - # pylint: disable=broad-except + # pylint: disable-next=broad-except except Exception as error: _LOGGER.error("Failed to generate presigned url: %s", error) diff --git a/homeassistant/components/mjpeg/translations/lv.json b/homeassistant/components/mjpeg/translations/lv.json new file mode 100644 index 00000000000..958428b5323 --- /dev/null +++ b/homeassistant/components/mjpeg/translations/lv.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + }, + "options": { + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mjpeg/translations/uk.json b/homeassistant/components/mjpeg/translations/uk.json new file mode 100644 index 00000000000..ce308f6cb89 --- /dev/null +++ b/homeassistant/components/mjpeg/translations/uk.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044c", + "invalid_auth": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "step": { + "user": { + "data": { + "mjpeg_url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 MJPEG", + "name": "\u0406\u043c'\u044f", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "still_image_url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u044f\u0442\u0438 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL" + } + } + } + }, + "options": { + "error": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "step": { + "init": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/manifest.json b/homeassistant/components/moat/manifest.json index f8612cc992f..3a69a9d2cf1 100644 --- a/homeassistant/components/moat/manifest.json +++ b/homeassistant/components/moat/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/moat", "bluetooth": [{ "local_name": "Moat_S*", "connectable": false }], "requirements": ["moat-ble==0.1.1"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" } diff --git a/homeassistant/components/moat/sensor.py b/homeassistant/components/moat/sensor.py index fc83d1fe757..f75717fad40 100644 --- a/homeassistant/components/moat/sensor.py +++ b/homeassistant/components/moat/sensor.py @@ -1,8 +1,6 @@ """Support for moat ble sensors.""" from __future__ import annotations -from typing import Optional, Union - from moat_ble import DeviceClass, DeviceKey, SensorUpdate, Units from homeassistant import config_entries @@ -122,9 +120,7 @@ async def async_setup_entry( class MoatBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a moat ble sensor.""" diff --git a/homeassistant/components/moat/translations/lv.json b/homeassistant/components/moat/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/moat/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moat/translations/tr.json b/homeassistant/components/moat/translations/tr.json index f63cee3493c..66d94aa9414 100644 --- a/homeassistant/components/moat/translations/tr.json +++ b/homeassistant/components/moat/translations/tr.json @@ -8,13 +8,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index eb0bf100aee..d7afb7b9998 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/mobile_app", "requirements": ["PyNaCl==1.5.0"], "dependencies": ["http", "webhook", "person", "tag", "websocket_api"], - "after_dependencies": ["cloud", "camera", "notify"], + "after_dependencies": ["cloud", "camera", "conversation", "notify"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index 0d2c05df518..fc325b1b6e9 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -1,6 +1,7 @@ """Sensor platform for mobile_app.""" from __future__ import annotations +from datetime import date, datetime from typing import Any from homeassistant.components.sensor import RestoreSensor, SensorDeviceClass @@ -10,6 +11,7 @@ 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 homeassistant.helpers.typing import StateType from homeassistant.util import dt as dt_util from .const import ( @@ -99,7 +101,7 @@ class MobileAppSensor(MobileAppEntity, RestoreSensor): self._config[ATTR_SENSOR_UOM] = last_sensor_data.native_unit_of_measurement @property - def native_value(self): + def native_value(self) -> StateType | date | datetime: """Return the state of the sensor.""" if (state := self._config[ATTR_SENSOR_STATE]) in (None, STATE_UNKNOWN): return None @@ -122,7 +124,7 @@ class MobileAppSensor(MobileAppEntity, RestoreSensor): return state @property - def native_unit_of_measurement(self): + def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement this sensor expresses itself in.""" return self._config.get(ATTR_SENSOR_UOM) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index 107058352c1..6476b681256 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -15,10 +15,14 @@ from nacl.exceptions import CryptoError from nacl.secret import SecretBox import voluptuous as vol -from homeassistant.components import camera, cloud, notify as hass_notify, tag -from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES as BINARY_SENSOR_CLASSES, +from homeassistant.components import ( + camera, + cloud, + conversation, + notify as hass_notify, + tag, ) +from homeassistant.components.binary_sensor import BinarySensorDeviceClass from homeassistant.components.camera import CameraEntityFeature from homeassistant.components.device_tracker import ( ATTR_BATTERY, @@ -27,10 +31,7 @@ from homeassistant.components.device_tracker import ( ATTR_LOCATION_NAME, ) from homeassistant.components.frontend import MANIFEST_JSON -from homeassistant.components.sensor import ( - DEVICE_CLASSES as SENSOR_CLASSES, - STATE_CLASSES as SENSOSR_STATE_CLASSES, -) +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -52,7 +53,7 @@ from homeassistant.helpers import ( template, ) from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA +from homeassistant.helpers.entity import EntityCategory from homeassistant.util.decorator import Registry from .const import ( @@ -125,8 +126,7 @@ WEBHOOK_COMMANDS: Registry[ str, Callable[[HomeAssistant, ConfigEntry, Any], Coroutine[Any, Any, Response]] ] = Registry() -COMBINED_CLASSES = set(BINARY_SENSOR_CLASSES + SENSOR_CLASSES) -SENSOR_TYPES = [ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR] +SENSOR_TYPES = (ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR) WEBHOOK_PAYLOAD_SCHEMA = vol.Schema( { @@ -301,6 +301,28 @@ async def webhook_fire_event( return empty_okay_response() +@WEBHOOK_COMMANDS.register("conversation_process") +@validate_schema( + { + vol.Required("text"): cv.string, + vol.Optional("language"): cv.string, + vol.Optional("conversation_id"): cv.string, + } +) +async def webhook_conversation_process( + hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, Any] +) -> Response: + """Handle a conversation process webhook.""" + result = await conversation.async_converse( + hass, + text=data["text"], + language=data.get("language"), + conversation_id=data.get("conversation_id"), + context=registration_context(config_entry.data), + ) + return webhook_response(result.as_dict(), registration=config_entry.data) + + @WEBHOOK_COMMANDS.register("stream_camera") @validate_schema({vol.Required(ATTR_CAMERA_ENTITY_ID): cv.string}) async def webhook_stream_camera( @@ -479,19 +501,27 @@ def _extract_sensor_unique_id(webhook_id: str, unique_id: str) -> str: vol.All( { vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, - vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.All( - vol.Lower, vol.In(COMBINED_CLASSES) + vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.Any( + None, + vol.All(vol.Lower, vol.Coerce(BinarySensorDeviceClass)), + vol.All(vol.Lower, vol.Coerce(SensorDeviceClass)), ), vol.Required(ATTR_SENSOR_NAME): cv.string, vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, - vol.Optional(ATTR_SENSOR_UOM): cv.string, + vol.Optional(ATTR_SENSOR_UOM): vol.Any(None, cv.string), vol.Optional(ATTR_SENSOR_STATE, default=None): vol.Any( - None, bool, str, int, float + None, bool, int, float, str + ), + vol.Optional(ATTR_SENSOR_ENTITY_CATEGORY): vol.Any( + None, vol.Coerce(EntityCategory) + ), + vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): vol.Any( + None, cv.icon + ), + vol.Optional(ATTR_SENSOR_STATE_CLASS): vol.Any( + None, vol.Coerce(SensorStateClass) ), - vol.Optional(ATTR_SENSOR_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA, - vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, - vol.Optional(ATTR_SENSOR_STATE_CLASS): vol.In(SENSOSR_STATE_CLASSES), vol.Optional(ATTR_SENSOR_DISABLED): bool, }, _validate_state_class_sensor, @@ -591,8 +621,10 @@ async def webhook_update_sensor_states( sensor_schema_full = vol.Schema( { vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, - vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, - vol.Required(ATTR_SENSOR_STATE): vol.Any(None, bool, str, int, float), + vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): vol.Any( + None, cv.icon + ), + vol.Required(ATTR_SENSOR_STATE): vol.Any(None, bool, int, float, str), vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, } diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 06d7b1b6a11..4f416874f9d 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -10,6 +10,7 @@ from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_DEVICE_CLASS, CONF_NAME, + CONF_UNIQUE_ID, STATE_ON, ) from homeassistant.core import HomeAssistant, callback @@ -59,8 +60,8 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity): 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: list = [] + self._coordinator: DataUpdateCoordinator[list[int] | None] | None = None + self._result: list[int] = [] super().__init__(hub, entry) async def async_setup_slaves( @@ -121,16 +122,26 @@ class ModbusBinarySensor(BasePlatform, RestoreEntity, BinarySensorEntity): self._coordinator.async_set_updated_data(self._result) -class SlaveSensor(CoordinatorEntity, RestoreEntity, BinarySensorEntity): +class SlaveSensor( + CoordinatorEntity[DataUpdateCoordinator[list[int] | None]], + RestoreEntity, + BinarySensorEntity, +): """Modbus slave binary sensor.""" def __init__( - self, coordinator: DataUpdateCoordinator[Any], idx: int, entry: dict[str, Any] + self, + coordinator: DataUpdateCoordinator[list[int] | None], + 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_unique_id = entry.get(CONF_UNIQUE_ID) + if self._attr_unique_id: + self._attr_unique_id = f"{self._attr_unique_id}_{idx}" self._attr_available = False self._result_inx = idx super().__init__(coordinator) @@ -146,6 +157,5 @@ class SlaveSensor(CoordinatorEntity, RestoreEntity, BinarySensorEntity): def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" result = self.coordinator.data - if result: - self._attr_is_on = bool(result[self._result_inx] & 1) + self._attr_is_on = bool(result[self._result_inx] & 1) if result else None super()._handle_coordinator_update() diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 96127f39bbd..a9a5a13c98b 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -2,7 +2,7 @@ "domain": "modbus", "name": "Modbus", "documentation": "https://www.home-assistant.io/integrations/modbus", - "requirements": ["pymodbus==2.5.3"], + "requirements": ["pymodbus==3.1.1"], "codeowners": ["@adamchengtkc", "@janiversen", "@vzahradnik"], "quality_scale": "gold", "iot_class": "local_polling", diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index e2240f530c6..fb30d245850 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -7,8 +7,8 @@ from collections.abc import Callable import logging from typing import Any -from pymodbus.client.sync import ( - BaseModbusClient, +from pymodbus.client import ( + ModbusBaseClient, ModbusSerialClient, ModbusTcpClient, ModbusUdpClient, @@ -255,7 +255,7 @@ class ModbusHub: """Initialize the Modbus hub.""" # generic configuration - self._client: BaseModbusClient | None = None + self._client: ModbusBaseClient | None = None self._async_cancel_listener: Callable[[], None] | None = None self._in_error = False self._lock = asyncio.Lock() @@ -371,16 +371,16 @@ class ModbusHub: except ModbusException as exception_error: self._log_error(str(exception_error), error_state=False) return False - else: - message = f"modbus {self.name} communication open" - _LOGGER.info(message) - return True + + message = f"modbus {self.name} communication open" + _LOGGER.info(message) + return True def _pymodbus_call( self, unit: int | None, address: int, value: int | list[int], use_call: str ) -> ModbusResponse: """Call sync. pymodbus.""" - kwargs = {"unit": unit} if unit else {} + kwargs = {"slave": unit} if unit else {} entry = self._pb_call[use_call] try: result = entry.func(address, value, **kwargs) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 7e9295fdb14..04b986e41ba 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -6,7 +6,12 @@ import logging from typing import Any from homeassistant.components.sensor import CONF_STATE_CLASS, SensorEntity -from homeassistant.const import CONF_NAME, CONF_SENSORS, CONF_UNIT_OF_MEASUREMENT +from homeassistant.const import ( + CONF_NAME, + CONF_SENSORS, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity @@ -58,7 +63,7 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): ) -> None: """Initialize the modbus register sensor.""" super().__init__(hub, entry) - self._coordinator: DataUpdateCoordinator[Any] | None = None + self._coordinator: DataUpdateCoordinator[list[int] | None] | None = None self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) self._attr_state_class = entry.get(CONF_STATE_CLASS) @@ -110,7 +115,9 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): result = self.unpack_structure_result(raw_result.registers) if self._coordinator: if result: - result_array = result.split(",") + result_array = list( + map(float if self._precision else int, result.split(",")) + ) self._attr_native_value = result_array[0] self._coordinator.async_set_updated_data(result_array) else: @@ -126,16 +133,26 @@ class ModbusRegisterSensor(BaseStructPlatform, RestoreEntity, SensorEntity): self.async_write_ha_state() -class SlaveSensor(CoordinatorEntity, RestoreEntity, SensorEntity): - """Modbus slave binary sensor.""" +class SlaveSensor( + CoordinatorEntity[DataUpdateCoordinator[list[int] | None]], + RestoreEntity, + SensorEntity, +): + """Modbus slave register sensor.""" def __init__( - self, coordinator: DataUpdateCoordinator[Any], idx: int, entry: dict[str, Any] + self, + coordinator: DataUpdateCoordinator[list[int] | None], + idx: int, + entry: dict[str, Any], ) -> None: - """Initialize the Modbus binary sensor.""" + """Initialize the Modbus register sensor.""" idx += 1 self._idx = idx self._attr_name = f"{entry[CONF_NAME]} {idx}" + self._attr_unique_id = entry.get(CONF_UNIQUE_ID) + if self._attr_unique_id: + self._attr_unique_id = f"{self._attr_unique_id}_{idx}" self._attr_available = False super().__init__(coordinator) @@ -149,6 +166,5 @@ class SlaveSensor(CoordinatorEntity, RestoreEntity, SensorEntity): def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" result = self.coordinator.data - if result: - self._attr_native_value = result[self._idx] + self._attr_native_value = result[self._idx] if result else None super()._handle_coordinator_update() diff --git a/homeassistant/components/modem_callerid/config_flow.py b/homeassistant/components/modem_callerid/config_flow.py index 2bc857a16f4..537fe81da11 100644 --- a/homeassistant/components/modem_callerid/config_flow.py +++ b/homeassistant/components/modem_callerid/config_flow.py @@ -111,5 +111,5 @@ class PhoneModemFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await api.test(dev_path) except EXCEPTIONS: return {"base": "cannot_connect"} - else: - return None + + return None diff --git a/homeassistant/components/modem_callerid/translations/lv.json b/homeassistant/components/modem_callerid/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/modem_callerid/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modern_forms/translations/lv.json b/homeassistant/components/modern_forms/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/modern_forms/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moehlenhoff_alpha2/__init__.py b/homeassistant/components/moehlenhoff_alpha2/__init__.py index b2d3438250f..4992ecf34a7 100644 --- a/homeassistant/components/moehlenhoff_alpha2/__init__.py +++ b/homeassistant/components/moehlenhoff_alpha2/__init__.py @@ -17,7 +17,7 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.BINARY_SENSOR] +PLATFORMS = [Platform.BUTTON, Platform.CLIMATE, Platform.SENSOR, Platform.BINARY_SENSOR] UPDATE_INTERVAL = timedelta(seconds=60) @@ -124,7 +124,7 @@ class Alpha2BaseCoordinator(DataUpdateCoordinator[dict[str, dict]]): """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}") + raise ValueError(f"Invalid heat area mode: {heat_area_mode}") _LOGGER.debug( "Setting mode of heat area %s to %d", heat_area_id, diff --git a/homeassistant/components/moehlenhoff_alpha2/button.py b/homeassistant/components/moehlenhoff_alpha2/button.py new file mode 100644 index 00000000000..4a8f21b089d --- /dev/null +++ b/homeassistant/components/moehlenhoff_alpha2/button.py @@ -0,0 +1,41 @@ +"""Button entity to set the time of the Alpha2 base.""" + +from homeassistant.components.button import ButtonEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util import dt + +from . import Alpha2BaseCoordinator +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add Alpha2 button entities.""" + + coordinator: Alpha2BaseCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities([Alpha2TimeSyncButton(coordinator, config_entry.entry_id)]) + + +class Alpha2TimeSyncButton(CoordinatorEntity[Alpha2BaseCoordinator], ButtonEntity): + """Alpha2 virtual time sync button.""" + + _attr_name = "Sync time" + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__(self, coordinator: Alpha2BaseCoordinator, entry_id: str) -> None: + """Initialize Alpha2TimeSyncButton.""" + super().__init__(coordinator) + + self._attr_unique_id = f"{entry_id}:sync_time" + + async def async_press(self) -> None: + """Synchronize current local time from HA instance to base station.""" + await self.coordinator.base.set_datetime(dt.now()) diff --git a/homeassistant/components/moehlenhoff_alpha2/manifest.json b/homeassistant/components/moehlenhoff_alpha2/manifest.json index 12e7a927906..961971468a9 100644 --- a/homeassistant/components/moehlenhoff_alpha2/manifest.json +++ b/homeassistant/components/moehlenhoff_alpha2/manifest.json @@ -3,7 +3,7 @@ "name": "Möhlenhoff Alpha 2", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/moehlenhoff_alpha2", - "requirements": ["moehlenhoff-alpha2==1.2.1"], + "requirements": ["moehlenhoff-alpha2==1.3.0"], "iot_class": "local_push", "codeowners": ["@j-a-n"] } diff --git a/homeassistant/components/moehlenhoff_alpha2/translations/lv.json b/homeassistant/components/moehlenhoff_alpha2/translations/lv.json index d15111e97b0..eb9dca9b9ee 100644 --- a/homeassistant/components/moehlenhoff_alpha2/translations/lv.json +++ b/homeassistant/components/moehlenhoff_alpha2/translations/lv.json @@ -1,3 +1,8 @@ { + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + }, "title": "M\u00f6hlenhoff Alpha2" } \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/lv.json b/homeassistant/components/monoprice/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/monoprice/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/bg.json b/homeassistant/components/moon/translations/bg.json index 0ab1984d0c6..d2ed7641de1 100644 --- a/homeassistant/components/moon/translations/bg.json +++ b/homeassistant/components/moon/translations/bg.json @@ -16,15 +16,19 @@ "first_quarter": "\u041f\u044a\u0440\u0432\u0430 \u0447\u0435\u0442\u0432\u044a\u0440\u0442", "full_moon": "\u041f\u044a\u043b\u043d\u043e\u043b\u0443\u043d\u0438\u0435", "last_quarter": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0430 \u0447\u0435\u0442\u0432\u044a\u0440\u0442", - "new_moon": "\u041d\u043e\u0432\u043e\u043b\u0443\u043d\u0438\u0435" + "new_moon": "\u041d\u043e\u0432\u043e\u043b\u0443\u043d\u0438\u0435", + "waning_crescent": "\u0417\u0430\u043b\u044f\u0437\u0432\u0430\u0449 \u043f\u043e\u043b\u0443\u043c\u0435\u0441\u0435\u0446", + "waning_gibbous": "\u041d\u0430\u043c\u0430\u043b\u044f\u0432\u0430\u0449\u0430 \u043b\u0443\u043d\u0430", + "waxing_crescent": "\u0418\u0437\u0433\u0440\u044f\u0432\u0430\u0449 \u043f\u043e\u043b\u0443\u043c\u0435\u0441\u0435\u0446", + "waxing_gibbous": "\u0420\u0430\u0441\u0442\u044f\u0449\u0430 \u043b\u0443\u043d\u0430" } } } }, "issues": { "removed_yaml": { - "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 Moon \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u043e.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043e\u0442 Home Assistant.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", - "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Moon \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" + "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043d\u0430 \u041b\u0443\u043d\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u0442\u0430 \u043d\u0430 YAML \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u043e.\n\n\u0412\u0430\u0448\u0430\u0442\u0430 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430\u0449\u0430 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u043e\u0442 Home Assistant.\n\n\u041f\u0440\u0435\u043c\u0430\u0445\u043d\u0435\u0442\u0435 YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043e\u0442 \u0432\u0430\u0448\u0438\u044f \u0444\u0430\u0439\u043b configuration.yaml \u0438 \u0440\u0435\u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0439\u0442\u0435 Home Assistant, \u0437\u0430 \u0434\u0430 \u043a\u043e\u0440\u0438\u0433\u0438\u0440\u0430\u0442\u0435 \u0442\u043e\u0437\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c.", + "title": "YAML \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 \u041b\u0443\u043d\u0430 \u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" } }, "title": "\u041b\u0443\u043d\u0430" diff --git a/homeassistant/components/moon/translations/fr.json b/homeassistant/components/moon/translations/fr.json index 0b67320b311..56c7ac0a863 100644 --- a/homeassistant/components/moon/translations/fr.json +++ b/homeassistant/components/moon/translations/fr.json @@ -9,5 +9,27 @@ } } }, + "entity": { + "sensor": { + "phase": { + "state": { + "first_quarter": "Premier quartier", + "full_moon": "Pleine lune", + "last_quarter": "Dernier quartier", + "new_moon": "Nouvelle lune", + "waning_crescent": "Dernier croissant", + "waning_gibbous": "Gibbeuse d\u00e9croissante", + "waxing_crescent": "Premier croissant", + "waxing_gibbous": "Gibbeuse croissante" + } + } + } + }, + "issues": { + "removed_yaml": { + "description": "La configuration de Lune \u00e0 l'aide de YAML a \u00e9t\u00e9 supprim\u00e9e. \n\nVotre configuration YAML existante n'est pas utilis\u00e9e par Home Assistant. \n\nSupprimez la configuration YAML de votre fichier configuration.yaml et red\u00e9marrez Home Assistant pour r\u00e9soudre ce probl\u00e8me.", + "title": "La configuration YAML pour Lune a \u00e9t\u00e9 supprim\u00e9e" + } + }, "title": "Lune" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/ja.json b/homeassistant/components/moon/translations/ja.json index f7678a63278..9b84e2febf1 100644 --- a/homeassistant/components/moon/translations/ja.json +++ b/homeassistant/components/moon/translations/ja.json @@ -9,5 +9,27 @@ } } }, + "entity": { + "sensor": { + "phase": { + "state": { + "first_quarter": "\u4e0a\u5f26\u306e\u6708", + "full_moon": "\u6e80\u6708", + "last_quarter": "\u4e0b\u5f26\u306e\u6708", + "new_moon": "\u65b0\u6708", + "waning_crescent": "\u4e8c\u5341\u516d\u591c", + "waning_gibbous": "\u5341\u516b\u591c", + "waxing_crescent": "\u4e09\u65e5\u6708", + "waxing_gibbous": "\u5341\u4e09\u591c" + } + } + } + }, + "issues": { + "removed_yaml": { + "description": "Moon\u306eYAML\u3092\u4f7f\u7528\u3057\u305f\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f\u3002\n\n\u3059\u3067\u306b\u65e2\u5b58\u306eYAML\u8a2d\u5b9a\u306f\u3001Home Assistant\u3067\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", + "title": "Moon YAML\u306e\u8a2d\u5b9a\u306f\u524a\u9664\u3055\u308c\u307e\u3057\u305f" + } + }, "title": "\u6708" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/nl.json b/homeassistant/components/moon/translations/nl.json index 241c41ea21e..3b5428e19dd 100644 --- a/homeassistant/components/moon/translations/nl.json +++ b/homeassistant/components/moon/translations/nl.json @@ -13,10 +13,23 @@ "sensor": { "phase": { "state": { - "full_moon": "Volle maan" + "first_quarter": "Eerste kwartier", + "full_moon": "Volle maan", + "last_quarter": "Laatste kwartier", + "new_moon": "Nieuwe maan", + "waning_crescent": "Afnemende, sikkelvormige maan", + "waning_gibbous": "Afnemende maan", + "waxing_crescent": "Wassende, sikkelvormige maan", + "waxing_gibbous": "Wassende maan" } } } }, + "issues": { + "removed_yaml": { + "description": "Instellen van `moon` via YAML is niet langer een mogelijkheid.\n\nDe bestaande YAML configuratie wordt niet gebruikt door Home Assistant.\n\nVerwijder de YAML configuratie van je configuration.yaml bestand en herstart Home Assistant om dit probleem op te lossen.", + "title": "De `moon` YAML configuratie is verwijderd" + } + }, "title": "Maan" } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/sensor.bg.json b/homeassistant/components/moon/translations/sensor.bg.json index 1bb8964f9ab..247fd6fd50f 100644 --- a/homeassistant/components/moon/translations/sensor.bg.json +++ b/homeassistant/components/moon/translations/sensor.bg.json @@ -1,14 +1,14 @@ { "state": { "moon__phase": { - "first_quarter": "\u041f\u044a\u0440\u0432\u0430 \u0447\u0435\u0442\u0432\u044a\u0440\u0442\u0438\u043d\u0430", + "first_quarter": "\u041f\u044a\u0440\u0432\u0430 \u0447\u0435\u0442\u0432\u044a\u0440\u0442", "full_moon": "\u041f\u044a\u043b\u043d\u043e\u043b\u0443\u043d\u0438\u0435", - "last_quarter": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0430 \u0447\u0435\u0442\u0432\u044a\u0440\u0442\u0438\u043d\u0430", + "last_quarter": "\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0430 \u0447\u0435\u0442\u0432\u044a\u0440\u0442", "new_moon": "\u041d\u043e\u0432\u043e\u043b\u0443\u043d\u0438\u0435", - "waning_crescent": "\u041d\u0430\u043c\u0430\u043b\u044f\u0432\u0430\u0449 \u043f\u043e\u043b\u0443\u043c\u0435\u0441\u0435\u0446", - "waning_gibbous": "\u041d\u0430\u043c\u0430\u043b\u044f\u0432\u0430\u0449 \u043f\u043e\u043b\u0443\u043c\u0435\u0441\u0435\u0446", - "waxing_crescent": "\u041d\u0430\u0440\u0430\u0441\u0442\u0432\u0430\u0449 \u043f\u043e\u043b\u0443\u043c\u0435\u0441\u0435\u0446", - "waxing_gibbous": "\u041d\u0430\u0440\u0430\u0441\u0442\u0432\u0430\u0449 \u043f\u043e\u043b\u0443\u043c\u0435\u0441\u0435\u0446" + "waning_crescent": "\u0417\u0430\u043b\u044f\u0437\u0432\u0430\u0449 \u043f\u043e\u043b\u0443\u043c\u0435\u0441\u0435\u0446", + "waning_gibbous": "\u041d\u0430\u043c\u0430\u043b\u044f\u0432\u0430\u0449\u0430 \u043b\u0443\u043d\u0430", + "waxing_crescent": "\u0418\u0437\u0433\u0440\u044f\u0432\u0430\u0449 \u043f\u043e\u043b\u0443\u043c\u0435\u0441\u0435\u0446", + "waxing_gibbous": "\u0420\u0430\u0441\u0442\u044f\u0449\u0430 \u043b\u0443\u043d\u0430" } } } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/tr.json b/homeassistant/components/moon/translations/tr.json index d1c5810d269..e27e83d4adf 100644 --- a/homeassistant/components/moon/translations/tr.json +++ b/homeassistant/components/moon/translations/tr.json @@ -5,7 +5,23 @@ }, "step": { "user": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" + } + } + }, + "entity": { + "sensor": { + "phase": { + "state": { + "first_quarter": "\u0130lk D\u00f6rd\u00fcn", + "full_moon": "Dolunay", + "last_quarter": "Son D\u00f6rd\u00fcn", + "new_moon": "Yeni Ay", + "waning_crescent": "Azalan Hilal", + "waning_gibbous": "\u015ei\u015fkin Ay", + "waxing_crescent": "Hilal", + "waxing_gibbous": "\u015ei\u015fkin Ay" + } } } }, diff --git a/homeassistant/components/moon/translations/uk.json b/homeassistant/components/moon/translations/uk.json new file mode 100644 index 00000000000..8e67822b577 --- /dev/null +++ b/homeassistant/components/moon/translations/uk.json @@ -0,0 +1,12 @@ +{ + "entity": { + "sensor": { + "phase": { + "state": { + "waning_gibbous": "\u0421\u043f\u0430\u0434\u0430\u044e\u0447\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c", + "waxing_crescent": "\u041c\u043e\u043b\u043e\u0434\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mopeka/__init__.py b/homeassistant/components/mopeka/__init__.py new file mode 100644 index 00000000000..a000ef0cc88 --- /dev/null +++ b/homeassistant/components/mopeka/__init__.py @@ -0,0 +1,49 @@ +"""The Mopeka integration.""" +from __future__ import annotations + +import logging + +from mopeka_iot_ble import MopekaIOTBluetoothDeviceData + +from homeassistant.components.bluetooth import BluetoothScanningMode +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothProcessorCoordinator, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Mopeka BLE device from a config entry.""" + address = entry.unique_id + assert address is not None + data = MopekaIOTBluetoothDeviceData() + coordinator = hass.data.setdefault(DOMAIN, {})[ + entry.entry_id + ] = PassiveBluetoothProcessorCoordinator( + hass, + _LOGGER, + address=address, + mode=BluetoothScanningMode.PASSIVE, + update_method=data.update, + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload( + coordinator.async_start() + ) # only start after all platforms have had a chance to subscribe + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/mopeka/config_flow.py b/homeassistant/components/mopeka/config_flow.py new file mode 100644 index 00000000000..54a2e7bcaf3 --- /dev/null +++ b/homeassistant/components/mopeka/config_flow.py @@ -0,0 +1,94 @@ +"""Config flow for mopeka integration.""" +from __future__ import annotations + +from typing import Any + +from mopeka_iot_ble import MopekaIOTBluetoothDeviceData as DeviceData +import voluptuous as vol + +from homeassistant.components.bluetooth import ( + BluetoothServiceInfoBleak, + async_discovered_service_info, +) +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class MopekaConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for mopeka.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + self._discovery_info: BluetoothServiceInfoBleak | None = None + self._discovered_device: DeviceData | None = None + self._discovered_devices: dict[str, str] = {} + + async def async_step_bluetooth( + self, discovery_info: BluetoothServiceInfoBleak + ) -> FlowResult: + """Handle the bluetooth discovery step.""" + await self.async_set_unique_id(discovery_info.address) + self._abort_if_unique_id_configured() + device = DeviceData() + if not device.supported(discovery_info): + return self.async_abort(reason="not_supported") + self._discovery_info = discovery_info + self._discovered_device = device + return await self.async_step_bluetooth_confirm() + + async def async_step_bluetooth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm discovery.""" + assert self._discovered_device is not None + device = self._discovered_device + assert self._discovery_info is not None + discovery_info = self._discovery_info + title = device.title or device.get_device_name() or discovery_info.name + if user_input is not None: + return self.async_create_entry(title=title, data={}) + + self._set_confirm_only() + placeholders = {"name": title} + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="bluetooth_confirm", description_placeholders=placeholders + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the user step to pick discovered device.""" + if user_input is not None: + address = user_input[CONF_ADDRESS] + await self.async_set_unique_id(address, raise_on_progress=False) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=self._discovered_devices[address], data={} + ) + + current_addresses = self._async_current_ids() + for discovery_info in async_discovered_service_info(self.hass, False): + address = discovery_info.address + if address in current_addresses or address in self._discovered_devices: + continue + device = DeviceData() + if device.supported(discovery_info): + self._discovered_devices[address] = ( + device.title or device.get_device_name() or discovery_info.name + ) + + if not self._discovered_devices: + return self.async_abort(reason="no_devices_found") + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)} + ), + ) diff --git a/homeassistant/components/mopeka/const.py b/homeassistant/components/mopeka/const.py new file mode 100644 index 00000000000..0d78146f5a8 --- /dev/null +++ b/homeassistant/components/mopeka/const.py @@ -0,0 +1,3 @@ +"""Constants for the Mopeka integration.""" + +DOMAIN = "mopeka" diff --git a/homeassistant/components/mopeka/device.py b/homeassistant/components/mopeka/device.py new file mode 100644 index 00000000000..74bc389d3ae --- /dev/null +++ b/homeassistant/components/mopeka/device.py @@ -0,0 +1,15 @@ +"""Support for Mopeka devices.""" +from __future__ import annotations + +from mopeka_iot_ble import DeviceKey + +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothEntityKey, +) + + +def device_key_to_bluetooth_entity_key( + device_key: DeviceKey, +) -> PassiveBluetoothEntityKey: + """Convert a device key to an entity key.""" + return PassiveBluetoothEntityKey(device_key.key, device_key.device_id) diff --git a/homeassistant/components/mopeka/manifest.json b/homeassistant/components/mopeka/manifest.json new file mode 100644 index 00000000000..4f03ebad856 --- /dev/null +++ b/homeassistant/components/mopeka/manifest.json @@ -0,0 +1,25 @@ +{ + "domain": "mopeka", + "name": "Mopeka", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/mopeka", + "bluetooth": [ + { + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 89, + "manufacturer_data_start": [3], + "connectable": false + }, + { + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + "manufacturer_id": 89, + "manufacturer_data_start": [8], + "connectable": false + } + ], + "requirements": ["mopeka_iot_ble==0.4.0"], + "dependencies": ["bluetooth_adapters"], + "codeowners": ["@bdraco"], + "iot_class": "local_push", + "integration_type": "device" +} diff --git a/homeassistant/components/mopeka/sensor.py b/homeassistant/components/mopeka/sensor.py new file mode 100644 index 00000000000..26965cd9a14 --- /dev/null +++ b/homeassistant/components/mopeka/sensor.py @@ -0,0 +1,141 @@ +"""Support for Mopeka sensors.""" +from __future__ import annotations + +from mopeka_iot_ble import SensorUpdate + +from homeassistant import config_entries +from homeassistant.components.bluetooth.passive_update_processor import ( + PassiveBluetoothDataProcessor, + PassiveBluetoothDataUpdate, + PassiveBluetoothProcessorCoordinator, + PassiveBluetoothProcessorEntity, +) +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + UnitOfElectricPotential, + UnitOfLength, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info + +from .const import DOMAIN +from .device import device_key_to_bluetooth_entity_key + +SENSOR_DESCRIPTIONS = { + "battery": SensorEntityDescription( + key="battery", + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + ), + "battery_voltage": SensorEntityDescription( + key="battery_voltage", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "tank_level": SensorEntityDescription( + key="tank_level", + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.MILLIMETERS, + state_class=SensorStateClass.MEASUREMENT, + ), + "signal_strength": SensorEntityDescription( + key="signal_strength", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "reading_quality": SensorEntityDescription( + key="reading_quality", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), + "temperature": SensorEntityDescription( + key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + "accelerometer_x": SensorEntityDescription( + key="accelerometer_x", + entity_category=EntityCategory.DIAGNOSTIC, + ), + "accelerometer_y": SensorEntityDescription( + key="accelerometer_y", + entity_category=EntityCategory.DIAGNOSTIC, + ), +} + + +def sensor_update_to_bluetooth_data_update( + sensor_update: SensorUpdate, +) -> PassiveBluetoothDataUpdate: + """Convert a sensor update to a bluetooth data update.""" + return PassiveBluetoothDataUpdate( + devices={ + device_id: sensor_device_info_to_hass_device_info(device_info) + for device_id, device_info in sensor_update.devices.items() + }, + entity_descriptions={ + device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[ + device_key.key + ] + for device_key in sensor_update.entity_descriptions + if device_key.key in SENSOR_DESCRIPTIONS + }, + entity_data={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value + for device_key, sensor_values in sensor_update.entity_values.items() + }, + entity_names={ + device_key_to_bluetooth_entity_key(device_key): sensor_values.name + for device_key, sensor_values in sensor_update.entity_values.items() + }, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: config_entries.ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Mopeka BLE sensors.""" + coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] + processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update) + entry.async_on_unload( + processor.async_add_entities_listener( + MopekaBluetoothSensorEntity, async_add_entities + ) + ) + entry.async_on_unload(coordinator.async_register_processor(processor)) + + +class MopekaBluetoothSensorEntity( + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], + SensorEntity, +): + """Representation of a Mopeka sensor.""" + + @property + def native_value(self) -> int | float | None: + """Return the native value.""" + return self.processor.entity_data.get(self.entity_key) diff --git a/homeassistant/components/mopeka/strings.json b/homeassistant/components/mopeka/strings.json new file mode 100644 index 00000000000..a045d84771e --- /dev/null +++ b/homeassistant/components/mopeka/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "flow_title": "[%key:component::bluetooth::config::flow_title%]", + "step": { + "user": { + "description": "[%key:component::bluetooth::config::step::user::description%]", + "data": { + "address": "[%key:component::bluetooth::config::step::user::data::address%]" + } + }, + "bluetooth_confirm": { + "description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]" + } + }, + "abort": { + "not_supported": "Device not supported", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index 378a2f1f03d..e53b006ddd8 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -71,8 +71,8 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): except (timeout, ParseException): # let the error be logged and handled by the motionblinds library return {ATTR_AVAILABLE: False} - else: - return {ATTR_AVAILABLE: True} + + return {ATTR_AVAILABLE: True} def update_blind(self, blind): """Fetch data from a blind.""" @@ -84,8 +84,8 @@ class DataUpdateCoordinatorMotionBlinds(DataUpdateCoordinator): except (timeout, ParseException): # let the error be logged and handled by the motionblinds library return {ATTR_AVAILABLE: False} - else: - return {ATTR_AVAILABLE: True} + + return {ATTR_AVAILABLE: True} async def _async_update_data(self): """Fetch the latest data from the gateway and blinds.""" diff --git a/homeassistant/components/motion_blinds/translations/el.json b/homeassistant/components/motion_blinds/translations/el.json index b9d67703c57..eedb8133cf9 100644 --- a/homeassistant/components/motion_blinds/translations/el.json +++ b/homeassistant/components/motion_blinds/translations/el.json @@ -8,7 +8,7 @@ "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" }, - "flow_title": "Motion Blinds", + "flow_title": "{short_mac} ({ip_address})", "step": { "connect": { "data": { diff --git a/homeassistant/components/motion_blinds/translations/lt.json b/homeassistant/components/motion_blinds/translations/lt.json new file mode 100644 index 00000000000..c82e4b7b7e6 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/lt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u012erenginys jau sukonfig\u016bruotas" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/lv.json b/homeassistant/components/motion_blinds/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motioneye/config_flow.py b/homeassistant/components/motioneye/config_flow.py index 662c4d23660..4ab4761fe0b 100644 --- a/homeassistant/components/motioneye/config_flow.py +++ b/homeassistant/components/motioneye/config_flow.py @@ -72,7 +72,7 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): ): str, vol.Optional( CONF_ADMIN_PASSWORD, - default=user_input.get(CONF_ADMIN_PASSWORD), + default=user_input.get(CONF_ADMIN_PASSWORD, ""), ): str, vol.Optional( CONF_SURVEILLANCE_USERNAME, @@ -80,7 +80,7 @@ class MotionEyeConfigFlow(ConfigFlow, domain=DOMAIN): ): str, vol.Optional( CONF_SURVEILLANCE_PASSWORD, - default=user_input.get(CONF_SURVEILLANCE_PASSWORD), + default=user_input.get(CONF_SURVEILLANCE_PASSWORD, ""), ): str, } ), diff --git a/homeassistant/components/motioneye/manifest.json b/homeassistant/components/motioneye/manifest.json index 5c1dbb376a0..ff282c69150 100644 --- a/homeassistant/components/motioneye/manifest.json +++ b/homeassistant/components/motioneye/manifest.json @@ -5,7 +5,7 @@ "config_flow": true, "dependencies": ["http", "webhook"], "after_dependencies": ["media_source"], - "requirements": ["motioneye-client==0.3.12"], + "requirements": ["motioneye-client==0.3.14"], "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 20fc4359ab2..46300e3d3db 100644 --- a/homeassistant/components/motioneye/media_source.py +++ b/homeassistant/components/motioneye/media_source.py @@ -3,7 +3,7 @@ from __future__ import annotations import logging from pathlib import PurePath -from typing import Optional, cast +from typing import cast from motioneye_client.const import KEY_MEDIA_LIST, KEY_MIME_TYPE, KEY_PATH @@ -90,7 +90,7 @@ class MotionEyeMediaSource(MediaSource): base = [None] * 4 data = identifier.split("#", 3) return cast( - tuple[Optional[str], Optional[str], Optional[str], Optional[str]], + tuple[str | None, str | None, str | None, str | None], tuple(data + base)[:4], # type: ignore[operator] ) diff --git a/homeassistant/components/motioneye/translations/uk.json b/homeassistant/components/motioneye/translations/uk.json new file mode 100644 index 00000000000..74352054433 --- /dev/null +++ b/homeassistant/components/motioneye/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u043e\u0441\u043b\u0443\u0433\u0430 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0430", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f", + "invalid_url": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439 URL", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "admin_password": "\u041f\u0430\u0440\u043e\u043b\u044c \u0430\u0434\u043c\u0456\u043d\u0456\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 3f6f2122757..34c457f395a 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -486,7 +486,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entity.async_remove() for mqtt_platform in mqtt_platforms for entity in mqtt_platform.entities.values() - if not entity._discovery_data # type: ignore[attr-defined] # pylint: disable=protected-access + # pylint: disable-next=protected-access + if not entity._discovery_data # type: ignore[attr-defined] if mqtt_platform.config_entry and mqtt_platform.domain in RELOADABLE_PLATFORMS ] @@ -542,7 +543,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: mqtt_data.reload_entry = False reload_manual_setup = True - # When the entry was disabled before, reload manual set up items to enable MQTT again + # When the entry was disabled before, reload manual set up items to enable + # MQTT again if mqtt_data.reload_needed: mqtt_data.reload_needed = False reload_manual_setup = True @@ -710,7 +712,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Trigger reload manual MQTT items at entry setup if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is False: - # The entry is disabled reload legacy manual items when the entry is enabled again + # The entry is disabled reload legacy manual items when + # the entry is enabled again mqtt_data.reload_needed = True elif mqtt_entry_status is True: # The entry is reloaded: diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 913a6e13400..8664027e245 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -42,7 +42,10 @@ ABBREVIATIONS = { "cmd_tpl": "command_template", "cod_arm_req": "code_arm_required", "cod_dis_req": "code_disarm_required", + "cod_form": "code_format", "cod_trig_req": "code_trigger_required", + "curr_hum_t": "current_humidity_topic", + "curr_hum_tpl": "current_humidity_template", "curr_temp_t": "current_temperature_topic", "curr_temp_tpl": "current_temperature_template", "dev": "device", @@ -78,6 +81,7 @@ ABBREVIATIONS = { "hold_stat_tpl": "hold_state_template", "hold_stat_t": "hold_state_topic", "hs_cmd_t": "hs_command_topic", + "hs_cmd_tpl": "hs_command_template", "hs_stat_t": "hs_state_topic", "hs_val_tpl": "hs_value_template", "ic": "icon", @@ -195,13 +199,16 @@ ABBREVIATIONS = { "stat_cla": "state_class", "stat_clsd": "state_closed", "stat_closing": "state_closing", + "stat_jam": "state_jammed", "stat_off": "state_off", "stat_on": "state_on", "stat_open": "state_open", "stat_opening": "state_opening", "stat_stopped": "state_stopped", "stat_locked": "state_locked", + "stat_locking": "state_locking", "stat_unlocked": "state_unlocked", + "stat_unlocking": "state_unlocking", "stat_t": "state_topic", "stat_tpl": "state_template", "stat_val_tpl": "state_value_template", @@ -250,6 +257,7 @@ ABBREVIATIONS = { "whit_val_stat_t": "white_value_state_topic", "whit_val_tpl": "white_value_template", "xy_cmd_t": "xy_command_topic", + "xy_cmd_tpl": "xy_command_template", "xy_stat_t": "xy_state_topic", "xy_val_tpl": "xy_value_template", "l_ver_t": "latest_version_topic", diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index aa796d9ea8f..86513113281 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -112,7 +112,8 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT alarm control panels under the alarm_control_panel platform key was deprecated in HA Core 2022.6 +# Configuring MQTT alarm control panels under the alarm_control_panel platform key +# was deprecated in HA Core 2022.6; # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(alarm.DOMAIN), @@ -126,7 +127,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT alarm control panel through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT alarm control panel through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 5ed9fdfb76f..135151f179c 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -69,7 +69,8 @@ PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Binary sensors under the binary_sensor platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Binary sensors under the binary_sensor platform key was deprecated in +# HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(binary_sensor.DOMAIN), @@ -83,7 +84,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT binary sensor through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT binary sensor through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index d50a06a46d8..f81f78a487a 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -46,7 +46,8 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Buttons under the button platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Buttons under the button platform key was deprecated in +# HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(button.DOMAIN), @@ -61,7 +62,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT button through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT button through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 6ece232775a..b3a78f4d2ff 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -70,7 +70,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT camera through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT camera through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 7dc6048f2f7..755bf3636df 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -1,11 +1,8 @@ """Support for MQTT message handling.""" -# pylint: disable=deprecated-typing-alias -# In Python 3.9.0 and 3.9.1 collections.abc.Callable -# can't be used inside typing.Union or typing.Optional from __future__ import annotations import asyncio -from collections.abc import Coroutine, Iterable +from collections.abc import Callable, Coroutine, Iterable from functools import lru_cache, partial, wraps import inspect from itertools import groupby @@ -13,7 +10,7 @@ import logging from operator import attrgetter import ssl import time -from typing import TYPE_CHECKING, Any, Callable, Union, cast +from typing import TYPE_CHECKING, Any, Union, cast import uuid import attr @@ -88,7 +85,7 @@ _LOGGER = logging.getLogger(__name__) DISCOVERY_COOLDOWN = 2 TIMEOUT_ACK = 10 -SubscribePayloadType = Union[str, bytes] # Only bytes if encoding is None +SubscribePayloadType = str | bytes # Only bytes if encoding is None def publish( @@ -132,7 +129,8 @@ async def async_publish( return outgoing_payload = str(payload) if encoding != DEFAULT_ENCODING: - # a string is encoded as utf-8 by default, other encoding requires bytes as payload + # A string is encoded as utf-8 by default, other encoding + # requires bytes as payload try: outgoing_payload = outgoing_payload.encode(encoding) except (AttributeError, LookupError, UnicodeEncodeError): diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 64a5908368b..a58ccf8a190 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -13,7 +13,9 @@ from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DEFAULT_MAX_HUMIDITY, DEFAULT_MAX_TEMP, + DEFAULT_MIN_HUMIDITY, DEFAULT_MIN_TEMP, FAN_AUTO, FAN_HIGH, @@ -31,6 +33,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, CONF_NAME, + CONF_OPTIMISTIC, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_TEMPERATURE_UNIT, @@ -47,7 +50,13 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import subscription from .config import DEFAULT_RETAIN, MQTT_BASE_SCHEMA -from .const import CONF_ENCODING, CONF_QOS, CONF_RETAIN, PAYLOAD_NONE +from .const import ( + CONF_ENCODING, + CONF_QOS, + CONF_RETAIN, + DEFAULT_OPTIMISTIC, + PAYLOAD_NONE, +) from .debug_info import log_messages from .mixins import ( MQTT_ENTITY_COMMON_SCHEMA, @@ -73,11 +82,14 @@ CONF_ACTION_TOPIC = "action_topic" 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 no longer supported, support was removed with release 2022.9 +# AWAY and HOLD mode topics and templates are no longer supported, +# support was 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" +CONF_CURRENT_HUMIDITY_TEMPLATE = "current_humidity_template" +CONF_CURRENT_HUMIDITY_TOPIC = "current_humidity_topic" CONF_CURRENT_TEMP_TEMPLATE = "current_temperature_template" CONF_CURRENT_TEMP_TOPIC = "current_temperature_topic" CONF_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_command_template" @@ -85,18 +97,29 @@ CONF_FAN_MODE_COMMAND_TOPIC = "fan_mode_command_topic" CONF_FAN_MODE_LIST = "fan_modes" CONF_FAN_MODE_STATE_TEMPLATE = "fan_mode_state_template" CONF_FAN_MODE_STATE_TOPIC = "fan_mode_state_topic" -# AWAY and HOLD mode topics and templates are no longer supported, support was removed with release 2022.9 +# AWAY and HOLD mode topics and templates are no longer supported, +# support was 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" CONF_HOLD_STATE_TOPIC = "hold_state_topic" CONF_HOLD_LIST = "hold_modes" +CONF_HUMIDITY_COMMAND_TEMPLATE = "target_humidity_command_template" +CONF_HUMIDITY_COMMAND_TOPIC = "target_humidity_command_topic" +CONF_HUMIDITY_STATE_TEMPLATE = "target_humidity_state_template" +CONF_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic" +CONF_HUMIDITY_MAX = "max_humidity" +CONF_HUMIDITY_MIN = "min_humidity" CONF_MODE_COMMAND_TEMPLATE = "mode_command_template" CONF_MODE_COMMAND_TOPIC = "mode_command_topic" CONF_MODE_LIST = "modes" CONF_MODE_STATE_TEMPLATE = "mode_state_template" CONF_MODE_STATE_TOPIC = "mode_state_topic" + +# CONF_POWER_COMMAND_TOPIC, CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE are deprecated, +# support for CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE was already removed or never added +# support was deprecated with release 2023.2 and will be removed with release 2023.8 CONF_POWER_COMMAND_TOPIC = "power_command_topic" CONF_POWER_STATE_TEMPLATE = "power_state_template" CONF_POWER_STATE_TOPIC = "power_state_topic" @@ -157,8 +180,10 @@ MQTT_CLIMATE_ATTRIBUTES_BLOCKED = frozenset( VALUE_TEMPLATE_KEYS = ( CONF_AUX_STATE_TEMPLATE, + CONF_CURRENT_HUMIDITY_TEMPLATE, CONF_CURRENT_TEMP_TEMPLATE, CONF_FAN_MODE_STATE_TEMPLATE, + CONF_HUMIDITY_STATE_TEMPLATE, CONF_MODE_STATE_TEMPLATE, CONF_POWER_STATE_TEMPLATE, CONF_ACTION_TEMPLATE, @@ -171,6 +196,7 @@ VALUE_TEMPLATE_KEYS = ( COMMAND_TEMPLATE_KEYS = { CONF_FAN_MODE_COMMAND_TEMPLATE, + CONF_HUMIDITY_COMMAND_TEMPLATE, CONF_MODE_COMMAND_TEMPLATE, CONF_PRESET_MODE_COMMAND_TEMPLATE, CONF_SWING_MODE_COMMAND_TEMPLATE, @@ -184,9 +210,12 @@ TOPIC_KEYS = ( CONF_ACTION_TOPIC, CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC, + CONF_CURRENT_HUMIDITY_TOPIC, CONF_CURRENT_TEMP_TOPIC, CONF_FAN_MODE_COMMAND_TOPIC, CONF_FAN_MODE_STATE_TOPIC, + CONF_HUMIDITY_COMMAND_TOPIC, + CONF_HUMIDITY_STATE_TOPIC, CONF_MODE_COMMAND_TOPIC, CONF_MODE_STATE_TOPIC, CONF_POWER_COMMAND_TOPIC, @@ -211,11 +240,41 @@ def valid_preset_mode_configuration(config: ConfigType) -> ConfigType: return config +def valid_humidity_range_configuration(config: ConfigType) -> ConfigType: + """Validate a target_humidity range configuration, throws otherwise.""" + if config[CONF_HUMIDITY_MIN] >= config[CONF_HUMIDITY_MAX]: + raise ValueError("target_humidity_max must be > target_humidity_min") + if config[CONF_HUMIDITY_MAX] > 100: + raise ValueError("max_humidity must be <= 100") + + return config + + +def valid_humidity_state_configuration(config: ConfigType) -> ConfigType: + """Validate humidity state. + + Ensure that if CONF_HUMIDITY_STATE_TOPIC is set then + CONF_HUMIDITY_COMMAND_TOPIC is also set. + """ + if ( + CONF_HUMIDITY_STATE_TOPIC in config + and CONF_HUMIDITY_COMMAND_TOPIC not in config + ): + raise ValueError( + f"{CONF_HUMIDITY_STATE_TOPIC} cannot be used without" + f" {CONF_HUMIDITY_COMMAND_TOPIC}" + ) + + return config + + _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_AUX_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_AUX_STATE_TEMPLATE): cv.template, vol.Optional(CONF_AUX_STATE_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_CURRENT_HUMIDITY_TEMPLATE): cv.template, + vol.Optional(CONF_CURRENT_HUMIDITY_TOPIC): valid_subscribe_topic, vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template, vol.Optional(CONF_CURRENT_TEMP_TOPIC): valid_subscribe_topic, vol.Optional(CONF_FAN_MODE_COMMAND_TEMPLATE): cv.template, @@ -226,6 +285,16 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( ): cv.ensure_list, vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_FAN_MODE_STATE_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_HUMIDITY_COMMAND_TEMPLATE): cv.template, + vol.Optional(CONF_HUMIDITY_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_HUMIDITY_MIN, default=DEFAULT_MIN_HUMIDITY): vol.Coerce( + float + ), + vol.Optional(CONF_HUMIDITY_MAX, default=DEFAULT_MAX_HUMIDITY): vol.Coerce( + float + ), + vol.Optional(CONF_HUMIDITY_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_HUMIDITY_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_MODE_COMMAND_TOPIC): valid_publish_topic, vol.Optional( @@ -242,6 +311,7 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, vol.Optional(CONF_POWER_COMMAND_TOPIC): valid_publish_topic, @@ -253,7 +323,8 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_ACTION_TEMPLATE): cv.template, vol.Optional(CONF_ACTION_TOPIC): valid_subscribe_topic, - # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together + # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST + # must be used together vol.Inclusive( CONF_PRESET_MODE_COMMAND_TOPIC, "preset_modes" ): valid_publish_topic, @@ -294,7 +365,8 @@ _PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( PLATFORM_SCHEMA_MODERN = vol.All( # Support CONF_SEND_IF_OFF is removed with release 2022.9 cv.removed(CONF_SEND_IF_OFF), - # AWAY and HOLD mode topics and templates are no longer supported, support was removed with release 2022.9 + # AWAY and HOLD mode topics and templates are no longer supported, + # support was removed with release 2022.9 cv.removed(CONF_AWAY_MODE_COMMAND_TOPIC), cv.removed(CONF_AWAY_MODE_STATE_TEMPLATE), cv.removed(CONF_AWAY_MODE_STATE_TOPIC), @@ -303,11 +375,20 @@ PLATFORM_SCHEMA_MODERN = vol.All( cv.removed(CONF_HOLD_STATE_TEMPLATE), cv.removed(CONF_HOLD_STATE_TOPIC), cv.removed(CONF_HOLD_LIST), + # CONF_POWER_COMMAND_TOPIC, CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE are deprecated, + # support for CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE was already removed or never added + # support was deprecated with release 2023.2 and will be removed with release 2023.8 + cv.deprecated(CONF_POWER_COMMAND_TOPIC), + cv.deprecated(CONF_POWER_STATE_TEMPLATE), + cv.deprecated(CONF_POWER_STATE_TOPIC), _PLATFORM_SCHEMA_BASE, valid_preset_mode_configuration, + valid_humidity_range_configuration, + valid_humidity_state_configuration, ) -# Configuring MQTT Climate under the climate platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Climate under the climate platform key was deprecated in +# HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(climate.DOMAIN), @@ -319,7 +400,8 @@ DISCOVERY_SCHEMA = vol.All( _DISCOVERY_SCHEMA_BASE, # Support CONF_SEND_IF_OFF is removed with release 2022.9 cv.removed(CONF_SEND_IF_OFF), - # AWAY and HOLD mode topics and templates are no longer supported, support was removed with release 2022.9 + # AWAY and HOLD mode topics and templates are no longer supported, + # support was removed with release 2022.9 cv.removed(CONF_AWAY_MODE_COMMAND_TOPIC), cv.removed(CONF_AWAY_MODE_STATE_TEMPLATE), cv.removed(CONF_AWAY_MODE_STATE_TOPIC), @@ -328,7 +410,15 @@ DISCOVERY_SCHEMA = vol.All( cv.removed(CONF_HOLD_STATE_TEMPLATE), cv.removed(CONF_HOLD_STATE_TOPIC), cv.removed(CONF_HOLD_LIST), + # CONF_POWER_COMMAND_TOPIC, CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE are deprecated, + # support for CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE was already removed or never added + # support was deprecated with release 2023.2 and will be removed with release 2023.8 + cv.deprecated(CONF_POWER_COMMAND_TOPIC), + cv.deprecated(CONF_POWER_STATE_TEMPLATE), + cv.deprecated(CONF_POWER_STATE_TOPIC), valid_preset_mode_configuration, + valid_humidity_range_configuration, + valid_humidity_state_configuration, ) @@ -337,7 +427,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT climate device through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT climate device through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) @@ -364,6 +454,7 @@ class MqttClimate(MqttEntity, ClimateEntity): _command_templates: dict[str, Callable[[PublishPayloadType], PublishPayloadType]] _value_templates: dict[str, Callable[[ReceivePayloadType], ReceivePayloadType]] _feature_preset_mode: bool + _optimistic: bool _optimistic_preset_mode: bool _topic: dict[str, Any] @@ -375,6 +466,13 @@ class MqttClimate(MqttEntity, ClimateEntity): discovery_data: DiscoveryInfoType | None, ) -> None: """Initialize the climate device.""" + self._attr_fan_mode = None + self._attr_hvac_action = None + self._attr_hvac_mode = None + self._attr_is_aux_heat = None + self._attr_swing_mode = None + self._attr_target_temperature_low = None + self._attr_target_temperature_high = None MqttEntity.__init__(self, hass, config, config_entry, discovery_data) @staticmethod @@ -387,6 +485,8 @@ class MqttClimate(MqttEntity, ClimateEntity): self._attr_hvac_modes = config[CONF_MODE_LIST] self._attr_min_temp = config[CONF_TEMP_MIN] self._attr_max_temp = config[CONF_TEMP_MAX] + self._attr_min_humidity = config[CONF_HUMIDITY_MIN] + self._attr_max_humidity = config[CONF_HUMIDITY_MAX] self._attr_precision = config.get(CONF_PRECISION, super().precision) self._attr_fan_modes = config[CONF_FAN_MODE_LIST] self._attr_swing_modes = config[CONF_SWING_MODE_LIST] @@ -397,27 +497,23 @@ class MqttClimate(MqttEntity, ClimateEntity): self._topic = {key: config.get(key) for key in TOPIC_KEYS} - # set to None in non-optimistic mode - self._attr_target_temperature = None - self._attr_fan_mode = None - self._attr_hvac_mode = None - self._attr_swing_mode = None - self._attr_target_temperature_low = None - self._attr_target_temperature_high = None + self._optimistic = config[CONF_OPTIMISTIC] - if self._topic[CONF_TEMP_STATE_TOPIC] is None: + if self._topic[CONF_TEMP_STATE_TOPIC] is None or self._optimistic: self._attr_target_temperature = config[CONF_TEMP_INITIAL] - if self._topic[CONF_TEMP_LOW_STATE_TOPIC] is None: + if self._topic[CONF_TEMP_LOW_STATE_TOPIC] is None or self._optimistic: self._attr_target_temperature_low = config[CONF_TEMP_INITIAL] - if self._topic[CONF_TEMP_HIGH_STATE_TOPIC] is None: + if self._topic[CONF_TEMP_HIGH_STATE_TOPIC] is None or self._optimistic: self._attr_target_temperature_high = config[CONF_TEMP_INITIAL] - if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: + if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None or self._optimistic: self._attr_fan_mode = FAN_LOW - if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: + if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None or self._optimistic: self._attr_swing_mode = SWING_OFF - if self._topic[CONF_MODE_STATE_TOPIC] is None: + if self._topic[CONF_MODE_STATE_TOPIC] is None or self._optimistic: self._attr_hvac_mode = HVACMode.OFF + if self._topic[CONF_AUX_STATE_TOPIC] is None or self._optimistic: + self._attr_is_aux_heat = False self._feature_preset_mode = CONF_PRESET_MODE_COMMAND_TOPIC in config if self._feature_preset_mode: presets = [] @@ -428,10 +524,9 @@ class MqttClimate(MqttEntity, ClimateEntity): self._attr_preset_mode = PRESET_NONE else: self._attr_preset_modes = [] - self._optimistic_preset_mode = CONF_PRESET_MODE_STATE_TOPIC not in config - self._attr_hvac_action = None - - self._attr_is_aux_heat = False + self._optimistic_preset_mode = ( + self._optimistic or CONF_PRESET_MODE_STATE_TOPIC not in config + ) value_templates: dict[str, Template | None] = {} for key in VALUE_TEMPLATE_KEYS: @@ -472,6 +567,9 @@ class MqttClimate(MqttEntity, ClimateEntity): ): support |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + if self._topic[CONF_HUMIDITY_COMMAND_TOPIC] is not None: + support |= ClimateEntityFeature.TARGET_HUMIDITY + if (self._topic[CONF_FAN_MODE_STATE_TOPIC] is not None) or ( self._topic[CONF_FAN_MODE_COMMAND_TOPIC] is not None ): @@ -541,23 +639,23 @@ class MqttClimate(MqttEntity, ClimateEntity): add_subscription(topics, CONF_ACTION_TOPIC, handle_action_received) @callback - def handle_temperature_received( + def handle_climate_attribute_received( msg: ReceiveMessage, template_name: str, attr: str ) -> None: - """Handle temperature coming via MQTT.""" + """Handle climate attributes coming via MQTT.""" payload = render_template(msg, template_name) try: setattr(self, attr, float(payload)) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) except ValueError: - _LOGGER.error("Could not parse temperature from %s", payload) + _LOGGER.error("Could not parse %s from %s", template_name, payload) @callback @log_messages(self.hass, self.entity_id) def handle_current_temperature_received(msg: ReceiveMessage) -> None: """Handle current temperature coming via MQTT.""" - handle_temperature_received( + handle_climate_attribute_received( msg, CONF_CURRENT_TEMP_TEMPLATE, "_attr_current_temperature" ) @@ -569,7 +667,7 @@ class MqttClimate(MqttEntity, ClimateEntity): @log_messages(self.hass, self.entity_id) def handle_target_temperature_received(msg: ReceiveMessage) -> None: """Handle target temperature coming via MQTT.""" - handle_temperature_received( + handle_climate_attribute_received( msg, CONF_TEMP_STATE_TEMPLATE, "_attr_target_temperature" ) @@ -581,7 +679,7 @@ class MqttClimate(MqttEntity, ClimateEntity): @log_messages(self.hass, self.entity_id) def handle_temperature_low_received(msg: ReceiveMessage) -> None: """Handle target temperature low coming via MQTT.""" - handle_temperature_received( + handle_climate_attribute_received( msg, CONF_TEMP_LOW_STATE_TEMPLATE, "_attr_target_temperature_low" ) @@ -593,7 +691,7 @@ class MqttClimate(MqttEntity, ClimateEntity): @log_messages(self.hass, self.entity_id) def handle_temperature_high_received(msg: ReceiveMessage) -> None: """Handle target temperature high coming via MQTT.""" - handle_temperature_received( + handle_climate_attribute_received( msg, CONF_TEMP_HIGH_STATE_TEMPLATE, "_attr_target_temperature_high" ) @@ -601,6 +699,31 @@ class MqttClimate(MqttEntity, ClimateEntity): topics, CONF_TEMP_HIGH_STATE_TOPIC, handle_temperature_high_received ) + @callback + @log_messages(self.hass, self.entity_id) + def handle_current_humidity_received(msg: ReceiveMessage) -> None: + """Handle current humidity coming via MQTT.""" + handle_climate_attribute_received( + msg, CONF_CURRENT_HUMIDITY_TEMPLATE, "_attr_current_humidity" + ) + + add_subscription( + topics, CONF_CURRENT_HUMIDITY_TOPIC, handle_current_humidity_received + ) + + @callback + @log_messages(self.hass, self.entity_id) + def handle_target_humidity_received(msg: ReceiveMessage) -> None: + """Handle target humidity coming via MQTT.""" + + handle_climate_attribute_received( + msg, CONF_HUMIDITY_STATE_TEMPLATE, "_attr_target_humidity" + ) + + add_subscription( + topics, CONF_HUMIDITY_STATE_TOPIC, handle_target_humidity_received + ) + @callback def handle_mode_received( msg: ReceiveMessage, template_name: str, attr: str, mode_list: str @@ -731,21 +854,25 @@ class MqttClimate(MqttEntity, ClimateEntity): self._config[CONF_ENCODING], ) - async def _set_temperature( + async def _set_climate_attribute( self, temp: float | None, cmnd_topic: str, cmnd_template: str, state_topic: str, attr: str, - ) -> None: - if temp is not None: - if self._topic[state_topic] is None: - # optimistic mode - setattr(self, attr, temp) + ) -> bool: + if temp is None: + return False + changed = False + if self._optimistic or self._topic[state_topic] is None: + # optimistic mode + changed = True + setattr(self, attr, temp) - payload = self._command_templates[cmnd_template](temp) - await self._publish(cmnd_topic, payload) + payload = self._command_templates[cmnd_template](temp) + await self._publish(cmnd_topic, payload) + return changed async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperatures.""" @@ -753,7 +880,7 @@ class MqttClimate(MqttEntity, ClimateEntity): if (operation_mode := kwargs.get(ATTR_HVAC_MODE)) is not None: await self.async_set_hvac_mode(operation_mode) - await self._set_temperature( + changed = await self._set_climate_attribute( kwargs.get(ATTR_TEMPERATURE), CONF_TEMP_COMMAND_TOPIC, CONF_TEMP_COMMAND_TEMPLATE, @@ -761,7 +888,7 @@ class MqttClimate(MqttEntity, ClimateEntity): "_attr_target_temperature", ) - await self._set_temperature( + changed |= await self._set_climate_attribute( kwargs.get(ATTR_TARGET_TEMP_LOW), CONF_TEMP_LOW_COMMAND_TOPIC, CONF_TEMP_LOW_COMMAND_TEMPLATE, @@ -769,7 +896,7 @@ class MqttClimate(MqttEntity, ClimateEntity): "_attr_target_temperature_low", ) - await self._set_temperature( + changed |= await self._set_climate_attribute( kwargs.get(ATTR_TARGET_TEMP_HIGH), CONF_TEMP_HIGH_COMMAND_TOPIC, CONF_TEMP_HIGH_COMMAND_TEMPLATE, @@ -777,6 +904,21 @@ class MqttClimate(MqttEntity, ClimateEntity): "_attr_target_temperature_high", ) + if not changed: + return + self.async_write_ha_state() + + async def async_set_humidity(self, humidity: int) -> None: + """Set new target humidity.""" + + await self._set_climate_attribute( + humidity, + CONF_HUMIDITY_COMMAND_TOPIC, + CONF_HUMIDITY_COMMAND_TEMPLATE, + CONF_HUMIDITY_STATE_TOPIC, + "_attr_target_humidity", + ) + self.async_write_ha_state() async def async_set_swing_mode(self, swing_mode: str) -> None: @@ -784,7 +926,7 @@ class MqttClimate(MqttEntity, ClimateEntity): payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE](swing_mode) await self._publish(CONF_SWING_MODE_COMMAND_TOPIC, payload) - if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: + if self._optimistic or self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: self._attr_swing_mode = swing_mode self.async_write_ha_state() @@ -793,7 +935,7 @@ class MqttClimate(MqttEntity, ClimateEntity): payload = self._command_templates[CONF_FAN_MODE_COMMAND_TEMPLATE](fan_mode) await self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload) - if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: + if self._optimistic or self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: self._attr_fan_mode = fan_mode self.async_write_ha_state() @@ -809,7 +951,7 @@ class MqttClimate(MqttEntity, ClimateEntity): payload = self._command_templates[CONF_MODE_COMMAND_TEMPLATE](hvac_mode) await self._publish(CONF_MODE_COMMAND_TOPIC, payload) - if self._topic[CONF_MODE_STATE_TOPIC] is None: + if self._optimistic or self._topic[CONF_MODE_STATE_TOPIC] is None: self._attr_hvac_mode = hvac_mode self.async_write_ha_state() @@ -839,7 +981,7 @@ class MqttClimate(MqttEntity, ClimateEntity): self._config[CONF_PAYLOAD_ON] if state else self._config[CONF_PAYLOAD_OFF], ) - if self._topic[CONF_AUX_STATE_TOPIC] is None: + if self._optimistic or self._topic[CONF_AUX_STATE_TOPIC] is None: self._attr_is_aux_heat = state self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/config.py b/homeassistant/components/mqtt/config.py index 88adcac7194..895a8e3b581 100644 --- a/homeassistant/components/mqtt/config.py +++ b/homeassistant/components/mqtt/config.py @@ -3,7 +3,7 @@ from __future__ import annotations import voluptuous as vol -from homeassistant.const import CONF_VALUE_TEMPLATE +from homeassistant.const import CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE from homeassistant.helpers import config_validation as cv from .const import ( @@ -13,6 +13,7 @@ from .const import ( CONF_RETAIN, CONF_STATE_TOPIC, DEFAULT_ENCODING, + DEFAULT_OPTIMISTIC, DEFAULT_QOS, DEFAULT_RETAIN, ) @@ -37,6 +38,7 @@ MQTT_RO_SCHEMA = MQTT_BASE_SCHEMA.extend( MQTT_RW_SCHEMA = MQTT_BASE_SCHEMA.extend( { vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, } diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index b79ff30f111..e7e16fc684d 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -76,7 +76,6 @@ from .const import ( DEFAULT_WS_PATH, DOMAIN, SUPPORTED_PROTOCOLS, - SUPPORTED_TRANSPORTS, TRANSPORT_TCP, TRANSPORT_WEBSOCKETS, ) @@ -119,6 +118,10 @@ PROTOCOL_SELECTOR = SelectSelector( mode=SelectSelectorMode.DROPDOWN, ) ) +SUPPORTED_TRANSPORTS = [ + SelectOptionDict(value=TRANSPORT_TCP, label="TCP"), + SelectOptionDict(value=TRANSPORT_WEBSOCKETS, label="WebSocket"), +] TRANSPORT_SELECTOR = SelectSelector( SelectSelectorConfig( options=SUPPORTED_TRANSPORTS, @@ -129,14 +132,15 @@ WS_HEADERS_SELECTOR = TextSelector( TextSelectorConfig(type=TextSelectorType.TEXT, multiline=True) ) CA_VERIFICATION_MODES = [ - SelectOptionDict(value="off", label="Off"), - SelectOptionDict(value="auto", label="Auto"), - SelectOptionDict(value="custom", label="Custom"), + "off", + "auto", + "custom", ] BROKER_VERIFICATION_SELECTOR = SelectSelector( SelectSelectorConfig( options=CA_VERIFICATION_MODES, mode=SelectSelectorMode.DROPDOWN, + translation_key=SET_CA_CERT, ) ) @@ -155,7 +159,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - _hassio_discovery = None + _hassio_discovery: dict[str, Any] | None = None @staticmethod @callback @@ -589,7 +593,8 @@ async def async_get_broker_settings( current_user = current_config.get(CONF_USERNAME) current_pass = current_config.get(CONF_PASSWORD) - # Treat the previous post as an update of the current settings (if there was a basic broker setup step) + # Treat the previous post as an update of the current settings + # (if there was a basic broker setup step) current_config.update(user_input_basic) # Get default settings for advanced broker options diff --git a/homeassistant/components/mqtt/config_integration.py b/homeassistant/components/mqtt/config_integration.py index 4140c5963ca..bbd6861435b 100644 --- a/homeassistant/components/mqtt/config_integration.py +++ b/homeassistant/components/mqtt/config_integration.py @@ -81,64 +81,84 @@ DEFAULT_VALUES = { PLATFORM_CONFIG_SCHEMA_BASE = vol.Schema( { Platform.ALARM_CONTROL_PANEL.value: vol.All( - cv.ensure_list, [alarm_control_panel_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [alarm_control_panel_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] # noqa: E501 ), Platform.BINARY_SENSOR.value: vol.All( - cv.ensure_list, [binary_sensor_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [binary_sensor_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.BUTTON.value: vol.All( - cv.ensure_list, [button_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [button_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.CAMERA.value: vol.All( - cv.ensure_list, [camera_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [camera_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.CLIMATE.value: vol.All( - cv.ensure_list, [climate_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [climate_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.COVER.value: vol.All( - cv.ensure_list, [cover_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [cover_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.DEVICE_TRACKER.value: vol.All( - cv.ensure_list, [device_tracker_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [device_tracker_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.FAN.value: vol.All( - cv.ensure_list, [fan_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [fan_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.HUMIDIFIER.value: vol.All( - cv.ensure_list, [humidifier_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [humidifier_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.LOCK.value: vol.All( - cv.ensure_list, [lock_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [lock_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.LIGHT.value: vol.All( - cv.ensure_list, [light_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [light_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.NUMBER.value: vol.All( - cv.ensure_list, [number_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [number_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.SCENE.value: vol.All( - cv.ensure_list, [scene_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [scene_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.SELECT.value: vol.All( - cv.ensure_list, [select_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [select_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.SENSOR.value: vol.All( - cv.ensure_list, [sensor_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [sensor_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.SIREN.value: vol.All( - cv.ensure_list, [siren_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [siren_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.SWITCH.value: vol.All( - cv.ensure_list, [switch_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [switch_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.TEXT.value: vol.All( - cv.ensure_list, [text_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [text_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.UPDATE.value: vol.All( - cv.ensure_list, [update_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [update_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), Platform.VACUUM.value: vol.All( - cv.ensure_list, [vacuum_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type] + cv.ensure_list, + [vacuum_platform.PLATFORM_SCHEMA_MODERN], # type: ignore[has-type] ), } ) diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 0cb81dc9f74..f7e2cbe5b1b 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -41,6 +41,7 @@ DEFAULT_PREFIX = "homeassistant" DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status" DEFAULT_DISCOVERY = True DEFAULT_ENCODING = "utf-8" +DEFAULT_OPTIMISTIC = False DEFAULT_QOS = 0 DEFAULT_PAYLOAD_AVAILABLE = "online" DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline" @@ -55,7 +56,6 @@ SUPPORTED_PROTOCOLS = [PROTOCOL_31, PROTOCOL_311, PROTOCOL_5] TRANSPORT_TCP = "tcp" TRANSPORT_WEBSOCKETS = "websockets" -SUPPORTED_TRANSPORTS = [TRANSPORT_TCP, TRANSPORT_WEBSOCKETS] DEFAULT_PORT = 1883 DEFAULT_KEEPALIVE = 60 diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index e13b704436f..66b8e60b561 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -42,6 +42,7 @@ from .const import ( CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, + DEFAULT_OPTIMISTIC, ) from .debug_info import log_messages from .mixins import ( @@ -84,7 +85,6 @@ TILT_PAYLOAD = "tilt" COVER_PAYLOAD = "cover" DEFAULT_NAME = "MQTT Cover" -DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_CLOSE = "CLOSE" DEFAULT_PAYLOAD_OPEN = "OPEN" DEFAULT_PAYLOAD_STOP = "STOP" @@ -227,7 +227,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT cover through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT cover through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) @@ -656,7 +656,8 @@ class MqttCover(MqttEntity, CoverEntity): tilt = kwargs[ATTR_TILT_POSITION] percentage_tilt = tilt tilt = self.find_in_range_from_percent(tilt) - # Handover the tilt after calculated from percent would make it more consistent with receiving templates + # Handover the tilt after calculated from percent would make it more + # consistent with receiving templates variables = { "tilt_position": percentage_tilt, "entity_id": self.entity_id, diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker.py index 92f213f4bdf..b55c3754696 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -61,7 +61,8 @@ PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend( DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) -# Configuring MQTT Device Trackers under the device_tracker platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Device Trackers under the device_tracker platform key was deprecated +# in HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All(warn_for_legacy_schema(device_tracker.DOMAIN)) @@ -71,7 +72,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT device_tracker through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT device_tracker through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index b6104a570d4..3accb7c8ade 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -198,7 +198,7 @@ async def async_start( # noqa: C901 if discovery_hash in mqtt_data.discovery_pending_discovered: pending = mqtt_data.discovery_pending_discovered[discovery_hash]["pending"] pending.appendleft(discovery_payload) - _LOGGER.info( + _LOGGER.debug( "Component has already been discovered: %s %s, queuing update", component, discovery_id, diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 82fd0d4063a..74290abb757 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -88,7 +88,6 @@ DEFAULT_NAME = "MQTT Fan" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_RESET = "None" -DEFAULT_OPTIMISTIC = False DEFAULT_SPEED_RANGE_MIN = 1 DEFAULT_SPEED_RANGE_MAX = 100 @@ -128,7 +127,6 @@ def valid_preset_mode_configuration(config: ConfigType) -> ConfigType: _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_OSCILLATION_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_OSCILLATION_COMMAND_TEMPLATE): cv.template, @@ -138,7 +136,8 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_PERCENTAGE_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_PERCENTAGE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_PERCENTAGE_VALUE_TEMPLATE): cv.template, - # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST must be used together + # CONF_PRESET_MODE_COMMAND_TOPIC and CONF_PRESET_MODES_LIST + # must be used together vol.Inclusive( CONF_PRESET_MODE_COMMAND_TOPIC, "preset_modes" ): valid_publish_topic, @@ -196,7 +195,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT fan through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 38d3e46ea45..93069791a79 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -76,7 +76,6 @@ CONF_TARGET_HUMIDITY_STATE_TEMPLATE = "target_humidity_state_template" CONF_TARGET_HUMIDITY_STATE_TOPIC = "target_humidity_state_topic" DEFAULT_NAME = "MQTT Humidifier" -DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_RESET = "None" @@ -102,7 +101,11 @@ def valid_mode_configuration(config: ConfigType) -> ConfigType: def valid_humidity_range_configuration(config: ConfigType) -> ConfigType: - """Validate that the target_humidity range configuration is valid, throws if it isn't.""" + """Validate humidity range. + + Ensures that the target_humidity range configuration is valid, + throws if it isn't. + """ if config[CONF_TARGET_HUMIDITY_MIN] >= config[CONF_TARGET_HUMIDITY_MAX]: raise ValueError("target_humidity_max must be > target_humidity_min") if config[CONF_TARGET_HUMIDITY_MAX] > 100: @@ -128,7 +131,6 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_MODE_STATE_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_VALUE_TEMPLATE): cv.template, @@ -149,7 +151,8 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Humidifiers under the humidifier platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Humidifiers under the humidifier platform key was deprecated in +# HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(humidifier.DOMAIN), @@ -173,7 +176,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT humidifier through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT humidifier through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index d2f8a5ac03e..2d27d86e9e9 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -88,6 +88,7 @@ CONF_EFFECT_COMMAND_TOPIC = "effect_command_topic" CONF_EFFECT_LIST = "effect_list" CONF_EFFECT_STATE_TOPIC = "effect_state_topic" CONF_EFFECT_VALUE_TEMPLATE = "effect_value_template" +CONF_HS_COMMAND_TEMPLATE = "hs_command_template" CONF_HS_COMMAND_TOPIC = "hs_command_topic" CONF_HS_STATE_TOPIC = "hs_state_topic" CONF_HS_VALUE_TEMPLATE = "hs_value_template" @@ -105,6 +106,7 @@ CONF_RGBWW_COMMAND_TEMPLATE = "rgbww_command_template" CONF_RGBWW_COMMAND_TOPIC = "rgbww_command_topic" CONF_RGBWW_STATE_TOPIC = "rgbww_state_topic" CONF_RGBWW_VALUE_TEMPLATE = "rgbww_value_template" +CONF_XY_COMMAND_TEMPLATE = "xy_command_template" CONF_XY_COMMAND_TOPIC = "xy_command_topic" CONF_XY_STATE_TOPIC = "xy_state_topic" CONF_XY_VALUE_TEMPLATE = "xy_value_template" @@ -136,7 +138,6 @@ MQTT_LIGHT_ATTRIBUTES_BLOCKED = frozenset( DEFAULT_BRIGHTNESS_SCALE = 255 DEFAULT_NAME = "MQTT LightEntity" -DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_WHITE_SCALE = 255 @@ -148,9 +149,11 @@ COMMAND_TEMPLATE_KEYS = [ CONF_BRIGHTNESS_COMMAND_TEMPLATE, CONF_COLOR_TEMP_COMMAND_TEMPLATE, CONF_EFFECT_COMMAND_TEMPLATE, + CONF_HS_COMMAND_TEMPLATE, CONF_RGB_COMMAND_TEMPLATE, CONF_RGBW_COMMAND_TEMPLATE, CONF_RGBWW_COMMAND_TEMPLATE, + CONF_XY_COMMAND_TEMPLATE, ] VALUE_TEMPLATE_KEYS = [ CONF_BRIGHTNESS_VALUE_TEMPLATE, @@ -186,6 +189,7 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_EFFECT_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_EFFECT_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_HS_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_HS_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_HS_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_HS_VALUE_TEMPLATE): cv.template, @@ -195,7 +199,6 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_ON_COMMAND_TYPE, default=DEFAULT_ON_COMMAND_TYPE): vol.In( VALUES_ON_COMMAND_TYPE ), - 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_RGB_COMMAND_TEMPLATE): cv.template, @@ -215,6 +218,7 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_WHITE_SCALE, default=DEFAULT_WHITE_SCALE): vol.All( vol.Coerce(int), vol.Range(min=1) ), + vol.Optional(CONF_XY_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_XY_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_XY_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_XY_VALUE_TEMPLATE): cv.template, @@ -623,7 +627,7 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): self._attr_hs_color = cast(tuple[float, float], hs_color) get_mqtt_data(self.hass).state_write_requests.write_state_request(self) except ValueError: - _LOGGER.debug("Failed to parse hs state update: '%s'", payload) + _LOGGER.warning("Failed to parse hs state update: '%s'", payload) add_topic(CONF_HS_STATE_TOPIC, hs_received) @@ -765,7 +769,11 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): hs_color: str | None = kwargs.get(ATTR_HS_COLOR) if hs_color and self._topic[CONF_HS_COMMAND_TOPIC] is not None: - await publish(CONF_HS_COMMAND_TOPIC, f"{hs_color[0]},{hs_color[1]}") + device_hs_payload = self._command_templates[CONF_HS_COMMAND_TEMPLATE]( + f"{hs_color[0]},{hs_color[1]}", + {"hue": hs_color[0], "sat": hs_color[1]}, + ) + await publish(CONF_HS_COMMAND_TOPIC, device_hs_payload) should_update |= set_optimistic(ATTR_HS_COLOR, hs_color, ColorMode.HS) rgb: tuple[int, int, int] | None @@ -799,7 +807,11 @@ class MqttLight(MqttEntity, LightEntity, RestoreEntity): if (xy_color := kwargs.get(ATTR_XY_COLOR)) and self._topic[ CONF_XY_COMMAND_TOPIC ] is not None: - await publish(CONF_XY_COMMAND_TOPIC, f"{xy_color[0]},{xy_color[1]}") + device_xy_payload = self._command_templates[CONF_XY_COMMAND_TEMPLATE]( + f"{xy_color[0]},{xy_color[1]}", + {"x": xy_color[0], "y": xy_color[1]}, + ) + await publish(CONF_XY_COMMAND_TOPIC, device_xy_payload) should_update |= set_optimistic(ATTR_XY_COLOR, xy_color, ColorMode.XY) if ( diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 09413e1f0ac..0ba523c73f6 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -83,7 +83,6 @@ DEFAULT_EFFECT = False DEFAULT_FLASH_TIME_LONG = 10 DEFAULT_FLASH_TIME_SHORT = 2 DEFAULT_NAME = "MQTT JSON Light" -DEFAULT_OPTIMISTIC = False DEFAULT_RGB = False DEFAULT_XY = False DEFAULT_HS = False @@ -135,7 +134,6 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_MAX_MIREDS): cv.positive_int, vol.Optional(CONF_MIN_MIREDS): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All( vol.Coerce(int), vol.In([0, 1, 2]) ), diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 21691acc916..654ca205a65 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -63,7 +63,6 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "mqtt_template" DEFAULT_NAME = "MQTT Template Light" -DEFAULT_OPTIMISTIC = False CONF_BLUE_TEMPLATE = "blue_template" CONF_BRIGHTNESS_TEMPLATE = "brightness_template" @@ -103,7 +102,6 @@ _PLATFORM_SCHEMA_BASE = ( vol.Optional(CONF_MAX_MIREDS): cv.positive_int, vol.Optional(CONF_MIN_MIREDS): cv.positive_int, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_RED_TEMPLATE): cv.template, vol.Optional(CONF_STATE_TEMPLATE): cv.template, } diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index b956f2e1b88..0598c0354ed 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable import functools +import re from typing import Any import voluptuous as vol @@ -10,15 +11,21 @@ import voluptuous as vol from homeassistant.components import lock from homeassistant.components.lock import LockEntity, LockEntityFeature from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE +from homeassistant.const import ( + ATTR_CODE, + CONF_NAME, + CONF_OPTIMISTIC, + CONF_VALUE_TEMPLATE, +) 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 homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, TemplateVarsType from . import subscription from .config import MQTT_RW_SCHEMA from .const import ( + CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, CONF_ENCODING, CONF_QOS, @@ -32,23 +39,36 @@ from .mixins import ( async_setup_entry_helper, warn_for_legacy_schema, ) -from .models import MqttValueTemplate, ReceiveMessage, ReceivePayloadType +from .models import ( + MqttCommandTemplate, + MqttValueTemplate, + PublishPayloadType, + ReceiveMessage, + ReceivePayloadType, +) from .util import get_mqtt_data +CONF_CODE_FORMAT = "code_format" + CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_UNLOCK = "payload_unlock" CONF_PAYLOAD_OPEN = "payload_open" CONF_STATE_LOCKED = "state_locked" +CONF_STATE_LOCKING = "state_locking" CONF_STATE_UNLOCKED = "state_unlocked" +CONF_STATE_UNLOCKING = "state_unlocking" +CONF_STATE_JAMMED = "state_jammed" DEFAULT_NAME = "MQTT Lock" -DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_LOCK = "LOCK" DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" DEFAULT_PAYLOAD_OPEN = "OPEN" DEFAULT_STATE_LOCKED = "LOCKED" +DEFAULT_STATE_LOCKING = "LOCKING" DEFAULT_STATE_UNLOCKED = "UNLOCKED" +DEFAULT_STATE_UNLOCKING = "UNLOCKING" +DEFAULT_STATE_JAMMED = "JAMMED" MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset( { @@ -59,13 +79,17 @@ MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset( PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { + vol.Optional(CONF_CODE_FORMAT): cv.is_regex, + vol.Optional(CONF_COMMAND_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_LOCK, default=DEFAULT_PAYLOAD_LOCK): cv.string, vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): cv.string, vol.Optional(CONF_PAYLOAD_OPEN): cv.string, + vol.Optional(CONF_STATE_JAMMED, default=DEFAULT_STATE_JAMMED): cv.string, vol.Optional(CONF_STATE_LOCKED, default=DEFAULT_STATE_LOCKED): cv.string, + vol.Optional(CONF_STATE_LOCKING, default=DEFAULT_STATE_LOCKING): cv.string, vol.Optional(CONF_STATE_UNLOCKED, default=DEFAULT_STATE_UNLOCKED): cv.string, + vol.Optional(CONF_STATE_UNLOCKING, default=DEFAULT_STATE_UNLOCKING): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) @@ -78,13 +102,21 @@ PLATFORM_SCHEMA = vol.All( DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.REMOVE_EXTRA) +STATE_CONFIG_KEYS = [ + CONF_STATE_JAMMED, + CONF_STATE_LOCKED, + CONF_STATE_LOCKING, + CONF_STATE_UNLOCKED, + CONF_STATE_UNLOCKING, +] + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT lock through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT lock through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) @@ -108,7 +140,12 @@ class MqttLock(MqttEntity, LockEntity): _entity_id_format = lock.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LOCK_ATTRIBUTES_BLOCKED + _compiled_pattern: re.Pattern[Any] | None _optimistic: bool + _valid_states: list[str] + _command_template: Callable[ + [PublishPayloadType, TemplateVarsType], PublishPayloadType + ] _value_template: Callable[[ReceivePayloadType], ReceivePayloadType] def __init__( @@ -129,7 +166,18 @@ class MqttLock(MqttEntity, LockEntity): def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" - self._optimistic = config[CONF_OPTIMISTIC] + self._optimistic = ( + config[CONF_OPTIMISTIC] or self._config.get(CONF_STATE_TOPIC) is None + ) + + self._compiled_pattern = config.get(CONF_CODE_FORMAT) + self._attr_code_format = ( + self._compiled_pattern.pattern if self._compiled_pattern else None + ) + + self._command_template = MqttCommandTemplate( + config.get(CONF_COMMAND_TEMPLATE), entity=self + ).async_render self._value_template = MqttValueTemplate( config.get(CONF_VALUE_TEMPLATE), @@ -140,18 +188,25 @@ class MqttLock(MqttEntity, LockEntity): if CONF_PAYLOAD_OPEN in config: self._attr_supported_features |= LockEntityFeature.OPEN + self._valid_states = [config[state] for state in STATE_CONFIG_KEYS] + def _prepare_subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" + topics: dict[str, dict[str, Any]] = {} + qos: int = self._config[CONF_QOS] + encoding: str | None = self._config[CONF_ENCODING] or None + @callback @log_messages(self.hass, self.entity_id) def message_received(msg: ReceiveMessage) -> None: - """Handle new MQTT messages.""" + """Handle new lock state messages.""" payload = self._value_template(msg.payload) - if payload == self._config[CONF_STATE_LOCKED]: - self._attr_is_locked = True - elif payload == self._config[CONF_STATE_UNLOCKED]: - self._attr_is_locked = False + if payload in self._valid_states: + self._attr_is_locked = payload == self._config[CONF_STATE_LOCKED] + self._attr_is_locking = payload == self._config[CONF_STATE_LOCKING] + self._attr_is_unlocking = payload == self._config[CONF_STATE_UNLOCKING] + self._attr_is_jammed = payload == self._config[CONF_STATE_JAMMED] get_mqtt_data(self.hass).state_write_requests.write_state_request(self) @@ -159,18 +214,18 @@ class MqttLock(MqttEntity, LockEntity): # Force into optimistic mode. self._optimistic = True else: - self._sub_state = subscription.async_prepare_subscribe_topics( - self.hass, - self._sub_state, - { - "state_topic": { - "topic": self._config.get(CONF_STATE_TOPIC), - "msg_callback": message_received, - "qos": self._config[CONF_QOS], - "encoding": self._config[CONF_ENCODING] or None, - } - }, - ) + topics[CONF_STATE_TOPIC] = { + "topic": self._config.get(CONF_STATE_TOPIC), + "msg_callback": message_received, + CONF_QOS: qos, + CONF_ENCODING: encoding, + } + + self._sub_state = subscription.async_prepare_subscribe_topics( + self.hass, + self._sub_state, + topics, + ) async def _subscribe_topics(self) -> None: """(Re)Subscribe to topics.""" @@ -186,9 +241,13 @@ class MqttLock(MqttEntity, LockEntity): This method is a coroutine. """ + tpl_vars: TemplateVarsType = { + ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None + } + payload = self._command_template(self._config[CONF_PAYLOAD_LOCK], tpl_vars) await self.async_publish( self._config[CONF_COMMAND_TOPIC], - self._config[CONF_PAYLOAD_LOCK], + payload, self._config[CONF_QOS], self._config[CONF_RETAIN], self._config[CONF_ENCODING], @@ -203,9 +262,13 @@ class MqttLock(MqttEntity, LockEntity): This method is a coroutine. """ + tpl_vars: TemplateVarsType = { + ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None + } + payload = self._command_template(self._config[CONF_PAYLOAD_UNLOCK], tpl_vars) await self.async_publish( self._config[CONF_COMMAND_TOPIC], - self._config[CONF_PAYLOAD_UNLOCK], + payload, self._config[CONF_QOS], self._config[CONF_RETAIN], self._config[CONF_ENCODING], @@ -220,9 +283,13 @@ class MqttLock(MqttEntity, LockEntity): This method is a coroutine. """ + tpl_vars: TemplateVarsType = { + ATTR_CODE: kwargs.get(ATTR_CODE) if kwargs else None + } + payload = self._command_template(self._config[CONF_PAYLOAD_OPEN], tpl_vars) await self.async_publish( self._config[CONF_COMMAND_TOPIC], - self._config[CONF_PAYLOAD_OPEN], + payload, self._config[CONF_QOS], self._config[CONF_RETAIN], self._config[CONF_ENCODING], diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 1e6e9989577..e90301875e4 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -264,7 +264,10 @@ def warn_for_legacy_schema(domain: str) -> Callable[[ConfigType], ConfigType]: severity=IssueSeverity.ERROR, translation_key="deprecated_yaml", translation_placeholders={ - "more_info_url": f"https://www.home-assistant.io/integrations/{domain}.mqtt/#new_format", + "more_info_url": ( + "https://www.home-assistant.io" + f"/integrations/{domain}.mqtt/#new_format" + ), "platform": domain, }, ) @@ -600,7 +603,11 @@ class MqttAvailability(Entity): async def cleanup_device_registry( hass: HomeAssistant, device_id: str | None, config_entry_id: str | None ) -> None: - """Remove MQTT from the device registry entry if there are no remaining entities, triggers or tags.""" + """Clean up the device registry after MQTT removal. + + Remove MQTT from the device registry entry if there are no remaining + entities, triggers or tags. + """ # Local import to avoid circular dependencies # pylint: disable-next=import-outside-toplevel from . import device_trigger, tag @@ -649,7 +656,11 @@ def stop_discovery_updates( async def async_remove_discovery_payload( hass: HomeAssistant, discovery_data: DiscoveryInfoType ) -> None: - """Clear retained discovery topic in broker to avoid rediscovery after a restart of HA.""" + """Clear retained discovery payload. + + Remove discovery topic in broker to avoid rediscovery + after a restart of Home Assistant. + """ discovery_topic = discovery_data[ATTR_DISCOVERY_TOPIC] await async_publish(hass, discovery_topic, "", retain=True) @@ -733,8 +744,12 @@ class MqttDiscoveryDeviceUpdate(ABC): self.log_name, discovery_hash, ) - await self.async_update(discovery_payload) - if not discovery_payload: + try: + await self.async_update(discovery_payload) + finally: + send_discovery_done(self.hass, self._discovery_data) + self._discovery_data[ATTR_DISCOVERY_PAYLOAD] = discovery_payload + elif not discovery_payload: # Unregister and clean up the current discovery instance stop_discovery_updates( self.hass, self._discovery_data, self._remove_discovery_updated @@ -829,8 +844,9 @@ class MqttDiscoveryUpdate(Entity): ) -> None: """Remove entity's state and entity registry entry. - Remove entity from entity registry if it is registered, this also removes the state. - If the entity is not in the entity registry, just remove the state. + 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 = er.async_get(self.hass) if entity_entry := entity_registry.async_get(self.entity_id): @@ -843,7 +859,7 @@ class MqttDiscoveryUpdate(Entity): async def discovery_callback(payload: MQTTDiscoveryPayload) -> None: """Handle discovery update.""" - _LOGGER.info( + _LOGGER.debug( "Got update for entity with hash: %s '%s'", discovery_hash, payload, @@ -857,22 +873,27 @@ class MqttDiscoveryUpdate(Entity): _LOGGER.info("Removing component: %s", self.entity_id) self._cleanup_discovery_on_remove() await _async_remove_state_and_registry_entry(self) + send_discovery_done(self.hass, self._discovery_data) elif self._discovery_update: if old_payload != self._discovery_data[ATTR_DISCOVERY_PAYLOAD]: # Non-empty, changed payload: Notify component _LOGGER.info("Updating component: %s", self.entity_id) - await self._discovery_update(payload) + try: + await self._discovery_update(payload) + finally: + send_discovery_done(self.hass, self._discovery_data) else: # Non-empty, unchanged payload: Ignore to avoid changing states - _LOGGER.info("Ignoring unchanged update for: %s", self.entity_id) - send_discovery_done(self.hass, self._discovery_data) + _LOGGER.debug("Ignoring unchanged update for: %s", self.entity_id) + send_discovery_done(self.hass, self._discovery_data) if discovery_hash: assert self._discovery_data is not None debug_info.add_entity_discovery_data( self.hass, self._discovery_data, self.entity_id ) - # Set in case the entity has been removed and is re-added, for example when changing entity_id + # Set in case the entity has been removed and is re-added, + # for example when changing entity_id set_discovery_hash(self.hass, discovery_hash) self._remove_discovery_updated = async_dispatcher_connect( self.hass, @@ -883,11 +904,12 @@ class MqttDiscoveryUpdate(Entity): async def async_removed_from_registry(self) -> None: """Clear retained discovery topic in broker.""" if not self._removed_from_hass and self._discovery_data is not None: - # Stop subscribing to discovery updates to not trigger when we clear the - # discovery topic + # Stop subscribing to discovery updates to not trigger when we + # clear the discovery topic self._cleanup_discovery_on_remove() - # Clear the discovery topic so the entity is not rediscovered after a restart + # Clear the discovery topic so the entity is not + # rediscovered after a restart await async_remove_discovery_payload(self.hass, self._discovery_data) @callback diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 408e455365e..a88fb97b833 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -8,7 +8,7 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass, field import datetime as dt import logging -from typing import TYPE_CHECKING, Any, TypedDict, Union +from typing import TYPE_CHECKING, Any, TypedDict import attr @@ -39,7 +39,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_THIS = "this" -PublishPayloadType = Union[str, bytes, int, float, None] +PublishPayloadType = str | bytes | int | float | None @attr.s(slots=True, frozen=True) diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 6ec26e9a904..3682b19cf4e 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -64,7 +64,6 @@ CONF_MAX = "max" CONF_STEP = "step" DEFAULT_NAME = "MQTT Number" -DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_RESET = "None" MQTT_NUMBER_ATTRIBUTES_BLOCKED = frozenset( @@ -92,7 +91,6 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): vol.Coerce(float), vol.Optional(CONF_MODE, default=NumberMode.AUTO): vol.Coerce(NumberMode), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_RESET, default=DEFAULT_PAYLOAD_RESET): cv.string, vol.Optional(CONF_STEP, default=DEFAULT_STEP): vol.All( vol.Coerce(float), vol.Range(min=1e-3) @@ -124,7 +122,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT number through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT number through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 3454102e5e0..dd7f3347845 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -40,7 +40,8 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend( vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, vol.Optional(CONF_OBJECT_ID): cv.string, - # CONF_ENABLED_BY_DEFAULT is not added by default because we are not using the common schema here + # CONF_ENABLED_BY_DEFAULT is not added by default because + # we are not using the common schema here vol.Optional(CONF_ENABLED_BY_DEFAULT, default=True): cv.boolean, } ).extend(MQTT_AVAILABILITY_SCHEMA.schema) @@ -59,7 +60,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT scene through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT scene through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index ea8a20d1db5..b783a001f15 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -48,7 +48,6 @@ _LOGGER = logging.getLogger(__name__) CONF_OPTIONS = "options" DEFAULT_NAME = "MQTT Select" -DEFAULT_OPTIMISTIC = False MQTT_SELECT_ATTRIBUTES_BLOCKED = frozenset( { @@ -61,7 +60,6 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Required(CONF_OPTIONS): cv.ensure_list, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }, @@ -80,7 +78,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT select through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT select through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index dbb414921b5..33c1b9d9fb8 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -114,7 +114,8 @@ PLATFORM_SCHEMA_MODERN = vol.All( validate_options, ) -# Configuring MQTT Sensors under the sensor platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Sensors under the sensor platform key was deprecated in +# HA Core 2022.6 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(sensor.DOMAIN), ) @@ -131,7 +132,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT sensor through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT sensor through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) @@ -153,7 +154,7 @@ class MqttSensor(MqttEntity, RestoreSensor): """Representation of a sensor that can be updated using MQTT.""" _entity_id_format = ENTITY_ID_FORMAT - _attr_last_reset = None + _attr_last_reset: datetime | None = None _attributes_extra_blocked = MQTT_SENSOR_ATTRIBUTES_BLOCKED _expire_after: int | None _expired: bool | None @@ -248,7 +249,8 @@ class MqttSensor(MqttEntity, RestoreSensor): def _update_state(msg: ReceiveMessage) -> None: # auto-expire enabled? if self._expire_after is not None and self._expire_after > 0: - # When self._expire_after is set, and we receive a message, assume device is not expired since it has to be to receive the message + # When self._expire_after is set, and we receive a message, assume + # device is not expired since it has to be to receive the message self._expired = False # Reset old trigger diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index 6043773d5d6..b1ec05aefa3 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -67,7 +67,6 @@ from .util import get_mqtt_data DEFAULT_NAME = "MQTT Siren" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" -DEFAULT_OPTIMISTIC = False ENTITY_ID_FORMAT = siren.DOMAIN + ".{}" @@ -86,7 +85,6 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( 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, @@ -130,7 +128,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT siren through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT siren through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) @@ -388,4 +386,6 @@ class MqttSiren(MqttEntity, SirenEntity): """Update the extra siren state attributes.""" for attribute, support in SUPPORTED_ATTRIBUTES.items(): if self._attr_supported_features & support and attribute in data: - self._attr_extra_state_attributes[attribute] = data[attribute] # type: ignore[literal-required] + self._attr_extra_state_attributes[attribute] = data[ + attribute # type: ignore[literal-required] + ] diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 0ef5ea29068..b55fa5779b8 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -136,5 +136,14 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_inclusion": "[%key:component::mqtt::config::error::invalid_inclusion%]" } + }, + "selector": { + "set_ca_cert": { + "options": { + "off": "Off", + "auto": "Auto", + "custom": "Custom" + } + } } } diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 7c06a587c1d..521b08d2748 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -49,14 +49,12 @@ from .util import get_mqtt_data DEFAULT_NAME = "MQTT Switch" DEFAULT_PAYLOAD_ON = "ON" DEFAULT_PAYLOAD_OFF = "OFF" -DEFAULT_OPTIMISTIC = False CONF_STATE_ON = "state_on" CONF_STATE_OFF = "state_off" PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { 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, @@ -66,7 +64,8 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( } ).extend(MQTT_ENTITY_COMMON_SCHEMA.schema) -# Configuring MQTT Switches under the switch platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Switches under the switch platform key was deprecated in +# HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(switch.DOMAIN), @@ -80,7 +79,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT switch through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT switch through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/text.py b/homeassistant/components/mqtt/text.py index 032dba66719..cc05a2d8db1 100644 --- a/homeassistant/components/mqtt/text.py +++ b/homeassistant/components/mqtt/text.py @@ -52,7 +52,6 @@ CONF_MIN = "min" CONF_PATTERN = "pattern" DEFAULT_NAME = "MQTT Text" -DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_RESET = "None" MQTT_TEXT_ATTRIBUTES_BLOCKED = frozenset( @@ -84,7 +83,6 @@ _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( vol.Optional(CONF_MODE, default=text.TextMode.TEXT): vol.In( [text.TextMode.TEXT, text.TextMode.PASSWORD] ), - vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PATTERN): cv.is_regex, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }, @@ -104,7 +102,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT text through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT text through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/translations/bg.json b/homeassistant/components/mqtt/translations/bg.json index b1d5f535a67..996ac09b740 100644 --- a/homeassistant/components/mqtt/translations/bg.json +++ b/homeassistant/components/mqtt/translations/bg.json @@ -74,5 +74,14 @@ } } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "\u0410\u0432\u0442\u043e", + "custom": "\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d", + "off": "\u0418\u0437\u043a\u043b." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/ca.json b/homeassistant/components/mqtt/translations/ca.json index 0878737057b..84d6461ae23 100644 --- a/homeassistant/components/mqtt/translations/ca.json +++ b/homeassistant/components/mqtt/translations/ca.json @@ -136,5 +136,14 @@ "title": "Opcions d'MQTT" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Autom\u00e0tic", + "custom": "Personalitzat", + "off": "OFF" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json index 677bf5f4038..21cbba8984f 100644 --- a/homeassistant/components/mqtt/translations/de.json +++ b/homeassistant/components/mqtt/translations/de.json @@ -136,5 +136,14 @@ "title": "MQTT-Optionen" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Auto", + "custom": "Benutzerdefiniert", + "off": "Aus" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/el.json b/homeassistant/components/mqtt/translations/el.json index d04488af9dd..69befb6dbf3 100644 --- a/homeassistant/components/mqtt/translations/el.json +++ b/homeassistant/components/mqtt/translations/el.json @@ -8,7 +8,7 @@ "bad_birth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b8\u03ad\u03bc\u03b1 \u03b3\u03ad\u03bd\u03bd\u03b7\u03c3\u03b7\u03c2", "bad_certificate": "\u03a4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc CA \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf", "bad_client_cert": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7, \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03bc\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 PEM", - "bad_client_cert_key": "\u03a4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf \u03b9\u03b4\u03b9\u03c9\u03c4\u03b9\u03ba\u03cc \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b6\u03b5\u03cd\u03b3\u03bf\u03c2", + "bad_client_cert_key": "\u03a4\u03bf \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b7\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03c4\u03bf \u03b9\u03b4\u03b9\u03c9\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b6\u03b5\u03cd\u03b3\u03bf\u03c2", "bad_client_key": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b9\u03b4\u03b9\u03c9\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af, \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03ad\u03bd\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf \u03bc\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 PEM \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03c7\u03c9\u03c1\u03af\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "bad_discovery_prefix": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7\u03c2", "bad_will": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf \u03b8\u03ad\u03bc\u03b1", @@ -132,9 +132,18 @@ "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.", + "description": "Discovery - \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), \u03bf \u0392\u03bf\u03b7\u03b8\u03cc\u03c2 Home \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\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd \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.\n \u03a0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 Discovery - \u03a4\u03bf \u03c0\u03c1\u03cc\u03b8\u03b5\u03bc\u03b1 \u03bc\u03b5 \u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03be\u03b5\u03ba\u03b9\u03bd\u03ac \u03ad\u03bd\u03b1 \u03b8\u03ad\u03bc\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03b1\u03ba\u03ac\u03bb\u03c5\u03c8\u03b7.\n \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 \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.\n \u039c\u03ae\u03bd\u03c5\u03bc\u03b1 \u03b8\u03ad\u03bb\u03b7\u03c3\u03b7\u03c2 - \u03a4\u03bf \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1 \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\u03b9\u03c3\u03bc\u03bf\u03cd (\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 \u03b1\u03ba\u03ac\u03b8\u03b1\u03c1\u03c4\u03bf\u03c5 (\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\u03ae\u03c2 \u03c4\u03bf\u03c5 \u03c3\u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03bf) \u03b1\u03c0\u03bf\u03c3\u03c5\u03bd\u03b4\u03ad\u03c9.", "title": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 MQTT" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "\u0391\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf", + "custom": "\u03a0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03c3\u03bc\u03ad\u03bd\u03bf", + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/en.json b/homeassistant/components/mqtt/translations/en.json index 1f092dfdc96..e8faf814a88 100644 --- a/homeassistant/components/mqtt/translations/en.json +++ b/homeassistant/components/mqtt/translations/en.json @@ -136,5 +136,14 @@ "title": "MQTT options" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Auto", + "custom": "Custom", + "off": "Off" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/es-419.json b/homeassistant/components/mqtt/translations/es-419.json index 9cccbf8658b..5f620b34482 100644 --- a/homeassistant/components/mqtt/translations/es-419.json +++ b/homeassistant/components/mqtt/translations/es-419.json @@ -46,5 +46,12 @@ "button_short_release": "\"{subtype}\" soltado", "button_triple_press": "\"{subtype}\" pulsado 3 veces" } + }, + "selector": { + "set_ca_cert": { + "options": { + "off": "Apagado" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/es.json b/homeassistant/components/mqtt/translations/es.json index ea3753b3bd7..7f9f5d818cf 100644 --- a/homeassistant/components/mqtt/translations/es.json +++ b/homeassistant/components/mqtt/translations/es.json @@ -136,5 +136,14 @@ "title": "Opciones de MQTT" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Autom\u00e1tico", + "custom": "Personalizado", + "off": "Apagado" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/et.json b/homeassistant/components/mqtt/translations/et.json index fad3910f91b..72b62318f15 100644 --- a/homeassistant/components/mqtt/translations/et.json +++ b/homeassistant/components/mqtt/translations/et.json @@ -136,5 +136,14 @@ "title": "MQTT valikud" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Automaatne", + "custom": "Kohandatud", + "off": "V\u00e4ljas" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/he.json b/homeassistant/components/mqtt/translations/he.json index 6355a9b911f..d0d47117fa6 100644 --- a/homeassistant/components/mqtt/translations/he.json +++ b/homeassistant/components/mqtt/translations/he.json @@ -136,5 +136,14 @@ "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea MQTT" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "\u05d0\u05d5\u05d8\u05d5\u05de\u05d8\u05d9", + "custom": "\u05de\u05d5\u05ea\u05d0\u05dd \u05d0\u05d9\u05e9\u05d9\u05ea", + "off": "\u05db\u05d1\u05d5\u05d9" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/hu.json b/homeassistant/components/mqtt/translations/hu.json index ad26cdbe56f..3e4307ff223 100644 --- a/homeassistant/components/mqtt/translations/hu.json +++ b/homeassistant/components/mqtt/translations/hu.json @@ -76,7 +76,7 @@ "title": "A manu\u00e1lisan konfigur\u00e1lt MQTT {platform} figyelmet ig\u00e9nyel" }, "deprecated_yaml_broker_settings": { - "description": "A k\u00f6vetkez\u0151 be\u00e1ll\u00edt\u00e1sok a `configuration.yaml'-ben tal\u00e1lhat\u00f3ak, \u00e1tker\u00fcltek az MQTT config bejegyz\u00e9sbe, \u00e9s mostant\u00f3l fel\u00fcl\u00edrj\u00e1k a `configuration.yaml'-ben tal\u00e1lhat\u00f3 be\u00e1ll\u00edt\u00e1sokat:\n`{deprecated_settings}`\n\nK\u00e9rj\u00fck, t\u00e1vol\u00edtsa el ezeket a be\u00e1ll\u00edt\u00e1sokat a `configuration.yaml` f\u00e1jlb\u00f3l \u00e9s ind\u00edtsa \u00fajra a Home Assistant-ot a probl\u00e9ma megold\u00e1s\u00e1hoz. Tov\u00e1bbi inform\u00e1ci\u00f3 a [document\u00e1ci\u00f3ban]({more_info_url}).", + "description": "A k\u00f6vetkez\u0151 be\u00e1ll\u00edt\u00e1sok a `configuration.yaml'-ben tal\u00e1lhat\u00f3ak, \u00e1tker\u00fcltek az MQTT config bejegyz\u00e9sbe, \u00e9s mostant\u00f3l fel\u00fcl\u00edrj\u00e1k a `configuration.yaml'-ben tal\u00e1lhat\u00f3 be\u00e1ll\u00edt\u00e1sokat:\n`{deprecated_settings}`\n\nK\u00e9rem, t\u00e1vol\u00edtsa el ezeket a be\u00e1ll\u00edt\u00e1sokat a `configuration.yaml` f\u00e1jlb\u00f3l \u00e9s ind\u00edtsa \u00fajra a Home Assistant-ot a probl\u00e9ma megold\u00e1s\u00e1hoz. Tov\u00e1bbi inform\u00e1ci\u00f3 a [document\u00e1ci\u00f3ban]({more_info_url}).", "title": "Elavult MQTT-be\u00e1ll\u00edt\u00e1sok tal\u00e1lhat\u00f3k a \"configuration.yaml\" f\u00e1jlban" } }, @@ -136,5 +136,14 @@ "title": "MQTT opci\u00f3k" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Automatikus", + "custom": "Egy\u00e9ni", + "off": "Ki" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/id.json b/homeassistant/components/mqtt/translations/id.json index 1dbc0710453..501adcd3424 100644 --- a/homeassistant/components/mqtt/translations/id.json +++ b/homeassistant/components/mqtt/translations/id.json @@ -136,5 +136,14 @@ "title": "Opsi MQTT" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Otomatis", + "custom": "Khusus", + "off": "Mati" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/it.json b/homeassistant/components/mqtt/translations/it.json index 857ddce0358..76307a170bd 100644 --- a/homeassistant/components/mqtt/translations/it.json +++ b/homeassistant/components/mqtt/translations/it.json @@ -136,5 +136,14 @@ "title": "Opzioni MQTT" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Automatico", + "custom": "Personalizzato", + "off": "Spento" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/ja.json b/homeassistant/components/mqtt/translations/ja.json index 2d11e594cca..03f0eb67c35 100644 --- a/homeassistant/components/mqtt/translations/ja.json +++ b/homeassistant/components/mqtt/translations/ja.json @@ -89,5 +89,14 @@ "title": "MQTT\u30aa\u30d7\u30b7\u30e7\u30f3" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "\u81ea\u52d5", + "custom": "\u30ab\u30b9\u30bf\u30e0", + "off": "\u30aa\u30d5" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/lt.json b/homeassistant/components/mqtt/translations/lt.json index 35257770c75..df254d4dd39 100644 --- a/homeassistant/components/mqtt/translations/lt.json +++ b/homeassistant/components/mqtt/translations/lt.json @@ -12,5 +12,14 @@ } } } + }, + "options": { + "step": { + "broker": { + "data": { + "password": "Slapta\u017eodis" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/lv.json b/homeassistant/components/mqtt/translations/lv.json index 2ff60e6ad84..f72d0df6855 100644 --- a/homeassistant/components/mqtt/translations/lv.json +++ b/homeassistant/components/mqtt/translations/lv.json @@ -10,5 +10,14 @@ "turn_off": "Iesl\u0113gt", "turn_on": "Iesl\u0113gt" } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Autom\u0101tiski", + "custom": "Izv\u0113les", + "off": "Izsl\u0113gts" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/nl.json b/homeassistant/components/mqtt/translations/nl.json index 5e4574299bd..d1f58b00379 100644 --- a/homeassistant/components/mqtt/translations/nl.json +++ b/homeassistant/components/mqtt/translations/nl.json @@ -136,5 +136,14 @@ "title": "MQTT-opties" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Automatisch", + "custom": "Handmatig", + "off": "Uitgeschakeld" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index 1e56625fe5c..93954f9c268 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -136,5 +136,14 @@ "title": "MQTT-alternativer" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Auto", + "custom": "Tilpasset", + "off": "Av" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json index b361fa11acb..eb151a6b2db 100644 --- a/homeassistant/components/mqtt/translations/pl.json +++ b/homeassistant/components/mqtt/translations/pl.json @@ -136,5 +136,14 @@ "title": "Opcje MQTT" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Automatyczny", + "custom": "R\u0119czny", + "off": "Brak" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/pt-BR.json b/homeassistant/components/mqtt/translations/pt-BR.json index 73753557d67..901323f9ba8 100644 --- a/homeassistant/components/mqtt/translations/pt-BR.json +++ b/homeassistant/components/mqtt/translations/pt-BR.json @@ -136,5 +136,14 @@ "title": "Op\u00e7\u00f5es de MQTT" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Auto", + "custom": "Personalizado", + "off": "Desligado" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/ru.json b/homeassistant/components/mqtt/translations/ru.json index acbf55512a2..5b44a23fcea 100644 --- a/homeassistant/components/mqtt/translations/ru.json +++ b/homeassistant/components/mqtt/translations/ru.json @@ -132,5 +132,14 @@ "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b MQTT" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438", + "custom": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439", + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/sk.json b/homeassistant/components/mqtt/translations/sk.json index acd06359037..3435af00fac 100644 --- a/homeassistant/components/mqtt/translations/sk.json +++ b/homeassistant/components/mqtt/translations/sk.json @@ -66,7 +66,7 @@ "button_quadruple_press": "\"{subtype}\" \u0161tvorn\u00e1sobne kliknut\u00e9", "button_quintuple_press": "\"{subtype}\" p\u00e4\u0165n\u00e1sobne kliknut\u00e9", "button_short_press": "\"{subtype}\" stla\u010den\u00e9", - "button_short_release": "\u201c{subtype}\u201c uvo\u013enen\u00e9", + "button_short_release": "\"{subtype}\" uvo\u013enen\u00e9", "button_triple_press": "\"{subtype}\" trojn\u00e1sobne kliknut\u00e9" } }, @@ -136,5 +136,14 @@ "title": "MQTT mo\u017enosti" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Auto", + "custom": "Vlastn\u00e9", + "off": "Vypnut\u00e9" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/sv.json b/homeassistant/components/mqtt/translations/sv.json index 249c8153a58..4d4910b3a78 100644 --- a/homeassistant/components/mqtt/translations/sv.json +++ b/homeassistant/components/mqtt/translations/sv.json @@ -10,9 +10,11 @@ "step": { "broker": { "data": { + "advanced_options": "Avancerade inst\u00e4llningar", "broker": "Broker", "password": "L\u00f6senord", "port": "Port", + "protocol": "MQTT-protokoll", "username": "Anv\u00e4ndarnamn" }, "description": "V\u00e4nligen ange anslutningsinformationen f\u00f6r din MQTT broker." @@ -38,13 +40,13 @@ "turn_on": "Starta" }, "trigger_type": { - "button_double_press": "\"{subtyp}\" dubbelklickad", - "button_long_press": "\" {subtype} \" kontinuerligt nedtryckt", - "button_long_release": "\" {subtype} \" sl\u00e4pptes efter l\u00e5ng tryckning", - "button_quadruple_press": "\"{subtyp}\" fyrdubbelt klickad", - "button_quintuple_press": "\"{subtype}\" kvintubbel klickade", - "button_short_press": "\"{subtyp}\" tryckt", - "button_short_release": "\"{subtyp}\" sl\u00e4pptes", + "button_double_press": "\"{subtype}\" dubbelklickad", + "button_long_press": "\"{subtype}\" kontinuerligt nedtryckt", + "button_long_release": "\"{subtype}\" sl\u00e4pptes efter l\u00e5ng tryckning", + "button_quadruple_press": "\"{subtype}\" fyrdubbelt klickad", + "button_quintuple_press": "\"{subtype}\" kvintubbelt klickad", + "button_short_press": "\"{subtype}\" tryckt", + "button_short_release": "\"{subtype}\" sl\u00e4pptes", "button_triple_press": "\" {subtype}\" trippelklickad" } }, @@ -89,5 +91,14 @@ "title": "MQTT-alternativ" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "Auto", + "custom": "Anpassad", + "off": "Fr\u00e5n" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/tr.json b/homeassistant/components/mqtt/translations/tr.json index 0cf9e430f88..f5819eb55b3 100644 --- a/homeassistant/components/mqtt/translations/tr.json +++ b/homeassistant/components/mqtt/translations/tr.json @@ -8,10 +8,11 @@ "bad_birth": "Ge\u00e7ersiz do\u011fum konusu", "bad_certificate": "CA sertifikas\u0131 ge\u00e7ersiz", "bad_client_cert": "Ge\u00e7ersiz m\u00fc\u015fteri sertifikas\u0131, PEM kodlu bir dosyan\u0131n sa\u011fland\u0131\u011f\u0131ndan emin olun", - "bad_client_cert_key": "\u0130stemci sertifikas\u0131 ve \u00f6zel ge\u00e7erli bir \u00e7ift de\u011fil", + "bad_client_cert_key": "\u0130stemci sertifikas\u0131 ve \u00f6zel anahtar ge\u00e7erli bir \u00e7ift de\u011fil", "bad_client_key": "Ge\u00e7ersiz \u00f6zel anahtar, PEM kodlu bir dosyan\u0131n parola olmadan sa\u011fland\u0131\u011f\u0131ndan emin olun", "bad_discovery_prefix": "Ge\u00e7ersiz ke\u015fif \u00f6neki", "bad_will": "Ge\u00e7ersiz irade konusu", + "bad_ws_headers": "JSON nesnesi olarak ge\u00e7erli HTTP ba\u015fl\u0131klar\u0131 sa\u011flay\u0131n", "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_inclusion": "\u0130stemci sertifikas\u0131 ve \u00f6zel anahtar birlikte yap\u0131land\u0131r\u0131lmal\u0131d\u0131r" }, @@ -20,10 +21,10 @@ "data": { "advanced_options": "Geli\u015fmi\u015f se\u00e7enekler", "broker": "Broker", - "certificate": "\u00d6zel CA sertifika dosyas\u0131n\u0131n yolu", - "client_cert": "\u0130stemci sertifika dosyas\u0131n\u0131n yolu", + "certificate": "\u00d6zel CA sertifika dosyas\u0131 y\u00fckleyin", + "client_cert": "\u0130stemci sertifikas\u0131 dosyas\u0131n\u0131 y\u00fckle", "client_id": "M\u00fc\u015fteri Kimli\u011fi (rastgele olu\u015fturulmu\u015f olana kadar bo\u015f b\u0131rak\u0131n)", - "client_key": "\u00d6zel anahtar dosyas\u0131n\u0131n yolu", + "client_key": "\u00d6zel anahtar dosyas\u0131n\u0131 y\u00fckleyin", "keepalive": "Canl\u0131 tutma mesajlar\u0131 g\u00f6nderme aras\u0131ndaki s\u00fcre", "password": "Parola", "port": "Port", @@ -31,7 +32,10 @@ "set_ca_cert": "Arac\u0131 sertifikas\u0131 do\u011frulama", "set_client_cert": "\u0130stemci sertifikas\u0131 kullan", "tls_insecure": "Arac\u0131 sertifika do\u011frulamas\u0131n\u0131 yoksay", - "username": "Kullan\u0131c\u0131 Ad\u0131" + "transport": "MQTT aktar\u0131m\u0131", + "username": "Kullan\u0131c\u0131 Ad\u0131", + "ws_headers": "JSON format\u0131nda WebSocket ba\u015fl\u0131klar\u0131", + "ws_path": "WebSocket yolu" }, "description": "L\u00fctfen MQTT brokerinizin ba\u011flant\u0131 bilgilerini girin." }, @@ -78,13 +82,14 @@ }, "options": { "error": { - "bad_birth": "Ge\u00e7ersiz do\u011fum konusu.", + "bad_birth": "Ge\u00e7ersiz do\u011fum konusu", "bad_certificate": "CA sertifikas\u0131 ge\u00e7ersiz", "bad_client_cert": "Ge\u00e7ersiz m\u00fc\u015fteri sertifikas\u0131, PEM kodlu bir dosyan\u0131n sa\u011fland\u0131\u011f\u0131ndan emin olun", - "bad_client_cert_key": "\u0130stemci sertifikas\u0131 ve \u00f6zel ge\u00e7erli bir \u00e7ift de\u011fil", + "bad_client_cert_key": "\u0130stemci sertifikas\u0131 ve \u00f6zel anahtar ge\u00e7erli bir \u00e7ift de\u011fil", "bad_client_key": "Ge\u00e7ersiz \u00f6zel anahtar, PEM kodlu bir dosyan\u0131n parola olmadan sa\u011fland\u0131\u011f\u0131ndan emin olun", "bad_discovery_prefix": "Ge\u00e7ersiz ke\u015fif \u00f6neki", - "bad_will": "Ge\u00e7ersiz olacak konu", + "bad_will": "Ge\u00e7ersiz irade konusu", + "bad_ws_headers": "JSON nesnesi olarak ge\u00e7erli HTTP ba\u015fl\u0131klar\u0131 sa\u011flay\u0131n", "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_inclusion": "\u0130stemci sertifikas\u0131 ve \u00f6zel anahtar birlikte yap\u0131land\u0131r\u0131lmal\u0131d\u0131r" }, @@ -94,7 +99,7 @@ "advanced_options": "Geli\u015fmi\u015f se\u00e7enekler", "broker": "Broker", "certificate": "\u00d6zel CA sertifika dosyas\u0131 y\u00fckleyin", - "client_cert": "\u0130stemci sertifika dosyas\u0131n\u0131 y\u00fckleyin", + "client_cert": "\u0130stemci sertifikas\u0131 dosyas\u0131n\u0131 y\u00fckle", "client_id": "M\u00fc\u015fteri Kimli\u011fi (rastgele olu\u015fturulmu\u015f olana kadar bo\u015f b\u0131rak\u0131n)", "client_key": "\u00d6zel anahtar dosyas\u0131n\u0131 y\u00fckleyin", "keepalive": "Canl\u0131 tutma mesajlar\u0131 g\u00f6nderme aras\u0131ndaki s\u00fcre", @@ -104,7 +109,10 @@ "set_ca_cert": "Arac\u0131 sertifikas\u0131 do\u011frulama", "set_client_cert": "\u0130stemci sertifikas\u0131 kullan", "tls_insecure": "Arac\u0131 sertifika do\u011frulamas\u0131n\u0131 yoksay", - "username": "Kullan\u0131c\u0131 Ad\u0131" + "transport": "MQTT aktar\u0131m\u0131", + "username": "Kullan\u0131c\u0131 Ad\u0131", + "ws_headers": "JSON format\u0131nda WebSocket ba\u015fl\u0131klar\u0131", + "ws_path": "WebSocket yolu" }, "description": "L\u00fctfen MQTT brokerinizin ba\u011flant\u0131 bilgilerini girin.", "title": "Broker se\u00e7enekleri" diff --git a/homeassistant/components/mqtt/translations/uk.json b/homeassistant/components/mqtt/translations/uk.json index ec09eef166c..11739ffd455 100644 --- a/homeassistant/components/mqtt/translations/uk.json +++ b/homeassistant/components/mqtt/translations/uk.json @@ -80,5 +80,13 @@ "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0438\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 MQTT." } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "\u0410\u0432\u0442\u043e", + "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json index 110eac492c5..a5b237788f7 100644 --- a/homeassistant/components/mqtt/translations/zh-Hant.json +++ b/homeassistant/components/mqtt/translations/zh-Hant.json @@ -136,5 +136,14 @@ "title": "MQTT \u9078\u9805" } } + }, + "selector": { + "set_ca_cert": { + "options": { + "auto": "\u81ea\u52d5", + "custom": "\u81ea\u8a02", + "off": "\u95dc\u9589" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index 9ee0690b120..f0158be11d7 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -76,7 +76,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT update through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT update through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 60f8d7a7d45..366a7dca159 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -34,7 +34,8 @@ def validate_mqtt_vacuum_discovery(config_value: ConfigType) -> ConfigType: return config -# Configuring MQTT Vacuums under the vacuum platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Vacuums under the vacuum platform key was deprecated in +# HA Core 2022.6 def validate_mqtt_vacuum(config_value: ConfigType) -> ConfigType: """Validate MQTT vacuum schema (deprecated).""" schemas = {LEGACY: PLATFORM_SCHEMA_LEGACY, STATE: PLATFORM_SCHEMA_STATE} @@ -56,7 +57,8 @@ DISCOVERY_SCHEMA = vol.All( MQTT_VACUUM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_vacuum_discovery ) -# Configuring MQTT Vacuums under the vacuum platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Vacuums under the vacuum platform key was deprecated in +# HA Core 2022.6 # Setup for the legacy YAML format was removed in HA Core 2022.12 PLATFORM_SCHEMA = vol.All( warn_for_legacy_schema(vacuum.DOMAIN), @@ -72,7 +74,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up MQTT vacuum through configuration.yaml and dynamically through MQTT discovery.""" + """Set up MQTT vacuum through YAML and through MQTT discovery.""" setup = functools.partial( _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 94053e4fb72..39b734235a0 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -160,7 +160,8 @@ PLATFORM_SCHEMA_LEGACY_MODERN = ( .extend(MQTT_VACUUM_SCHEMA.schema) ) -# Configuring MQTT Vacuums under the vacuum platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Vacuums under the vacuum platform key was deprecated in +# HA Core 2022.6 PLATFORM_SCHEMA_LEGACY = vol.All( cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_LEGACY_MODERN.schema), warn_for_legacy_schema(VACUUM_DOMAIN), @@ -413,7 +414,8 @@ class MqttVacuum(MqttEntity, VacuumEntity): def battery_icon(self) -> str: """Return the battery icon for the vacuum cleaner. - No need to check VacuumEntityFeature.BATTERY, this won't be called if battery_level is None. + No need to check VacuumEntityFeature.BATTERY, this won't be called if + battery_level is None. """ return icon_for_battery_level( battery_level=self.battery_level, charging=self._charging diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 210e189c2fc..3a5d267a5d4 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -154,7 +154,8 @@ PLATFORM_SCHEMA_STATE_MODERN = ( .extend(MQTT_VACUUM_SCHEMA.schema) ) -# Configuring MQTT Vacuums under the vacuum platform key was deprecated in HA Core 2022.6 +# Configuring MQTT Vacuums under the vacuum platform key was deprecated in +# HA Core 2022.6 PLATFORM_SCHEMA_STATE = vol.All( cv.PLATFORM_SCHEMA.extend(PLATFORM_SCHEMA_STATE_MODERN.schema), warn_for_legacy_schema(VACUUM_DOMAIN), diff --git a/homeassistant/components/mullvad/translations/lv.json b/homeassistant/components/mullvad/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/mullvad/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/lt.json b/homeassistant/components/myq/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/myq/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/uk.json b/homeassistant/components/myq/translations/uk.json index 12f8406de12..9a62d5fd558 100644 --- a/homeassistant/components/myq/translations/uk.json +++ b/homeassistant/components/myq/translations/uk.json @@ -9,6 +9,12 @@ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username} \u0431\u0456\u043b\u044c\u0448\u0435 \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439." + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 8316572379c..93b6a507356 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -63,27 +63,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][MYSENSORS_GATEWAYS][entry.entry_id] = gateway # Connect notify discovery as that integration doesn't support entry forwarding. - # Allow loading device tracker platform via discovery - # until refactor to config entry is done. - for platform in (Platform.DEVICE_TRACKER, Platform.NOTIFY): - load_discovery_platform = partial( - async_load_platform, - hass, - platform, - DOMAIN, - hass_config=hass.data[DOMAIN][DATA_HASS_CONFIG], - ) + load_discovery_platform = partial( + async_load_platform, + hass, + Platform.NOTIFY, + DOMAIN, + hass_config=hass.data[DOMAIN][DATA_HASS_CONFIG], + ) - on_unload( + on_unload( + hass, + entry.entry_id, + async_dispatcher_connect( hass, - entry.entry_id, - async_dispatcher_connect( - hass, - MYSENSORS_DISCOVERY.format(entry.entry_id, platform), - load_discovery_platform, - ), - ) + MYSENSORS_DISCOVERY.format(entry.entry_id, Platform.NOTIFY), + load_discovery_platform, + ), + ) await hass.config_entries.async_forward_entry_setups( entry, PLATFORMS_WITH_ENTRY_SUPPORT diff --git a/homeassistant/components/mysensors/binary_sensor.py b/homeassistant/components/mysensors/binary_sensor.py index 50ecf70f8fd..d8f4ec07cb2 100644 --- a/homeassistant/components/mysensors/binary_sensor.py +++ b/homeassistant/components/mysensors/binary_sensor.py @@ -1,12 +1,17 @@ """Support for MySensors binary sensors.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_ON, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -15,15 +20,49 @@ from .. import mysensors from .const import MYSENSORS_DISCOVERY, DiscoveryInfo from .helpers import on_unload -SENSORS = { - "S_DOOR": BinarySensorDeviceClass.DOOR, - "S_MOTION": BinarySensorDeviceClass.MOTION, - "S_SMOKE": BinarySensorDeviceClass.SMOKE, - "S_SPRINKLER": BinarySensorDeviceClass.SAFETY, - "S_WATER_LEAK": BinarySensorDeviceClass.SAFETY, - "S_SOUND": BinarySensorDeviceClass.SOUND, - "S_VIBRATION": BinarySensorDeviceClass.VIBRATION, - "S_MOISTURE": BinarySensorDeviceClass.MOISTURE, + +@dataclass +class MySensorsBinarySensorDescription(BinarySensorEntityDescription): + """Describe a MySensors binary sensor entity.""" + + is_on: Callable[[int, dict[int, str]], bool] = ( + lambda value_type, values: values[value_type] == "1" + ) + + +SENSORS: dict[str, MySensorsBinarySensorDescription] = { + "S_DOOR": MySensorsBinarySensorDescription( + key="S_DOOR", + device_class=BinarySensorDeviceClass.DOOR, + ), + "S_MOTION": MySensorsBinarySensorDescription( + key="S_MOTION", + device_class=BinarySensorDeviceClass.MOTION, + ), + "S_SMOKE": MySensorsBinarySensorDescription( + key="S_SMOKE", + device_class=BinarySensorDeviceClass.SMOKE, + ), + "S_SPRINKLER": MySensorsBinarySensorDescription( + key="S_SPRINKLER", + device_class=BinarySensorDeviceClass.SAFETY, + ), + "S_WATER_LEAK": MySensorsBinarySensorDescription( + key="S_WATER_LEAK", + device_class=BinarySensorDeviceClass.SAFETY, + ), + "S_SOUND": MySensorsBinarySensorDescription( + key="S_SOUND", + device_class=BinarySensorDeviceClass.SOUND, + ), + "S_VIBRATION": MySensorsBinarySensorDescription( + key="S_VIBRATION", + device_class=BinarySensorDeviceClass.VIBRATION, + ), + "S_MOISTURE": MySensorsBinarySensorDescription( + key="S_MOISTURE", + device_class=BinarySensorDeviceClass.MOISTURE, + ), } @@ -57,15 +96,17 @@ async def async_setup_entry( class MySensorsBinarySensor(mysensors.device.MySensorsEntity, BinarySensorEntity): - """Representation of a MySensors Binary Sensor child node.""" + """Representation of a MySensors binary sensor child node.""" + + entity_description: MySensorsBinarySensorDescription + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Set up the instance.""" + super().__init__(*args, **kwargs) + pres = self.gateway.const.Presentation + self.entity_description = SENSORS[pres(self.child_type).name] @property def is_on(self) -> bool: """Return True if the binary sensor is on.""" - return self._values.get(self.value_type) == STATE_ON - - @property - def device_class(self) -> BinarySensorDeviceClass | None: - """Return the class of this sensor, from DEVICE_CLASSES.""" - pres = self.gateway.const.Presentation - return SENSORS.get(pres(self.child_type).name) + return self.entity_description.is_on(self.value_type, self._child.values) diff --git a/homeassistant/components/mysensors/const.py b/homeassistant/components/mysensors/const.py index 42df81ae526..301e57c701f 100644 --- a/homeassistant/components/mysensors/const.py +++ b/homeassistant/components/mysensors/const.py @@ -91,6 +91,8 @@ LIGHT_TYPES: dict[SensorType, set[ValueType]] = { NOTIFY_TYPES: dict[SensorType, set[ValueType]] = {"S_INFO": {"V_TEXT"}} +REMOTE_TYPES: dict[SensorType, set[ValueType]] = {"S_IR": {"V_IR_SEND"}} + SENSOR_TYPES: dict[SensorType, set[ValueType]] = { "S_SOUND": {"V_LEVEL"}, "S_VIBRATION": {"V_LEVEL"}, @@ -107,7 +109,7 @@ SENSOR_TYPES: dict[SensorType, set[ValueType]] = { "S_POWER": {"V_WATT", "V_KWH", "V_VAR", "V_VA", "V_POWER_FACTOR"}, "S_DISTANCE": {"V_DISTANCE"}, "S_LIGHT_LEVEL": {"V_LIGHT_LEVEL", "V_LEVEL"}, - "S_IR": {"V_IR_RECEIVE"}, + "S_IR": {"V_IR_RECEIVE", "V_IR_RECORD"}, "S_WATER": {"V_FLOW", "V_VOLUME"}, "S_CUSTOM": {"V_VAR1", "V_VAR2", "V_VAR3", "V_VAR4", "V_VAR5", "V_CUSTOM"}, "S_SCENE_CONTROLLER": {"V_SCENE_ON", "V_SCENE_OFF"}, @@ -135,6 +137,7 @@ SWITCH_TYPES: dict[SensorType, set[ValueType]] = { "S_WATER_QUALITY": {"V_STATUS"}, } +TEXT_TYPES: dict[SensorType, set[ValueType]] = {"S_INFO": {"V_TEXT"}} PLATFORM_TYPES: dict[Platform, dict[SensorType, set[ValueType]]] = { Platform.BINARY_SENSOR: BINARY_SENSOR_TYPES, @@ -143,8 +146,10 @@ PLATFORM_TYPES: dict[Platform, dict[SensorType, set[ValueType]]] = { Platform.DEVICE_TRACKER: DEVICE_TRACKER_TYPES, Platform.LIGHT: LIGHT_TYPES, Platform.NOTIFY: NOTIFY_TYPES, + Platform.REMOTE: REMOTE_TYPES, Platform.SENSOR: SENSOR_TYPES, Platform.SWITCH: SWITCH_TYPES, + Platform.TEXT: TEXT_TYPES, } FLAT_PLATFORM_TYPES: dict[tuple[str, SensorType], set[ValueType]] = { @@ -161,5 +166,4 @@ for platform, platform_types in PLATFORM_TYPES.items(): PLATFORMS_WITH_ENTRY_SUPPORT = set(PLATFORM_TYPES.keys()) - { Platform.NOTIFY, - Platform.DEVICE_TRACKER, } diff --git a/homeassistant/components/mysensors/device_tracker.py b/homeassistant/components/mysensors/device_tracker.py index f2ea3736d15..920645a229a 100644 --- a/homeassistant/components/mysensors/device_tracker.py +++ b/homeassistant/components/mysensors/device_tracker.py @@ -1,94 +1,76 @@ """Support for tracking MySensors devices.""" from __future__ import annotations -from typing import Any, cast - -from homeassistant.components.device_tracker import AsyncSeeCallback +from homeassistant.components.device_tracker import SourceType, TrackerEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util import slugify +from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .. import mysensors -from .const import ATTR_GATEWAY_ID, DevId, DiscoveryInfo, GatewayId +from . import setup_mysensors_platform +from .const import MYSENSORS_DISCOVERY, DiscoveryInfo +from .device import MySensorsEntity from .helpers import on_unload -async def async_setup_scanner( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, - async_see: AsyncSeeCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> bool: - """Set up the MySensors device scanner.""" - if not discovery_info: - return False - - new_devices = mysensors.setup_mysensors_platform( - hass, - Platform.DEVICE_TRACKER, - cast(DiscoveryInfo, discovery_info), - MySensorsDeviceScanner, - device_args=(hass, async_see), - ) - if not new_devices: - return False - - for device in new_devices: - gateway_id: GatewayId = discovery_info[ATTR_GATEWAY_ID] - dev_id: DevId = (gateway_id, device.node_id, device.child_id, device.value_type) - on_unload( - hass, - gateway_id, - async_dispatcher_connect( - hass, - mysensors.const.CHILD_CALLBACK.format(*dev_id), - device.async_update_callback, - ), - ) - on_unload( - hass, - gateway_id, - async_dispatcher_connect( - hass, - mysensors.const.NODE_CALLBACK.format(gateway_id, device.node_id), - device.async_update_callback, - ), - ) - - return True - - -class MySensorsDeviceScanner(mysensors.device.MySensorsDevice): - """Represent a MySensors scanner.""" - - def __init__( - self, - hass: HomeAssistant, - async_see: AsyncSeeCallback, - *args: Any, - ) -> None: - """Set up instance.""" - super().__init__(*args) - self.async_see = async_see - self.hass = hass + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up this platform for a specific ConfigEntry(==Gateway).""" @callback - def _async_update_callback(self) -> None: - """Update the device.""" - self._async_update() + def async_discover(discovery_info: DiscoveryInfo) -> None: + """Discover and add a MySensors device tracker.""" + setup_mysensors_platform( + hass, + Platform.DEVICE_TRACKER, + discovery_info, + MySensorsDeviceTracker, + async_add_entities=async_add_entities, + ) + + on_unload( + hass, + config_entry.entry_id, + async_dispatcher_connect( + hass, + MYSENSORS_DISCOVERY.format(config_entry.entry_id, Platform.DEVICE_TRACKER), + async_discover, + ), + ) + + +class MySensorsDeviceTracker(MySensorsEntity, TrackerEntity): + """Represent a MySensors device tracker.""" + + _latitude: float | None = None + _longitude: float | None = None + + @property + def latitude(self) -> float | None: + """Return latitude value of the device.""" + return self._latitude + + @property + def longitude(self) -> float | None: + """Return longitude value of the device.""" + return self._longitude + + @property + def source_type(self) -> SourceType: + """Return the source type of the device.""" + return SourceType.GPS + + @callback + def _async_update(self) -> None: + """Update the controller with the latest value from a device.""" + super()._async_update() node = self.gateway.sensors[self.node_id] child = node.children[self.child_id] - position = child.values[self.value_type] + position: str = child.values[self.value_type] latitude, longitude, _ = position.split(",") - - self.hass.async_create_task( - self.async_see( - dev_id=slugify(self.name), - host_name=self.name, - gps=(latitude, longitude), - battery=node.battery_level, - attributes=self._extra_attributes, - ) - ) + self._latitude = float(latitude) + self._longitude = float(longitude) diff --git a/homeassistant/components/mysensors/notify.py b/homeassistant/components/mysensors/notify.py index b19ce7b85ca..97d4175a6f2 100644 --- a/homeassistant/components/mysensors/notify.py +++ b/homeassistant/components/mysensors/notify.py @@ -6,10 +6,12 @@ from typing import Any, cast from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.const import Platform from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util import slugify from .. import mysensors -from .const import DevId, DiscoveryInfo +from .const import DOMAIN, DevId, DiscoveryInfo async def async_get_service( @@ -63,6 +65,7 @@ class MySensorsNotificationService(BaseNotificationService): ] = mysensors.get_mysensors_devices( hass, Platform.NOTIFY ) # type: ignore[assignment] + self.hass = hass async def async_send_message(self, message: str = "", **kwargs: Any) -> None: """Send a message to a user.""" @@ -73,5 +76,25 @@ class MySensorsNotificationService(BaseNotificationService): if target_devices is None or device.name in target_devices ] + placeholders = { + "alternate_service": "text.set_value", + "deprecated_service": f"notify.{self._service_name}", + "alternate_target": str( + [f"text.{slugify(device.name)}" for device in devices] + ), + } + + async_create_issue( + self.hass, + DOMAIN, + "deprecated_notify_service", + breaks_in_ha_version="2023.4.0", + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key="deprecated_service", + translation_placeholders=placeholders, + ) + for device in devices: device.send_msg(message) diff --git a/homeassistant/components/mysensors/remote.py b/homeassistant/components/mysensors/remote.py new file mode 100644 index 00000000000..d72bbfa4235 --- /dev/null +++ b/homeassistant/components/mysensors/remote.py @@ -0,0 +1,124 @@ +"""Support MySensors IR transceivers.""" +from __future__ import annotations + +from collections.abc import Iterable +from typing import Any, cast + +from homeassistant.components.remote import ( + ATTR_COMMAND, + RemoteEntity, + RemoteEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import setup_mysensors_platform +from .const import MYSENSORS_DISCOVERY, DiscoveryInfo +from .device import MySensorsEntity +from .helpers import on_unload + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up this platform for a specific ConfigEntry(==Gateway).""" + + @callback + def async_discover(discovery_info: DiscoveryInfo) -> None: + """Discover and add a MySensors remote.""" + setup_mysensors_platform( + hass, + Platform.REMOTE, + discovery_info, + MySensorsRemote, + async_add_entities=async_add_entities, + ) + + on_unload( + hass, + config_entry.entry_id, + async_dispatcher_connect( + hass, + MYSENSORS_DISCOVERY.format(config_entry.entry_id, Platform.REMOTE), + async_discover, + ), + ) + + +class MySensorsRemote(MySensorsEntity, RemoteEntity): + """Representation of a MySensors IR transceiver.""" + + _current_command: str | None = None + + @property + def is_on(self) -> bool | None: + """Return True if remote is on.""" + set_req = self.gateway.const.SetReq + value = cast(str | None, self._child.values.get(set_req.V_LIGHT)) + if value is None: + return None + return value == "1" + + @property + def supported_features(self) -> RemoteEntityFeature: + """Flag supported features.""" + features = RemoteEntityFeature(0) + set_req = self.gateway.const.SetReq + if set_req.V_IR_RECORD in self._values: + features = features | RemoteEntityFeature.LEARN_COMMAND + return features + + async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None: + """Send commands to a device.""" + for cmd in command: + self._current_command = cmd + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, cmd, ack=1 + ) + + async def async_learn_command(self, **kwargs: Any) -> None: + """Learn a command from a device.""" + set_req = self.gateway.const.SetReq + commands: list[str] | None = kwargs.get(ATTR_COMMAND) + if commands is None: + raise ValueError("Command not specified for learn_command service") + + for command in commands: + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_IR_RECORD, command, ack=1 + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the IR transceiver on.""" + set_req = self.gateway.const.SetReq + if self._current_command: + self.gateway.set_child_value( + self.node_id, + self.child_id, + self.value_type, + self._current_command, + ack=1, + ) + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_LIGHT, 1, ack=1 + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the IR transceiver off.""" + set_req = self.gateway.const.SetReq + self.gateway.set_child_value( + self.node_id, self.child_id, set_req.V_LIGHT, 0, ack=1 + ) + + @callback + def _async_update(self) -> None: + """Update the controller with the latest value from a device.""" + super()._async_update() + self._current_command = cast( + str | None, self._child.values.get(self.value_type) + ) diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index 225a75e8c84..174b1f094b1 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -165,6 +165,10 @@ SENSORS: dict[str, SensorEntityDescription] = { device_class=SensorDeviceClass.CURRENT, state_class=SensorStateClass.MEASUREMENT, ), + "V_IR_RECORD": SensorEntityDescription( + key="V_IR_RECORD", + icon="mdi:remote", + ), "V_PH": SensorEntityDescription( key="V_PH", native_unit_of_measurement="pH", diff --git a/homeassistant/components/mysensors/strings.json b/homeassistant/components/mysensors/strings.json index dc5dc76c7ae..c192db7549f 100644 --- a/homeassistant/components/mysensors/strings.json +++ b/homeassistant/components/mysensors/strings.json @@ -83,5 +83,29 @@ "port_out_of_range": "Port number must be at least 1 and at most 65535", "unknown": "[%key:common::config_flow::error::unknown%]" } + }, + "issues": { + "deprecated_entity": { + "title": "The {deprecated_entity} entity will be removed", + "fix_flow": { + "step": { + "confirm": { + "title": "The {deprecated_entity} entity will be removed", + "description": "Update any automations or scripts that use this entity in service calls using the `{deprecated_service}` service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`." + } + } + } + }, + "deprecated_service": { + "title": "The {deprecated_service} service will be removed", + "fix_flow": { + "step": { + "confirm": { + "title": "The {deprecated_service} service will be removed", + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`." + } + } + } + } } } diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index df0e6fe8532..e5b0968785f 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -8,10 +8,11 @@ import voluptuous as vol from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback, split_entity_id 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.issue_registry import IssueSeverity, async_create_issue from .. import mysensors from .const import ( @@ -151,8 +152,35 @@ class MySensorsIRSwitch(MySensorsSwitch): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the IR switch on.""" set_req = self.gateway.const.SetReq + placeholders = { + "deprecated_entity": self.entity_id, + "alternate_target": f"remote.{split_entity_id(self.entity_id)[1]}", + } + if ATTR_IR_CODE in kwargs: self._ir_code = kwargs[ATTR_IR_CODE] + placeholders[ + "deprecated_service" + ] = f"{MYSENSORS_DOMAIN}.{SERVICE_SEND_IR_CODE}" + placeholders["alternate_service"] = "remote.send_command" + else: + placeholders["deprecated_service"] = "switch.turn_on" + placeholders["alternate_service"] = "remote.turn_on" + + async_create_issue( + self.hass, + MYSENSORS_DOMAIN, + ( + "deprecated_ir_switch_entity_" + f"{self.entity_id}_{placeholders['deprecated_service']}" + ), + breaks_in_ha_version="2023.4.0", + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key="deprecated_entity", + translation_placeholders=placeholders, + ) self.gateway.set_child_value( self.node_id, self.child_id, self.value_type, self._ir_code ) @@ -169,6 +197,22 @@ class MySensorsIRSwitch(MySensorsSwitch): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the IR switch off.""" + async_create_issue( + self.hass, + MYSENSORS_DOMAIN, + f"deprecated_ir_switch_entity_{self.entity_id}_switch.turn_off", + breaks_in_ha_version="2023.4.0", + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key="deprecated_entity", + translation_placeholders={ + "deprecated_entity": self.entity_id, + "deprecated_service": "switch.turn_off", + "alternate_service": "remote.turn_off", + "alternate_target": f"remote.{split_entity_id(self.entity_id)[1]}", + }, + ) set_req = self.gateway.const.SetReq self.gateway.set_child_value( self.node_id, self.child_id, set_req.V_LIGHT, 0, ack=1 diff --git a/homeassistant/components/mysensors/text.py b/homeassistant/components/mysensors/text.py new file mode 100644 index 00000000000..e7bb7add084 --- /dev/null +++ b/homeassistant/components/mysensors/text.py @@ -0,0 +1,60 @@ +"""Provide a text platform for MySensors.""" +from __future__ import annotations + +from homeassistant.components.text import TextEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .. import mysensors +from .const import MYSENSORS_DISCOVERY, DiscoveryInfo +from .device import MySensorsEntity +from .helpers import on_unload + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up this platform for a specific ConfigEntry(==Gateway).""" + + @callback + def async_discover(discovery_info: DiscoveryInfo) -> None: + """Discover and add a MySensors text entity.""" + mysensors.setup_mysensors_platform( + hass, + Platform.TEXT, + discovery_info, + MySensorsText, + async_add_entities=async_add_entities, + ) + + on_unload( + hass, + config_entry.entry_id, + async_dispatcher_connect( + hass, + MYSENSORS_DISCOVERY.format(config_entry.entry_id, Platform.TEXT), + async_discover, + ), + ) + + +class MySensorsText(MySensorsEntity, TextEntity): + """Representation of the value of a MySensors Text child node.""" + + _attr_native_max = 25 + + @property + def native_value(self) -> str | None: + """Return the value reported by the text.""" + return self._values.get(self.value_type) + + async def async_set_value(self, value: str) -> None: + """Change the value.""" + self.gateway.set_child_value( + self.node_id, self.child_id, self.value_type, value, ack=1 + ) diff --git a/homeassistant/components/mysensors/translations/bg.json b/homeassistant/components/mysensors/translations/bg.json index f5422095553..baf81042eb3 100644 --- a/homeassistant/components/mysensors/translations/bg.json +++ b/homeassistant/components/mysensors/translations/bg.json @@ -38,5 +38,27 @@ } } } + }, + "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "title": "\u041e\u0431\u0435\u043a\u0442\u044a\u0442 {deprecated_entity} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442" + } + } + }, + "title": "\u041e\u0431\u0435\u043a\u0442\u044a\u0442 {deprecated_entity} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442" + }, + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 {deprecated_service} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" + } + } + }, + "title": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 {deprecated_service} \u0449\u0435 \u0431\u044a\u0434\u0435 \u043f\u0440\u0435\u043c\u0430\u0445\u043d\u0430\u0442\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/ca.json b/homeassistant/components/mysensors/translations/ca.json index 23c07adc9fa..750b49335b0 100644 --- a/homeassistant/components/mysensors/translations/ca.json +++ b/homeassistant/components/mysensors/translations/ca.json @@ -83,5 +83,29 @@ "description": "Tria el m\u00e8tode de connexi\u00f3 a la passarel\u00b7la" } } + }, + "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquesta entitat en crides del servei `{deprecated_service}` perqu\u00e8 passin a utilitzar el servei `{alternate_service}` amb un ID d'entitat objectiu o 'target' `{alternate_target}`.", + "title": "L'entitat {deprecated_entity} s'eliminar\u00e0" + } + } + }, + "title": "L'entitat {deprecated_entity} s'eliminar\u00e0" + }, + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei perqu\u00e8 passin a utilitzar el servei `{alternate_service}` amb un ID d'entitat objectiu o 'target' `{alternate_target}`.", + "title": "El servei {deprecated_service} s'eliminar\u00e0" + } + } + }, + "title": "El servei {deprecated_service} s'eliminar\u00e0" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/de.json b/homeassistant/components/mysensors/translations/de.json index 5c477806600..9eb9ff5bad4 100644 --- a/homeassistant/components/mysensors/translations/de.json +++ b/homeassistant/components/mysensors/translations/de.json @@ -83,5 +83,29 @@ "description": "Verbindungsmethode zum Gateway w\u00e4hlen" } } + }, + "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diese Entit\u00e4t in Dienstaufrufen mit dem Dienst `{deprecated_service}` verwenden, um stattdessen den Dienst `{alternate_service}` mit einer Zielentit\u00e4ts-ID von `{alternate_target}` zu verwenden.", + "title": "Die Entit\u00e4t {deprecated_entity} wird entfernt" + } + } + }, + "title": "Die Entit\u00e4t {deprecated_entity} wird entfernt" + }, + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, um stattdessen den Dienst `{alternate_service}` mit einer Ziel-Entit\u00e4ts-ID von `{alternate_target}` zu verwenden.", + "title": "Der Dienst {deprecated_service} wird entfernt" + } + } + }, + "title": "Der Dienst {deprecated_service} wird entfernt" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/el.json b/homeassistant/components/mysensors/translations/el.json index f90d3770f7a..1b63c98c37f 100644 --- a/homeassistant/components/mysensors/translations/el.json +++ b/homeassistant/components/mysensors/translations/el.json @@ -83,5 +83,29 @@ "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03bc\u03b5 \u03c4\u03b7\u03bd \u03c0\u03cd\u03bb\u03b7" } } + }, + "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03c5\u03c7\u03cc\u03bd \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03c3\u03b5 \u03ba\u03bb\u03ae\u03c3\u03b5\u03b9\u03c2 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd \u03bc\u03b5 \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 `{deprecated_service}` \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 `{alternate_service}` \u03bc\u03b5 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2-\u03c3\u03c4\u03cc\u03c7\u03bf\u03c5 `{alternate_target}`.", + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 {deprecated_entity} \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + } + } + }, + "title": "\u0397 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 {deprecated_entity} \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + }, + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03c5\u03c7\u03cc\u03bd \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u00ab {alternate_service} \u00bb \u03bc\u03b5 \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c3\u03c4\u03cc\u03c7\u03bf\u03c5 \u00ab {alternate_target} \u00bb.", + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + } + } + }, + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 {deprecated_service} \u03b8\u03b1 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/en.json b/homeassistant/components/mysensors/translations/en.json index b85a28fb7d3..cb9423694ab 100644 --- a/homeassistant/components/mysensors/translations/en.json +++ b/homeassistant/components/mysensors/translations/en.json @@ -83,5 +83,29 @@ "description": "Choose connection method to the gateway" } } + }, + "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Update any automations or scripts that use this entity in service calls using the `{deprecated_service}` service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`.", + "title": "The {deprecated_entity} entity will be removed" + } + } + }, + "title": "The {deprecated_entity} entity will be removed" + }, + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Update any automations or scripts that use this service to instead use the `{alternate_service}` service with a target entity ID of `{alternate_target}`.", + "title": "The {deprecated_service} service will be removed" + } + } + }, + "title": "The {deprecated_service} service will be removed" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/es.json b/homeassistant/components/mysensors/translations/es.json index 03619e114bf..4bbe10f1908 100644 --- a/homeassistant/components/mysensors/translations/es.json +++ b/homeassistant/components/mysensors/translations/es.json @@ -83,5 +83,29 @@ "description": "Elige el m\u00e9todo de conexi\u00f3n a la puerta de enlace" } } + }, + "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualiza cualquier automatizaci\u00f3n o script que use esta entidad en llamadas de servicio usando el servicio `{deprecated_service}` para usar en su lugar el servicio `{alternate_service}` con un ID de entidad de destino de `{alternate_target}`.", + "title": "Se eliminar\u00e1 la entidad {deprecated_entity}" + } + } + }, + "title": "Se eliminar\u00e1 la entidad {deprecated_entity}" + }, + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio para usar en su lugar el servicio `{alternate_service}` con una ID de entidad de destino de `{alternate_target}`.", + "title": "Se eliminar\u00e1 el servicio {deprecated_service}" + } + } + }, + "title": "Se eliminar\u00e1 el servicio {deprecated_service}" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/et.json b/homeassistant/components/mysensors/translations/et.json index 06cdc0e74a1..3cbbf169c57 100644 --- a/homeassistant/components/mysensors/translations/et.json +++ b/homeassistant/components/mysensors/translations/et.json @@ -83,5 +83,29 @@ "description": "Vali l\u00fc\u00fcsi \u00fchendusviis" } } + }, + "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "V\u00e4rskenda k\u00f5iki automatiseerimisi v\u00f5i skripte, mis kasutavad seda olemit teenusek\u00f5nedes, kasutades teenust ` {deprecated_service} , et kasutada selle asemel teenust ` {alternate_service} , mille siht\u00fcksuse ID on ` {alternate_target} .", + "title": "\u00dcksus {deprecated_entity} eemaldatakse" + } + } + }, + "title": "\u00dcksus {deprecated_entity} eemaldatakse" + }, + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "V\u00e4rskenda k\u00f5iki automaatikaid v\u00f5i skripte, mis seda teenust kasutavad, et kasutada selle asemel teenust '{alternate_service}', mille sihtolemi ID on '{alternate_target}'.", + "title": "Teenus {deprecated_service} eemaldatakse" + } + } + }, + "title": "Teenus {deprecated_service} eemaldatakse" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/hu.json b/homeassistant/components/mysensors/translations/hu.json index 154afe15613..7e1d06b30d2 100644 --- a/homeassistant/components/mysensors/translations/hu.json +++ b/homeassistant/components/mysensors/translations/hu.json @@ -83,5 +83,17 @@ "description": "V\u00e1lassza ki az \u00e1tj\u00e1r\u00f3hoz val\u00f3 csatlakoz\u00e1si m\u00f3dot" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "title": "A(z) {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + } + }, + "title": "A(z) {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/id.json b/homeassistant/components/mysensors/translations/id.json index e5776c23316..d1bc547ecfe 100644 --- a/homeassistant/components/mysensors/translations/id.json +++ b/homeassistant/components/mysensors/translations/id.json @@ -83,5 +83,29 @@ "description": "Pilih metode koneksi ke gateway" } } + }, + "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Perbarui semua otomasi atau skrip yang menggunakan entitas ini dalam pemanggilan layanan `{deprecated_service}` untuk menggunakan layanan `{alternate_service}` dengan ID entitas target `{alternate_target}`.", + "title": "Entitas {deprecated_entity} akan dihapus" + } + } + }, + "title": "Entitas {deprecated_entity} akan dihapus" + }, + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Perbarui semua otomasi atau skrip yang menggunakan layanan ini untuk menggunakan layanan `{alternate_service}` dengan ID entitas target `{alternate_target}`.", + "title": "Layanan {deprecated_service} akan dihapus" + } + } + }, + "title": "Layanan {deprecated_service} akan dihapus" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/it.json b/homeassistant/components/mysensors/translations/it.json index b7063bb5e47..cdaaee87e83 100644 --- a/homeassistant/components/mysensors/translations/it.json +++ b/homeassistant/components/mysensors/translations/it.json @@ -83,5 +83,18 @@ "description": "Scegli il metodo di connessione al gateway" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aggiornare tutte le automazioni o gli script che utilizzano questo servizio per utilizzare invece il servizio `{alternate_service}` con un ID entit\u00e0 di destinazione `{alternate_target}`.", + "title": "Il servizio {deprecated_service} sar\u00e0 rimosso" + } + } + }, + "title": "Il servizio {deprecated_service} sar\u00e0 rimosso" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/lv.json b/homeassistant/components/mysensors/translations/lv.json new file mode 100644 index 00000000000..862ef1ca431 --- /dev/null +++ b/homeassistant/components/mysensors/translations/lv.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/nl.json b/homeassistant/components/mysensors/translations/nl.json index 32fa49352e7..c9ad860f288 100644 --- a/homeassistant/components/mysensors/translations/nl.json +++ b/homeassistant/components/mysensors/translations/nl.json @@ -71,7 +71,8 @@ "select_gateway_type": { "menu_options": { "gw_mqtt": "Configureer een MQTT-gateway", - "gw_serial": "Configureer een seri\u00eble gateway" + "gw_serial": "Configureer een seri\u00eble gateway", + "gw_tcp": "Configureer een TCP-gateway" } }, "user": { @@ -81,5 +82,18 @@ "description": "Kies de verbindingsmethode met de gateway" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Werk automatiseringen of scripts bij die deze dienst gebruiken en gebruik in plaats daarvan de `{alternate_service}` dienst met doel entiteit ID `{alternate_target}`.", + "title": "De {deprecated_service} service zal worden verwijderd" + } + } + }, + "title": "De {deprecated_service} service zal worden verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/no.json b/homeassistant/components/mysensors/translations/no.json index 0f119f61028..7dda500d843 100644 --- a/homeassistant/components/mysensors/translations/no.json +++ b/homeassistant/components/mysensors/translations/no.json @@ -83,5 +83,29 @@ "description": "Velg tilkoblingsmetode til gatewayen" } } + }, + "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne enheten i tjenesteanrop ved \u00e5 bruke ` {deprecated_service} `-tjenesten for i stedet \u00e5 bruke ` {alternate_service} `-tjenesten med en m\u00e5lenhets-ID p\u00e5 ` {alternate_target} `.", + "title": "{deprecated_entity} -enheten vil bli fjernet" + } + } + }, + "title": "{deprecated_entity} -enheten vil bli fjernet" + }, + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten for i stedet \u00e5 bruke ` {alternate_service} `-tjenesten med en m\u00e5lenhets-ID p\u00e5 ` {alternate_target} `.", + "title": "{deprecated_service} -tjenesten vil bli fjernet" + } + } + }, + "title": "{deprecated_service} -tjenesten vil bli fjernet" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/pl.json b/homeassistant/components/mysensors/translations/pl.json index 689bc021e89..a8ce9f0875d 100644 --- a/homeassistant/components/mysensors/translations/pl.json +++ b/homeassistant/components/mysensors/translations/pl.json @@ -83,5 +83,18 @@ "description": "Wybierz metod\u0119 po\u0142\u0105czenia z bramk\u0105" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Zaktualizuj wszystkie automatyzacje lub skrypty, kt\u00f3re u\u017cywaj\u0105 tej us\u0142ugi, aby zamiast tego u\u017cywa\u0142y us\u0142ugi `{alternate_service}` z encj\u0105 docelow\u0105 `{alternate_target}`.", + "title": "Us\u0142uga {deprecated_service} zostanie usuni\u0119ta" + } + } + }, + "title": "Us\u0142uga {deprecated_service} zostanie usuni\u0119ta" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/pt-BR.json b/homeassistant/components/mysensors/translations/pt-BR.json index ea76d19f358..67d04bd2328 100644 --- a/homeassistant/components/mysensors/translations/pt-BR.json +++ b/homeassistant/components/mysensors/translations/pt-BR.json @@ -83,5 +83,29 @@ "description": "Escolha o m\u00e9todo de conex\u00e3o com o gateway" } } + }, + "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Atualize todas as automa\u00e7\u00f5es ou scripts que usam essa entidade em chamadas de servi\u00e7o usando o servi\u00e7o `{deprecated_service}` para, em vez disso, usar o servi\u00e7o `{alternate_service}` com um ID de entidade de destino de `{alternate_target}`.", + "title": "A entidade {deprecated_entity} ser\u00e1 removida" + } + } + }, + "title": "A entidade {deprecated_entity} ser\u00e1 removida" + }, + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Atualize todas as automa\u00e7\u00f5es ou scripts que usam esse servi\u00e7o para usar o servi\u00e7o `{alternate_service}` com um ID de entidade de destino de `{alternate_target}`.", + "title": "O servi\u00e7o {deprecated_service} ser\u00e1 removido" + } + } + }, + "title": "O servi\u00e7o {deprecated_service} ser\u00e1 removido" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/pt.json b/homeassistant/components/mysensors/translations/pt.json index 2eb65a87447..2b245542f82 100644 --- a/homeassistant/components/mysensors/translations/pt.json +++ b/homeassistant/components/mysensors/translations/pt.json @@ -15,5 +15,10 @@ } } } + }, + "issues": { + "deprecated_entity": { + "title": "A entidade {deprecated_entity} ir\u00e1 ser removida" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/ru.json b/homeassistant/components/mysensors/translations/ru.json index eaac7b9230e..e12629945ab 100644 --- a/homeassistant/components/mysensors/translations/ru.json +++ b/homeassistant/components/mysensors/translations/ru.json @@ -83,5 +83,29 @@ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0448\u043b\u044e\u0437\u0443" } } + }, + "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0412 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f\u0445 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u0430\u0445, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0445 \u0441\u043b\u0443\u0436\u0431\u0443 `{deprecated_service}`, \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0443\u0436\u0431\u0443 `{alternate_service}` \u0441 \u0446\u0435\u043b\u0435\u0432\u044b\u043c \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u043c `{alternate_target}`.", + "title": "\u041e\u0431\u044a\u0435\u043a\u0442 {deprecated_entity} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d" + } + } + }, + "title": "\u041e\u0431\u044a\u0435\u043a\u0442 {deprecated_entity} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d" + }, + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0412 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f\u0445 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u0430\u0445, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0445 \u044d\u0442\u0443 \u0441\u043b\u0443\u0436\u0431\u0443, \u0442\u0435\u043f\u0435\u0440\u044c \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0443\u0436\u0431\u0443 `{alternate_service}` \u0441 \u0446\u0435\u043b\u0435\u0432\u044b\u043c \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u043c `{alternate_target}`.", + "title": "\u0421\u043b\u0443\u0436\u0431\u0430 {deprecated_service} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + } + }, + "title": "\u0421\u043b\u0443\u0436\u0431\u0430 {deprecated_service} \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/sk.json b/homeassistant/components/mysensors/translations/sk.json index a0c4453cd92..639d9a1ed45 100644 --- a/homeassistant/components/mysensors/translations/sk.json +++ b/homeassistant/components/mysensors/translations/sk.json @@ -83,5 +83,29 @@ "description": "Vyberte sp\u00f4sob pripojenia k br\u00e1ne" } } + }, + "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualizujte v\u0161etky automatiz\u00e1cie alebo skripty, ktor\u00e9 pou\u017e\u00edvaj\u00fa t\u00fato entitu vo volaniach slu\u017eby pomocou slu\u017eby `{deprecated_service}`, aby namiesto toho pou\u017e\u00edvali slu\u017ebu `{alternate_service}` s ID cie\u013eovej entity `{alternate_target}`.", + "title": "Entita {deprecated_entity} bude odstr\u00e1nen\u00e1" + } + } + }, + "title": "Entita {deprecated_entity} bude odstr\u00e1nen\u00e1" + }, + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Aktualizujte v\u0161etky automatiz\u00e1cie alebo skripty, ktor\u00e9 pou\u017e\u00edvaj\u00fa t\u00fato slu\u017ebu, aby namiesto nej pou\u017e\u00edvali slu\u017ebu `{alternate_service}` s ID cie\u013eovej entity `{alternate_target}`.", + "title": "Slu\u017eba {deprecated_service} bude odstr\u00e1nen\u00e1" + } + } + }, + "title": "Slu\u017eba {deprecated_service} bude odstr\u00e1nen\u00e1" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/tr.json b/homeassistant/components/mysensors/translations/tr.json index cd66bb79a0b..62b0cb3ac6a 100644 --- a/homeassistant/components/mysensors/translations/tr.json +++ b/homeassistant/components/mysensors/translations/tr.json @@ -83,5 +83,18 @@ "description": "A\u011f ge\u00e7idine ba\u011flant\u0131 y\u00f6ntemini se\u00e7in" } } + }, + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "Bu hizmeti kullanan t\u00fcm otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131, bunun yerine \" {alternate_service} \" hizmetini \" {alternate_target} alternate_target}\" hedef varl\u0131k kimli\u011fiyle kullanacak \u015fekilde g\u00fcncelleyin.", + "title": "{deprecated_service} hizmeti kald\u0131r\u0131lacak" + } + } + }, + "title": "{deprecated_service} hizmeti kald\u0131r\u0131lacak" + } } } \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/uk.json b/homeassistant/components/mysensors/translations/uk.json new file mode 100644 index 00000000000..eb52061373f --- /dev/null +++ b/homeassistant/components/mysensors/translations/uk.json @@ -0,0 +1,14 @@ +{ + "issues": { + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "title": "\u0421\u043b\u0443\u0436\u0431\u0443 {deprecated_service} \u0431\u0443\u0434\u0435 \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043e" + } + } + }, + "title": "\u0421\u043b\u0443\u0436\u0431\u0443 {deprecated_service} \u0431\u0443\u0434\u0435 \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mysensors/translations/zh-Hant.json b/homeassistant/components/mysensors/translations/zh-Hant.json index a86f7c480ff..d3a6d166992 100644 --- a/homeassistant/components/mysensors/translations/zh-Hant.json +++ b/homeassistant/components/mysensors/translations/zh-Hant.json @@ -83,5 +83,29 @@ "description": "\u9078\u64c7\u9598\u9053\u5668\u9023\u7dda\u65b9\u5f0f" } } + }, + "issues": { + "deprecated_entity": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u4f7f\u7528\u6b64\u5be6\u9ad4\u65bc\u670d\u52d9\u4e2d\u4f7f\u7528 `{deprecated_service}` \u670d\u52d9\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\u3001\u4ee5\u53d6\u4ee3\u4f7f\u7528\u76ee\u6a19\u5be6\u9ad4 ID \u70ba `{alternate_target}` \u4e4b `{alternate_service}` \u670d\u52d9\u3002", + "title": "{deprecated_entity} \u5be6\u9ad4\u5c07\u9032\u884c\u79fb\u9664" + } + } + }, + "title": "{deprecated_entity} \u5be6\u9ad4\u5c07\u9032\u884c\u79fb\u9664" + }, + "deprecated_service": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u4f7f\u7528\u6b64\u670d\u52d9\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\u3001\u4ee5\u53d6\u4ee3\u4f7f\u7528\u76ee\u6a19\u5be6\u9ad4 ID \u70ba `{alternate_target}` \u4e4b `{alternate_service}` \u670d\u52d9\u3002", + "title": "{deprecated_service} \u670d\u52d9\u5c07\u79fb\u9664" + } + } + }, + "title": "{deprecated_service} \u670d\u52d9\u5c07\u79fb\u9664" + } } } \ No newline at end of file diff --git a/homeassistant/components/nam/config_flow.py b/homeassistant/components/nam/config_flow.py index 3dc2d7f0ba0..20021f1e6d4 100644 --- a/homeassistant/components/nam/config_flow.py +++ b/homeassistant/components/nam/config_flow.py @@ -209,12 +209,12 @@ class NAMFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await async_check_credentials(self.hass, self.host, user_input) except (ApiError, AuthFailed, ClientConnectorError, asyncio.TimeoutError): return self.async_abort(reason="reauth_unsuccessful") - else: - self.hass.config_entries.async_update_entry( - self.entry, data={**user_input, CONF_HOST: self.host} - ) - await self.hass.config_entries.async_reload(self.entry.entry_id) - return self.async_abort(reason="reauth_successful") + + self.hass.config_entries.async_update_entry( + self.entry, data={**user_input, CONF_HOST: self.host} + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") return self.async_show_form( step_id="reauth_confirm", diff --git a/homeassistant/components/nam/diagnostics.py b/homeassistant/components/nam/diagnostics.py index 759a3cf976b..f3e4a20cc76 100644 --- a/homeassistant/components/nam/diagnostics.py +++ b/homeassistant/components/nam/diagnostics.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import asdict +from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry @@ -16,7 +17,7 @@ TO_REDACT = {CONF_PASSWORD, CONF_USERNAME} async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/nam/sensor.py b/homeassistant/components/nam/sensor.py index 13ed5675b3c..5dab563d409 100644 --- a/homeassistant/components/nam/sensor.py +++ b/homeassistant/components/nam/sensor.py @@ -1,6 +1,7 @@ """Support for the Nettigo Air Monitor service.""" from __future__ import annotations +from dataclasses import dataclass from datetime import datetime, timedelta import logging from typing import cast @@ -70,208 +71,224 @@ PARALLEL_UPDATES = 1 _LOGGER = logging.getLogger(__name__) -SENSORS: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( +AQI_LEVEL_STATE_MAPPING = { + "very low": "very_low", + "low": "low", + "medium": "medium", + "high": "high", + "very high": "very_high", +} + + +@dataclass +class NAMSensorEntityDescription(SensorEntityDescription): + """Describes NAM sensor entity.""" + + mapping: dict[str, str] | None = None + + +SENSORS: tuple[NAMSensorEntityDescription, ...] = ( + NAMSensorEntityDescription( key=ATTR_BME280_HUMIDITY, name="BME280 humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_BME280_PRESSURE, name="BME280 pressure", native_unit_of_measurement=UnitOfPressure.HPA, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_BME280_TEMPERATURE, name="BME280 temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_BMP180_PRESSURE, name="BMP180 pressure", native_unit_of_measurement=UnitOfPressure.HPA, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_BMP180_TEMPERATURE, name="BMP180 temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_BMP280_PRESSURE, name="BMP280 pressure", native_unit_of_measurement=UnitOfPressure.HPA, device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_BMP280_TEMPERATURE, name="BMP280 temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_HECA_HUMIDITY, name="HECA humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_HECA_TEMPERATURE, name="HECA temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_MHZ14A_CARBON_DIOXIDE, name="MH-Z14A carbon dioxide", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, device_class=SensorDeviceClass.CO2, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_PMSX003_CAQI, name="PMSx003 CAQI", icon="mdi:air-filter", ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_PMSX003_CAQI_LEVEL, name="PMSx003 CAQI level", icon="mdi:air-filter", device_class=SensorDeviceClass.ENUM, - options=["very low", "low", "medium", "high", "very high"], + mapping=AQI_LEVEL_STATE_MAPPING, translation_key="caqi_level", ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_PMSX003_P0, name="PMSx003 particulate matter 1.0", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM1, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_PMSX003_P1, name="PMSx003 particulate matter 10", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM10, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_PMSX003_P2, name="PMSx003 particulate matter 2.5", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_SDS011_CAQI, name="SDS011 CAQI", icon="mdi:air-filter", ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_SDS011_CAQI_LEVEL, name="SDS011 CAQI level", icon="mdi:air-filter", device_class=SensorDeviceClass.ENUM, - options=["very low", "low", "medium", "high", "very high"], + mapping=AQI_LEVEL_STATE_MAPPING, translation_key="caqi_level", ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_SDS011_P1, name="SDS011 particulate matter 10", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM10, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_SDS011_P2, name="SDS011 particulate matter 2.5", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_SHT3X_HUMIDITY, name="SHT3X humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_SHT3X_TEMPERATURE, name="SHT3X temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_SPS30_CAQI, name="SPS30 CAQI", icon="mdi:air-filter", ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_SPS30_CAQI_LEVEL, name="SPS30 CAQI level", icon="mdi:air-filter", device_class=SensorDeviceClass.ENUM, - options=["very low", "low", "medium", "high", "very high"], + mapping=AQI_LEVEL_STATE_MAPPING, translation_key="caqi_level", ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_SPS30_P0, name="SPS30 particulate matter 1.0", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM1, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_SPS30_P1, name="SPS30 particulate matter 10", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM10, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_SPS30_P2, name="SPS30 particulate matter 2.5", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, device_class=SensorDeviceClass.PM25, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_SPS30_P4, name="SPS30 particulate matter 4.0", native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, icon="mdi:molecule", state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_DHT22_HUMIDITY, name="DHT22 humidity", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_DHT22_TEMPERATURE, name="DHT22 temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_SIGNAL_STRENGTH, name="Signal strength", native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, @@ -280,7 +297,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, ), - SensorEntityDescription( + NAMSensorEntityDescription( key=ATTR_UPTIME, name="Uptime", device_class=SensorDeviceClass.TIMESTAMP, @@ -326,11 +343,12 @@ class NAMSensor(CoordinatorEntity[NAMDataUpdateCoordinator], SensorEntity): """Define an Nettigo Air Monitor sensor.""" _attr_has_entity_name = True + entity_description: NAMSensorEntityDescription def __init__( self, coordinator: NAMDataUpdateCoordinator, - description: SensorEntityDescription, + description: NAMSensorEntityDescription, ) -> None: """Initialize.""" super().__init__(coordinator) @@ -341,6 +359,11 @@ class NAMSensor(CoordinatorEntity[NAMDataUpdateCoordinator], SensorEntity): @property def native_value(self) -> StateType | datetime: """Return the state.""" + if self.entity_description.mapping is not None: + return self.entity_description.mapping[ + cast(str, getattr(self.coordinator.data, self.entity_description.key)) + ] + return cast( StateType, getattr(self.coordinator.data, self.entity_description.key) ) @@ -358,6 +381,13 @@ class NAMSensor(CoordinatorEntity[NAMDataUpdateCoordinator], SensorEntity): and getattr(self.coordinator.data, self.entity_description.key) is not None ) + @property + def options(self) -> list[str] | None: + """If the entity description provides a mapping, use that.""" + if self.entity_description.mapping: + return list(self.entity_description.mapping.values()) + return super().options + class NAMSensorUptime(NAMSensor): """Define an Nettigo Air Monitor uptime sensor.""" diff --git a/homeassistant/components/nam/strings.json b/homeassistant/components/nam/strings.json index a37bcc2983e..17983505e91 100644 --- a/homeassistant/components/nam/strings.json +++ b/homeassistant/components/nam/strings.json @@ -42,11 +42,11 @@ "sensor": { "caqi_level": { "state": { - "very low": "Very low", + "very_low": "Very low", "low": "Low", "medium": "Medium", "high": "High", - "very high": "Very high" + "very_high": "Very high" } } } diff --git a/homeassistant/components/nam/translations/bg.json b/homeassistant/components/nam/translations/bg.json index 69f6ab5783d..0c8ba0188b4 100644 --- a/homeassistant/components/nam/translations/bg.json +++ b/homeassistant/components/nam/translations/bg.json @@ -33,5 +33,15 @@ } } } + }, + "entity": { + "sensor": { + "caqi_level": { + "state": { + "very_high": "\u041c\u043d\u043e\u0433\u043e \u0432\u0438\u0441\u043e\u043a\u043e", + "very_low": "\u041c\u043d\u043e\u0433\u043e \u043d\u0438\u0441\u043a\u043e" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nam/translations/ca.json b/homeassistant/components/nam/translations/ca.json index de1d88e0b55..a95137c4e92 100644 --- a/homeassistant/components/nam/translations/ca.json +++ b/homeassistant/components/nam/translations/ca.json @@ -45,8 +45,8 @@ "high": "Alt", "low": "Baix", "medium": "Mitj\u00e0", - "very high": "Molt alt", - "very low": "Molt baix" + "very_high": "Molt alt", + "very_low": "Molt baix" } } } diff --git a/homeassistant/components/nam/translations/de.json b/homeassistant/components/nam/translations/de.json index 1e6307a5117..d949a56f101 100644 --- a/homeassistant/components/nam/translations/de.json +++ b/homeassistant/components/nam/translations/de.json @@ -45,8 +45,8 @@ "high": "Hoch", "low": "Niedrig", "medium": "Mittel", - "very high": "Sehr hoch", - "very low": "Sehr niedrig" + "very_high": "Sehr hoch", + "very_low": "Sehr niedrig" } } } diff --git a/homeassistant/components/nam/translations/el.json b/homeassistant/components/nam/translations/el.json index fe1bbc61634..3ed52df0cff 100644 --- a/homeassistant/components/nam/translations/el.json +++ b/homeassistant/components/nam/translations/el.json @@ -45,8 +45,8 @@ "high": "\u03a5\u03c8\u03b7\u03bb\u03cc", "low": "\u03a7\u03b1\u03bc\u03b7\u03bb\u03cc", "medium": "\u039c\u03b5\u03c3\u03b1\u03af\u03bf", - "very high": "\u03a0\u03bf\u03bb\u03cd \u03c5\u03c8\u03b7\u03bb\u03cc", - "very low": "\u03a0\u03bf\u03bb\u03cd \u03c7\u03b1\u03bc\u03b7\u03bb\u03cc" + "very_high": "\u03a0\u03bf\u03bb\u03cd \u03c5\u03c8\u03b7\u03bb\u03cc", + "very_low": "\u03a0\u03bf\u03bb\u03cd \u03c7\u03b1\u03bc\u03b7\u03bb\u03cc" } } } diff --git a/homeassistant/components/nam/translations/en.json b/homeassistant/components/nam/translations/en.json index 4fd31471742..74c854f1955 100644 --- a/homeassistant/components/nam/translations/en.json +++ b/homeassistant/components/nam/translations/en.json @@ -45,8 +45,8 @@ "high": "High", "low": "Low", "medium": "Medium", - "very high": "Very high", - "very low": "Very low" + "very_high": "Very high", + "very_low": "Very low" } } } diff --git a/homeassistant/components/nam/translations/es.json b/homeassistant/components/nam/translations/es.json index e76af9e18bb..4cbbc780782 100644 --- a/homeassistant/components/nam/translations/es.json +++ b/homeassistant/components/nam/translations/es.json @@ -45,8 +45,8 @@ "high": "Alto", "low": "Bajo", "medium": "Medio", - "very high": "Muy alto", - "very low": "Muy bajo" + "very_high": "Muy alto", + "very_low": "Muy bajo" } } } diff --git a/homeassistant/components/nam/translations/et.json b/homeassistant/components/nam/translations/et.json index 81f885df7f5..07e39a7d861 100644 --- a/homeassistant/components/nam/translations/et.json +++ b/homeassistant/components/nam/translations/et.json @@ -45,8 +45,8 @@ "high": "K\u00f5rge", "low": "Madal", "medium": "Keskmine", - "very high": "V\u00e4ga k\u00f5rge", - "very low": "V\u00e4ga madal" + "very_high": "V\u00e4ga k\u00f5rge", + "very_low": "V\u00e4ga madal" } } } diff --git a/homeassistant/components/nam/translations/hu.json b/homeassistant/components/nam/translations/hu.json index d31364681db..5758e84c67e 100644 --- a/homeassistant/components/nam/translations/hu.json +++ b/homeassistant/components/nam/translations/hu.json @@ -45,8 +45,8 @@ "high": "Magas", "low": "Alacsony", "medium": "K\u00f6zepes", - "very high": "Nagyon magas", - "very low": "Nagyon alacsony" + "very_high": "Nagyon magas", + "very_low": "Nagyon alacsony" } } } diff --git a/homeassistant/components/nam/translations/id.json b/homeassistant/components/nam/translations/id.json index 414c182231c..73ac6c48b23 100644 --- a/homeassistant/components/nam/translations/id.json +++ b/homeassistant/components/nam/translations/id.json @@ -45,8 +45,8 @@ "high": "Tinggi", "low": "Rendah", "medium": "Sedang", - "very high": "Sangat tinggi", - "very low": "Sangat rendah" + "very_high": "Sangat tinggi", + "very_low": "Sangat rendah" } } } diff --git a/homeassistant/components/nam/translations/it.json b/homeassistant/components/nam/translations/it.json index 805c2608d56..61a55c4e099 100644 --- a/homeassistant/components/nam/translations/it.json +++ b/homeassistant/components/nam/translations/it.json @@ -45,8 +45,8 @@ "high": "Alto", "low": "Basso", "medium": "Medio", - "very high": "Molto alto", - "very low": "Molto basso" + "very_high": "Molto alto", + "very_low": "Molto basso" } } } diff --git a/homeassistant/components/nam/translations/lv.json b/homeassistant/components/nam/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/nam/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/no.json b/homeassistant/components/nam/translations/no.json index f190bae5560..732ca1e8d54 100644 --- a/homeassistant/components/nam/translations/no.json +++ b/homeassistant/components/nam/translations/no.json @@ -45,8 +45,8 @@ "high": "H\u00f8y", "low": "Lav", "medium": "Medium", - "very high": "Veldig h\u00f8y", - "very low": "Veldig lav" + "very_high": "Veldig h\u00f8y", + "very_low": "Veldig lav" } } } diff --git a/homeassistant/components/nam/translations/pl.json b/homeassistant/components/nam/translations/pl.json index 931eb6ebab0..cb67f1fc8d7 100644 --- a/homeassistant/components/nam/translations/pl.json +++ b/homeassistant/components/nam/translations/pl.json @@ -45,8 +45,8 @@ "high": "wysoki", "low": "niski", "medium": "\u015bredni", - "very high": "bardzo wysoki", - "very low": "bardzo niski" + "very_high": "bardzo wysoki", + "very_low": "bardzo niski" } } } diff --git a/homeassistant/components/nam/translations/pt-BR.json b/homeassistant/components/nam/translations/pt-BR.json index c8b86aed38e..378f03e3eb0 100644 --- a/homeassistant/components/nam/translations/pt-BR.json +++ b/homeassistant/components/nam/translations/pt-BR.json @@ -45,8 +45,8 @@ "high": "Alto", "low": "Baixo", "medium": "M\u00e9dio", - "very high": "Muito alto", - "very low": "Muito baixo" + "very_high": "Muito alto", + "very_low": "Muito baixo" } } } diff --git a/homeassistant/components/nam/translations/ru.json b/homeassistant/components/nam/translations/ru.json index e4613f96452..ea2697743e7 100644 --- a/homeassistant/components/nam/translations/ru.json +++ b/homeassistant/components/nam/translations/ru.json @@ -45,8 +45,8 @@ "high": "\u0412\u044b\u0441\u043e\u043a\u0438\u0439", "low": "\u041d\u0438\u0437\u043a\u0438\u0439", "medium": "\u0421\u0440\u0435\u0434\u043d\u0438\u0439", - "very high": "\u041e\u0447\u0435\u043d\u044c \u0432\u044b\u0441\u043e\u043a\u0438\u0439", - "very low": "\u041e\u0447\u0435\u043d\u044c \u043d\u0438\u0437\u043a\u0438\u0439" + "very_high": "\u041e\u0447\u0435\u043d\u044c \u0432\u044b\u0441\u043e\u043a\u0438\u0439", + "very_low": "\u041e\u0447\u0435\u043d\u044c \u043d\u0438\u0437\u043a\u0438\u0439" } } } diff --git a/homeassistant/components/nam/translations/sk.json b/homeassistant/components/nam/translations/sk.json index 72e23d7485b..df67df998c4 100644 --- a/homeassistant/components/nam/translations/sk.json +++ b/homeassistant/components/nam/translations/sk.json @@ -45,8 +45,8 @@ "high": "Vysok\u00fd", "low": "N\u00edzky", "medium": "Stredn\u00fd", - "very high": "Ve\u013emi vysok\u00fd", - "very low": "Ve\u013emi n\u00edzky" + "very_high": "Ve\u013emi vysok\u00fd", + "very_low": "Ve\u013emi n\u00edzka" } } } diff --git a/homeassistant/components/nam/translations/tr.json b/homeassistant/components/nam/translations/tr.json index 34117104663..89a658b8bb8 100644 --- a/homeassistant/components/nam/translations/tr.json +++ b/homeassistant/components/nam/translations/tr.json @@ -37,5 +37,18 @@ "description": "Nettigo Air Monitor entegrasyonunu kurun." } } + }, + "entity": { + "sensor": { + "caqi_level": { + "state": { + "high": "Y\u00fcksek", + "low": "D\u00fc\u015f\u00fck", + "medium": "Orta", + "very_high": "\u00c7ok y\u00fcksek", + "very_low": "\u00c7ok d\u00fc\u015f\u00fck" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nam/translations/uk.json b/homeassistant/components/nam/translations/uk.json new file mode 100644 index 00000000000..5829a53b600 --- /dev/null +++ b/homeassistant/components/nam/translations/uk.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043d\u0435 \u0432\u0434\u0430\u043b\u0430\u0441\u044f, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u0442\u0430 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0457\u0457 \u0437\u043d\u043e\u0432\u0443." + }, + "step": { + "credentials": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c." + }, + "reauth_confirm": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0456\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0445\u043e\u0441\u0442\u0443: {host}" + } + } + }, + "entity": { + "sensor": { + "caqi_level": { + "state": { + "very_high": "\u0414\u0443\u0436\u0435 \u0432\u0438\u0441\u043e\u043a\u043e", + "very_low": "\u0414\u0443\u0436\u0435 \u043d\u0438\u0437\u044c\u043a\u043e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nam/translations/zh-Hant.json b/homeassistant/components/nam/translations/zh-Hant.json index d2f79fb92d7..2fca6ae74ad 100644 --- a/homeassistant/components/nam/translations/zh-Hant.json +++ b/homeassistant/components/nam/translations/zh-Hant.json @@ -45,8 +45,8 @@ "high": "\u9ad8", "low": "\u4f4e", "medium": "\u4e2d", - "very high": "\u6975\u9ad8", - "very low": "\u6975\u4f4e" + "very_high": "\u6975\u9ad8", + "very_low": "\u6975\u4f4e" } } } diff --git a/homeassistant/components/nanoleaf/__init__.py b/homeassistant/components/nanoleaf/__init__.py index 1d18e0078ec..4c3b0880170 100644 --- a/homeassistant/components/nanoleaf/__init__.py +++ b/homeassistant/components/nanoleaf/__init__.py @@ -41,7 +41,7 @@ class NanoleafEntryData: """Class for sharing data within the Nanoleaf integration.""" device: Nanoleaf - coordinator: DataUpdateCoordinator + coordinator: DataUpdateCoordinator[None] event_listener: asyncio.Task diff --git a/homeassistant/components/nanoleaf/button.py b/homeassistant/components/nanoleaf/button.py index 5297e98c88e..b6e48928473 100644 --- a/homeassistant/components/nanoleaf/button.py +++ b/homeassistant/components/nanoleaf/button.py @@ -27,7 +27,9 @@ async def async_setup_entry( class NanoleafIdentifyButton(NanoleafEntity, ButtonEntity): """Representation of a Nanoleaf identify button.""" - def __init__(self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator) -> None: + def __init__( + self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator[None] + ) -> None: """Initialize the Nanoleaf button.""" super().__init__(nanoleaf, coordinator) self._attr_unique_id = f"{nanoleaf.serial_no}_identify" diff --git a/homeassistant/components/nanoleaf/diagnostics.py b/homeassistant/components/nanoleaf/diagnostics.py index 61d7abea989..9783f7854f3 100644 --- a/homeassistant/components/nanoleaf/diagnostics.py +++ b/homeassistant/components/nanoleaf/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Nanoleaf.""" from __future__ import annotations +from typing import Any + from aionanoleaf import Nanoleaf from homeassistant.components.diagnostics import async_redact_data @@ -14,7 +16,7 @@ from .const import DOMAIN async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry, -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" device: Nanoleaf = hass.data[DOMAIN][config_entry.entry_id].device diff --git a/homeassistant/components/nanoleaf/entity.py b/homeassistant/components/nanoleaf/entity.py index 8df9e243d71..0fb043c4cc4 100644 --- a/homeassistant/components/nanoleaf/entity.py +++ b/homeassistant/components/nanoleaf/entity.py @@ -11,10 +11,12 @@ from homeassistant.helpers.update_coordinator import ( from .const import DOMAIN -class NanoleafEntity(CoordinatorEntity): +class NanoleafEntity(CoordinatorEntity[DataUpdateCoordinator[None]]): """Representation of a Nanoleaf entity.""" - def __init__(self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator) -> None: + def __init__( + self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator[None] + ) -> None: """Initialize an Nanoleaf entity.""" super().__init__(coordinator) self._nanoleaf = nanoleaf diff --git a/homeassistant/components/nanoleaf/light.py b/homeassistant/components/nanoleaf/light.py index 1a43cb4989a..20992594cb8 100644 --- a/homeassistant/components/nanoleaf/light.py +++ b/homeassistant/components/nanoleaf/light.py @@ -47,7 +47,9 @@ class NanoleafLight(NanoleafEntity, LightEntity): _attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS} _attr_supported_features = LightEntityFeature.EFFECT | LightEntityFeature.TRANSITION - def __init__(self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator) -> None: + def __init__( + self, nanoleaf: Nanoleaf, coordinator: DataUpdateCoordinator[None] + ) -> None: """Initialize the Nanoleaf light.""" super().__init__(nanoleaf, coordinator) self._attr_unique_id = nanoleaf.serial_no diff --git a/homeassistant/components/nanoleaf/translations/lv.json b/homeassistant/components/nanoleaf/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/nanoleaf/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/lv.json b/homeassistant/components/neato/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/neato/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/tr.json b/homeassistant/components/neato/translations/tr.json index 4c93fdc78b0..0b5f1324dfa 100644 --- a/homeassistant/components/neato/translations/tr.json +++ b/homeassistant/components/neato/translations/tr.json @@ -15,7 +15,7 @@ "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" }, "reauth_confirm": { - "title": "Kuruluma ba\u015flamak ister misiniz?" + "title": "Kurulumu ba\u015flatmak istiyor musunuz?" } } } diff --git a/homeassistant/components/nest/diagnostics.py b/homeassistant/components/nest/diagnostics.py index d350b719608..57ce4291cc6 100644 --- a/homeassistant/components/nest/diagnostics.py +++ b/homeassistant/components/nest/diagnostics.py @@ -41,7 +41,7 @@ def _async_get_nest_devices( async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" nest_devices = _async_get_nest_devices(hass, config_entry) if not nest_devices: @@ -64,7 +64,7 @@ async def async_get_device_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry, -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a device.""" nest_devices = _async_get_nest_devices(hass, config_entry) nest_device_id = next(iter(device.identifiers))[1] diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py index ee660b8062f..271706b16c0 100644 --- a/homeassistant/components/nest/legacy/__init__.py +++ b/homeassistant/components/nest/legacy/__init__.py @@ -141,7 +141,7 @@ async def async_setup_legacy_entry(hass: HomeAssistant, entry: ConfigEntry) -> b if not await hass.async_add_executor_job(hass.data[DATA_NEST].initialize): return False - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) def validate_structures(target_structures): all_structures = [structure.name for structure in nest.structures] diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 0d02e00dbbf..8487ad2b9ba 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -5,7 +5,7 @@ "dependencies": ["ffmpeg", "http", "application_credentials"], "after_dependencies": ["media_source"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.2.2"], + "requirements": ["python-nest==4.2.0", "google-nest-sdm==2.2.4"], "codeowners": ["@allenporter"], "quality_scale": "platinum", "dhcp": [ diff --git a/homeassistant/components/nest/translations/el.json b/homeassistant/components/nest/translations/el.json index 6964337c919..61138c18b96 100644 --- a/homeassistant/components/nest/translations/el.json +++ b/homeassistant/components/nest/translations/el.json @@ -22,7 +22,7 @@ "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)" + "wrong_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 (\u03ae\u03c4\u03b1\u03bd \u03c4\u03bf \u03af\u03b4\u03b9\u03bf \u03bc\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_upgrade": { @@ -44,7 +44,7 @@ "data": { "project_id": "\u0391\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" }, - "description": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ad\u03c1\u03b3\u03bf \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Nest Device Access, \u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf **\u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03ad\u03bd\u03b1 \u03c4\u03ad\u03bb\u03bf\u03c2 \u03cd\u03c8\u03bf\u03c5\u03c2 5 \u03b4\u03bf\u03bb\u03b1\u03c1\u03af\u03c9\u03bd \u0397\u03a0\u0391** \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c4\u03bf\u03c5.\n1. \u039c\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [Device Access Console]({device_access_console_url}), \u03ba\u03b1\u03b9 \u03bc\u03ad\u03c3\u03c9 \u03c4\u03b7\u03c2 \u03c1\u03bf\u03ae\u03c2 \u03c0\u03bb\u03b7\u03c1\u03c9\u03bc\u03ae\u03c2.\n1. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf **\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03ad\u03c1\u03b3\u03bf\u03c5**.\n1. \u0394\u03ce\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c3\u03c4\u03bf \u03ad\u03c1\u03b3\u03bf Device Access \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf **\u0395\u03c0\u03cc\u03bc\u03b5\u03bd\u03bf**.\n1. \u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 OAuth\n1. \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03ba\u03ac\u03bd\u03bf\u03bd\u03c4\u03b1\u03c2 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae **\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7** \u03ba\u03b1\u03b9 **\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03ad\u03c1\u03b3\u03bf\u03c5**.\n\n\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\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 \u03c3\u03b1\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 ([\u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2]({more_info_url})).\n", + "description": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ad\u03c1\u03b3\u03bf \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Nest, \u03c4\u03bf \u03bf\u03c0\u03bf\u03af\u03bf **\u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03b1\u03b2\u03bf\u03bb\u03ae \u03c3\u03c4\u03b7\u03bd Google \u03b5\u03bd\u03cc\u03c2 \u03c4\u03ad\u03bb\u03bf\u03c5\u03c2 \u03cd\u03c8\u03bf\u03c5\u03c2 5 \u03b4\u03bf\u03bb\u03b1\u03c1\u03af\u03c9\u03bd \u0397\u03a0\u0391** \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c4\u03bf\u03c5.\n1. \u039c\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd [Device Access Console]({device_access_console_url}), \u03ba\u03b1\u03b9 \u03bc\u03ad\u03c3\u03c9 \u03c4\u03b7\u03c2 \u03c1\u03bf\u03ae\u03c2 \u03c0\u03bb\u03b7\u03c1\u03c9\u03bc\u03ae\u03c2.\n1. \u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf **\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03ad\u03c1\u03b3\u03bf\u03c5**.\n1. \u0394\u03ce\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c3\u03c4\u03bf \u03ad\u03c1\u03b3\u03bf Device Access \u03ba\u03b1\u03b9 \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03bf **\u0395\u03c0\u03cc\u03bc\u03b5\u03bd\u03bf**.\n1. \u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 OAuth\n1. \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b1 \u03c3\u03c5\u03bc\u03b2\u03ac\u03bd\u03c4\u03b1 \u03ba\u03ac\u03bd\u03bf\u03bd\u03c4\u03b1\u03c2 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae **\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7** \u03ba\u03b1\u03b9 **\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03ad\u03c1\u03b3\u03bf\u03c5**.\n\n\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\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 \u03c3\u03b1\u03c2 \u03c0\u03b1\u03c1\u03b1\u03ba\u03ac\u03c4\u03c9 ([\u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2]({more_info_url})).", "title": "Nest: \u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ad\u03c1\u03b3\u03bf \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2" }, "device_project_upgrade": { diff --git a/homeassistant/components/nest/translations/id.json b/homeassistant/components/nest/translations/id.json index 28807e5a46c..4b4fdf40f52 100644 --- a/homeassistant/components/nest/translations/id.json +++ b/homeassistant/components/nest/translations/id.json @@ -20,7 +20,7 @@ "internal_error": "Kesalahan internal saat memvalidasi kode", "invalid_pin": "Invalid Kode PIN", "subscriber_error": "Kesalahan pelanggan tidak diketahui, lihat log", - "timeout": "Tenggang waktu memvalidasi kode telah habis.", + "timeout": "Tenggang waktu validasi kode habis.", "unknown": "Kesalahan yang tidak diharapkan", "wrong_project_id": "Masukkan ID Proyek Cloud yang valid (sebelumnya sama dengan ID Proyek Akses Perangkat)" }, diff --git a/homeassistant/components/nest/translations/lt.json b/homeassistant/components/nest/translations/lt.json index 629b65d347d..275e55497fb 100644 --- a/homeassistant/components/nest/translations/lt.json +++ b/homeassistant/components/nest/translations/lt.json @@ -10,5 +10,12 @@ } } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Aptiktas judesys", + "camera_person": "Aptiktas asmuo", + "camera_sound": "Aptiktas garsas" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index eee30bf455c..f116c92d67b 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -22,6 +22,12 @@ "wrong_project_id": "Voer een geldig Cloud Project ID in (found Device Acces Project ID)" }, "step": { + "cloud_project": { + "data": { + "cloud_project_id": "Google Cloud project-ID" + }, + "title": "Nest: Voer Cloud Project ID in" + }, "init": { "data": { "flow_impl": "Leverancier" diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index df461ad87a4..c286badba1e 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -44,7 +44,7 @@ "data": { "project_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, - "description": "\u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u043f\u0440\u043e\u0435\u043a\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443, \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e **Google \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043e\u043f\u043b\u0430\u0442\u0443 \u0432 \u0440\u0430\u0437\u043c\u0435\u0440\u0435 5 \u0434\u043e\u043b\u043b\u0430\u0440\u043e\u0432 \u0421\u0428\u0410**.\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u0432 [\u041a\u043e\u043d\u0441\u043e\u043b\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c]({device_access_console_url}) \u0438 \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u043e\u043f\u043b\u0430\u0442\u044b.\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 **Create project**.\n3. \u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Next**.\n4. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth.\n5. \u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0439\u0442\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f, \u043d\u0430\u0436\u0430\u0432 **Enable** \u0438 **Create project**. \n\n\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c \u043d\u0438\u0436\u0435 ([\u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435]({more_info_url})).", + "description": "\u0421\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u043f\u0440\u043e\u0435\u043a\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443, \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e **Google \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u043e\u043f\u043b\u0430\u0442\u0443 \u0432 \u0440\u0430\u0437\u043c\u0435\u0440\u0435 5 \u0434\u043e\u043b\u043b\u0430\u0440\u043e\u0432 \u0421\u0428\u0410**.\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0443 [Device Access Console]({device_access_console_url}) \u0438 \u043f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u043e\u043f\u043b\u0430\u0442\u044b.\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 **Create project**.\n3. \u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Next**.\n4. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 OAuth Client ID.\n5. \u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0443\u0439\u0442\u0435 \u0441\u043e\u0431\u044b\u0442\u0438\u044f, \u043d\u0430\u0436\u0430\u0432 **Enable** \u0438 **Create project**. \n\n\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0432 \u044d\u0442\u043e \u043f\u043e\u043b\u0435 Device Access Project ID ([\u043f\u043e\u0434\u0440\u043e\u0431\u043d\u0435\u0435]({more_info_url})).", "title": "Nest: \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, "device_project_upgrade": { diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 50578eb8223..3e489fe8ea5 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -63,7 +63,6 @@ DATA_PERSONS = "netatmo_persons" DATA_SCHEDULES = "netatmo_schedules" NETATMO_EVENT = "netatmo_event" -NETATMO_WEBHOOK_URL = None DEFAULT_DISCOVERY = True DEFAULT_PERSON = "unknown" diff --git a/homeassistant/components/netatmo/diagnostics.py b/homeassistant/components/netatmo/diagnostics.py index cac9c695f19..3a30dcd8588 100644 --- a/homeassistant/components/netatmo/diagnostics.py +++ b/homeassistant/components/netatmo/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Netatmo.""" 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 @@ -30,7 +32,7 @@ TO_REDACT = { async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" data_handler: NetatmoDataHandler = hass.data[DOMAIN][config_entry.entry_id][ DATA_HANDLER diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 765f7ab309c..69e4e8fc398 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -115,7 +115,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = ( entity_registry_enabled_default=True, native_unit_of_measurement=UnitOfPressure.MBAR, state_class=SensorStateClass.MEASUREMENT, - device_class=SensorDeviceClass.PRESSURE, + device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE, ), NetatmoSensorEntityDescription( key="pressure_trend", diff --git a/homeassistant/components/netatmo/translations/bg.json b/homeassistant/components/netatmo/translations/bg.json index d458bae9e8e..15e614060b5 100644 --- a/homeassistant/components/netatmo/translations/bg.json +++ b/homeassistant/components/netatmo/translations/bg.json @@ -17,6 +17,11 @@ } } }, + "device_automation": { + "trigger_type": { + "set_point": "\u0416\u0435\u043b\u0430\u043d\u0430\u0442\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 {entity_name} \u0435 \u0437\u0430\u0434\u0430\u0434\u0435\u043d\u0430 \u0440\u044a\u0447\u043d\u043e" + } + }, "options": { "step": { "public_weather": { diff --git a/homeassistant/components/netatmo/translations/sk.json b/homeassistant/components/netatmo/translations/sk.json index 801a8dae54e..37eb4c5a2a1 100644 --- a/homeassistant/components/netatmo/translations/sk.json +++ b/homeassistant/components/netatmo/translations/sk.json @@ -36,7 +36,7 @@ "person": "{entity_name} rozpoznala osobu", "person_away": "{entity_name} zistila, \u017ee osoba odi\u0161la", "set_point": "Cie\u013eov\u00e1 teplota {entity_name} nastaven\u00e1 manu\u00e1lne", - "therm_mode": "{entity_name} prepnut\u00e9 na \u201e{subtype}\u201c", + "therm_mode": "{entity_name} prepnut\u00e9 na \"{subtype}\"", "turned_off": "{entity_name} vypnut\u00e1", "turned_on": "{entity_name} zapnut\u00e1", "vehicle": "{entity_name} rozpoznal vozidlo" diff --git a/homeassistant/components/netgear/__init__.py b/homeassistant/components/netgear/__init__.py index c78ba024813..35f92ea622f 100644 --- a/homeassistant/components/netgear/__init__.py +++ b/homeassistant/components/netgear/__init__.py @@ -29,8 +29,8 @@ from .router import NetgearRouter _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=30) -SPEED_TEST_INTERVAL = timedelta(seconds=1800) -SCAN_INTERVAL_FIRMWARE = timedelta(seconds=18000) +SPEED_TEST_INTERVAL = timedelta(hours=2) +SCAN_INTERVAL_FIRMWARE = timedelta(hours=5) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/netgear/sensor.py b/homeassistant/components/netgear/sensor.py index 9b3b3b23e33..50239e5a3a4 100644 --- a/homeassistant/components/netgear/sensor.py +++ b/homeassistant/components/netgear/sensor.py @@ -431,6 +431,8 @@ class NetgearRouterSensorEntity(NetgearRouterCoordinatorEntity, RestoreSensor): sensor_data = await self.async_get_last_sensor_data() if sensor_data is not None: self._value = sensor_data.native_value + else: + await self.coordinator.async_request_refresh() @callback def async_update_device(self) -> None: diff --git a/homeassistant/components/netgear/translations/lv.json b/homeassistant/components/netgear/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/netgear/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear/translations/uk.json b/homeassistant/components/netgear/translations/uk.json new file mode 100644 index 00000000000..6fcd870c308 --- /dev/null +++ b/homeassistant/components/netgear/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)" + }, + "description": "\u0425\u043e\u0441\u0442 \u0437\u0430 \u0443\u043c\u043e\u0432\u0447\u0430\u043d\u043d\u044f\u043c: {host}\n\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0437\u0430 \u0443\u043c\u043e\u0432\u0447\u0430\u043d\u043d\u044f\u043c: {username}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/lt.json b/homeassistant/components/nexia/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/nexia/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/lv.json b/homeassistant/components/nexia/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/nexia/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nextdns/__init__.py b/homeassistant/components/nextdns/__init__.py index d3cf46828cd..9e67ccfa4fc 100644 --- a/homeassistant/components/nextdns/__init__.py +++ b/homeassistant/components/nextdns/__init__.py @@ -186,7 +186,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await asyncio.gather(*tasks) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nextdns/diagnostics.py b/homeassistant/components/nextdns/diagnostics.py index 077f2ca2988..2c0d313060f 100644 --- a/homeassistant/components/nextdns/diagnostics.py +++ b/homeassistant/components/nextdns/diagnostics.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import asdict +from typing import Any from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry @@ -24,7 +25,7 @@ TO_REDACT = {CONF_API_KEY, CONF_PROFILE_ID, CONF_UNIQUE_ID} async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinators = hass.data[DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/nfandroidtv/translations/el.json b/homeassistant/components/nfandroidtv/translations/el.json index 8512834e2b8..4c18b5b4156 100644 --- a/homeassistant/components/nfandroidtv/translations/el.json +++ b/homeassistant/components/nfandroidtv/translations/el.json @@ -13,7 +13,7 @@ "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." + "description": "\u0391\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b2\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03c0\u03bb\u03b7\u03c1\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03cc\u03bb\u03b5\u03c2 \u03bf\u03b9 \u03b1\u03c0\u03b1\u03b9\u03c4\u03ae\u03c3\u03b5\u03b9\u03c2." } } } diff --git a/homeassistant/components/nfandroidtv/translations/lv.json b/homeassistant/components/nfandroidtv/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/nfandroidtv/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/__init__.py b/homeassistant/components/nibe_heatpump/__init__.py index ed907a7ce6a..a7b3c0968cc 100644 --- a/homeassistant/components/nibe_heatpump/__init__.py +++ b/homeassistant/components/nibe_heatpump/__init__.py @@ -114,7 +114,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: sw_version=str(product_info.firmware_version), ) - if isinstance(connection, NibeGW): + if hasattr(connection, "PRODUCT_INFO_EVENT") and hasattr(connection, "subscribe"): connection.subscribe(connection.PRODUCT_INFO_EVENT, _on_product_info) else: reg.async_update_device(device_id=device_entry.id, model=heatpump.model.name) diff --git a/homeassistant/components/nibe_heatpump/number.py b/homeassistant/components/nibe_heatpump/number.py index 606588f7142..dda0b652638 100644 --- a/homeassistant/components/nibe_heatpump/number.py +++ b/homeassistant/components/nibe_heatpump/number.py @@ -54,6 +54,7 @@ class Number(CoilEntity, NumberEntity): self._attr_native_min_value = float(coil.min) self._attr_native_max_value = float(coil.max) + self._attr_native_step = 1 / coil.factor self._attr_native_unit_of_measurement = coil.unit self._attr_native_value = None diff --git a/homeassistant/components/nibe_heatpump/translations/cs.json b/homeassistant/components/nibe_heatpump/translations/cs.json index 5c7a625847f..fefc1832f0a 100644 --- a/homeassistant/components/nibe_heatpump/translations/cs.json +++ b/homeassistant/components/nibe_heatpump/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba", "url": "Zadan\u00e1 adresa URL nen\u00ed spr\u00e1vn\u011b zad\u00e1na ani podporov\u00e1na" }, "step": { diff --git a/homeassistant/components/nibe_heatpump/translations/el.json b/homeassistant/components/nibe_heatpump/translations/el.json index 1933e062a09..6644d9a4fc4 100644 --- a/homeassistant/components/nibe_heatpump/translations/el.json +++ b/homeassistant/components/nibe_heatpump/translations/el.json @@ -1,13 +1,13 @@ { "config": { "error": { - "address": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP. \u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IPV4.", + "address": "\u039a\u03b1\u03b8\u03bf\u03c1\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7. \u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ae \u03ad\u03bd\u03b1 \u03b5\u03c0\u03b9\u03bb\u03cd\u03c3\u03b9\u03bc\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.", "address_in_use": "\u0397 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b1\u03ba\u03c1\u03cc\u03b1\u03c3\u03b7\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae\u03b4\u03b7 \u03c3\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1.", - "model": "\u03a4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03b4\u03b5\u03bd \u03c6\u03b1\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9 modbus40", - "read": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03c4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03bb\u03af\u03b1. \u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2\" \u03ae \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP\".", + "model": "\u03a4\u03bf \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf \u03bc\u03bf\u03bd\u03c4\u03ad\u03bb\u03bf \u03b4\u03b5\u03bd \u03c6\u03b1\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9 \u03c4\u03bf MODBUS40", + "read": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03bb\u03af\u03b1. \u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03b7 \u00ab\u03b8\u03cd\u03c1\u03b1 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2\u00bb \u03ae \u00ab\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u00bb.", "unknown": "\u0391\u03c0\u03c1\u03bf\u03c3\u03b4\u03cc\u03ba\u03b7\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", "url": "\u0397 \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03b1\u03bb\u03ac \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03b7 \u03bf\u03cd\u03c4\u03b5 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9", - "write": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03c4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03c3\u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03bb\u03af\u03b1. \u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b8\u03cd\u03c1\u03b1 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2\" \u03ae \u03c4\u03b7\u03bd \"\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP\"." + "write": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 \u03c3\u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03bb\u03af\u03b1. \u0395\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03cd\u03c3\u03c4\u03b5 \u03c4\u03b7 \u00ab\u0398\u03cd\u03c1\u03b1 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2\u00bb \u03ae \u00ab\u0391\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u00bb." }, "step": { "modbus": { diff --git a/homeassistant/components/nibe_heatpump/translations/nl.json b/homeassistant/components/nibe_heatpump/translations/nl.json index 7e198e836d7..5355a473b9b 100644 --- a/homeassistant/components/nibe_heatpump/translations/nl.json +++ b/homeassistant/components/nibe_heatpump/translations/nl.json @@ -2,6 +2,13 @@ "config": { "error": { "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "menu_options": { + "nibegw": "NibeGW" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/sk.json b/homeassistant/components/nibe_heatpump/translations/sk.json index ef2a82b3553..b4ecba76952 100644 --- a/homeassistant/components/nibe_heatpump/translations/sk.json +++ b/homeassistant/components/nibe_heatpump/translations/sk.json @@ -4,10 +4,10 @@ "address": "Bola zadan\u00e1 neplatn\u00e1 vzdialen\u00e1 adresa. Adresa mus\u00ed by\u0165 IP adresa alebo rozl\u00ed\u0161ite\u013en\u00fd n\u00e1zov hostite\u013ea.", "address_in_use": "Vybran\u00fd port po\u010d\u00favania sa u\u017e v tomto syst\u00e9me pou\u017e\u00edva.", "model": "Zd\u00e1 sa, \u017ee vybran\u00fd model nepodporuje MODBUS40", - "read": "Chyba pri po\u017eiadavke na \u010d\u00edtanie z pumpy. Overte svoj \u201ePort na \u010d\u00edtanie\u201c alebo \u201eVzdialen\u00e1 adresa\u201c.", + "read": "Chyba pri po\u017eiadavke na \u010d\u00edtanie z pumpy. Overte svoj `Port na \u010d\u00edtanie` alebo `Vzdialen\u00e1 adresa`.", "unknown": "Neo\u010dak\u00e1van\u00e1 chyba", "url": "Zadan\u00e1 adresa URL nie je spr\u00e1vne vytvoren\u00e1 ani podporovan\u00e1", - "write": "Chyba pri \u017eiadosti o z\u00e1pis do pumpy. Overte svoj \u201ePort pre vzdialen\u00fd z\u00e1pis\u201c alebo \u201eVzdialen\u00e1 adresa\u201c." + "write": "Chyba pri \u017eiadosti o z\u00e1pis do pumpy. Overte svoj `Port pre vzdialen\u00fd z\u00e1pis` alebo `Vzdialen\u00e1 adresa`." }, "step": { "modbus": { diff --git a/homeassistant/components/nibe_heatpump/translations/sv.json b/homeassistant/components/nibe_heatpump/translations/sv.json index 5406e5b407f..7a7a0ea267d 100644 --- a/homeassistant/components/nibe_heatpump/translations/sv.json +++ b/homeassistant/components/nibe_heatpump/translations/sv.json @@ -7,6 +7,25 @@ "read": "Fel p\u00e5 l\u00e4sf\u00f6rfr\u00e5gan fr\u00e5n pumpen. Verifiera din \"Fj\u00e4rrl\u00e4sningsport\" eller \"Fj\u00e4rr-IP-adress\".", "unknown": "Ov\u00e4ntat fel", "write": "Fel vid skrivbeg\u00e4ran till pumpen. Verifiera din `Fj\u00e4rrskrivport` eller `Fj\u00e4rr-IP-adress`." + }, + "step": { + "modbus": { + "data": { + "modbus_url": "Modbus URL", + "model": "Modell av v\u00e4rmepump" + } + }, + "nibegw": { + "data": { + "model": "Modell av v\u00e4rmepump" + } + }, + "user": { + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/nibe_heatpump/translations/tr.json b/homeassistant/components/nibe_heatpump/translations/tr.json index 05b75d58f18..e80106d8c2c 100644 --- a/homeassistant/components/nibe_heatpump/translations/tr.json +++ b/homeassistant/components/nibe_heatpump/translations/tr.json @@ -3,14 +3,46 @@ "error": { "address": "Ge\u00e7ersiz uzak adres belirtildi. Adres bir IP adresi veya \u00e7\u00f6z\u00fclebilir bir ana bilgisayar ad\u0131 olmal\u0131d\u0131r.", "address_in_use": "Se\u00e7ilen dinleme ba\u011flant\u0131 noktas\u0131 bu sistemde zaten kullan\u0131l\u0131yor.", - "model": "Se\u00e7ilen model modbus40'\u0131 desteklemiyor gibi g\u00f6r\u00fcn\u00fcyor", - "read": "Pompadan okuma iste\u011finde hata. 'Uzaktan okuma ba\u011flant\u0131 noktas\u0131' veya 'Uzak IP adresinizi' do\u011frulay\u0131n.", + "model": "Se\u00e7ilen model MODBUS40'\u0131 desteklemiyor gibi g\u00f6r\u00fcn\u00fcyor", + "read": "Pompadan gelen okuma iste\u011finde hata. \"Uzaktan okuma ba\u011flant\u0131 noktas\u0131\" veya \"Uzak adres\"inizi do\u011frulay\u0131n.", "unknown": "Beklenmeyen hata", - "write": "Pompaya yazma iste\u011finde hata. \"Uzak yazma ba\u011flant\u0131 noktas\u0131\" veya \"Uzak IP adresi\"nizi do\u011frulay\u0131n." + "url": "Belirtilen URL d\u00fczg\u00fcn bi\u00e7imlendirilmemi\u015f veya desteklenmiyor", + "write": "Pompaya yazma iste\u011finde hata. \"Uzak yazma ba\u011flant\u0131 noktas\u0131\" veya \"Uzak adres\"inizi do\u011frulay\u0131n." }, "step": { - "user": { + "modbus": { + "data": { + "modbus_unit": "Modbus \u00dcnite Tan\u0131mlay\u0131c\u0131s\u0131", + "modbus_url": "Modbus URL'si", + "model": "Is\u0131 Pompas\u0131 Modeli" + }, + "data_description": { + "modbus_unit": "Is\u0131 Pompan\u0131z i\u00e7in \u00fcnite tan\u0131mlamas\u0131. Genellikle 0'da b\u0131rak\u0131labilir.", + "modbus_url": "Is\u0131 Pompan\u0131z veya MODBUS40 \u00fcnitenize ba\u011flant\u0131y\u0131 a\u00e7\u0131klayan Modbus URL'si. \u015eu formda olmal\u0131d\u0131r:\n - Modbus TCP ba\u011flant\u0131s\u0131 i\u00e7in `tcp://[HOST]:[PORT]`\n - Yerel bir Modbus RTU ba\u011flant\u0131s\u0131 i\u00e7in `serial://[LOCAL DEVICE]`\n - Uzak telnet tabanl\u0131 Modbus RTU ba\u011flant\u0131s\u0131 i\u00e7in `rfc2217://[HOST]:[PORT]`." + } + }, + "nibegw": { + "data": { + "ip_address": "Uzak adres", + "listening_port": "Yerel dinleme ba\u011flant\u0131 noktas\u0131", + "model": "Is\u0131 Pompas\u0131 Modeli", + "remote_read_port": "Uzaktan okuma ba\u011flant\u0131 noktas\u0131", + "remote_write_port": "Uzaktan yazma ba\u011flant\u0131 noktas\u0131" + }, + "data_description": { + "ip_address": "NibeGW biriminin adresi. Cihaz statik bir adresle yap\u0131land\u0131r\u0131lm\u0131\u015f olmal\u0131d\u0131r.", + "listening_port": "NibeGW biriminin veri g\u00f6ndermek \u00fczere yap\u0131land\u0131r\u0131ld\u0131\u011f\u0131 bu sistemdeki yerel ba\u011flant\u0131 noktas\u0131.", + "remote_read_port": "NibeGW biriminin okuma isteklerini dinledi\u011fi ba\u011flant\u0131 noktas\u0131.", + "remote_write_port": "NibeGW biriminin yazma isteklerini dinledi\u011fi ba\u011flant\u0131 noktas\u0131." + }, "description": "Entegrasyonu yap\u0131land\u0131rmaya \u00e7al\u0131\u015fmadan \u00f6nce \u015funlar\u0131 do\u011frulay\u0131n:\n - NibeGW \u00fcnitesi bir \u0131s\u0131 pompas\u0131na ba\u011fl\u0131d\u0131r.\n - Is\u0131 pompas\u0131 konfig\u00fcrasyonunda MODBUS40 aksesuar\u0131 etkinle\u015ftirildi.\n - Pompa, eksik MODBUS40 aksesuar\u0131 ile ilgili alarm durumuna ge\u00e7medi." + }, + "user": { + "description": "Pompan\u0131za ba\u011flant\u0131 y\u00f6ntemini se\u00e7in. Genel olarak, F serisi pompalar bir NibeGW \u00f6zel aksesuar\u0131 gerektirirken, S serisi pompalarda yerle\u015fik Modbus deste\u011fi bulunur.", + "menu_options": { + "modbus": "Modbus", + "nibegw": "NibeGW" + } } } } diff --git a/homeassistant/components/nina/translations/nl.json b/homeassistant/components/nina/translations/nl.json index 7c18eb1e6b6..91d54c6774f 100644 --- a/homeassistant/components/nina/translations/nl.json +++ b/homeassistant/components/nina/translations/nl.json @@ -32,10 +32,14 @@ "step": { "init": { "data": { + "_a_to_d": "Stad/provincie (A-D)", + "_e_to_h": "Stad/provincie (E-H)", "_i_to_l": "Stad/provincie (I-L)", "_m_to_q": "Stad/provincie (M-Q)", "_r_to_u": "Stad/provincie (R-U)", - "_v_to_z": "Stad/provincie (V-Z)" + "_v_to_z": "Stad/provincie (V-Z)", + "corona_filter": "Verwijder Corona waarschuwingen", + "slots": "Maximale waarschuwingen per stad/provincie" }, "title": "Opties" } diff --git a/homeassistant/components/nmap_tracker/translations/el.json b/homeassistant/components/nmap_tracker/translations/el.json index fe1a448a8be..512d55ef1e1 100644 --- a/homeassistant/components/nmap_tracker/translations/el.json +++ b/homeassistant/components/nmap_tracker/translations/el.json @@ -9,9 +9,9 @@ "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", + "exclude": "\u0394\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 (\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1\u03c4\u03b1) \u03b3\u03b9\u03b1 \u03b5\u03be\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \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", + "hosts": "\u0394\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 \u03b4\u03b9\u03ba\u03c4\u03cd\u03bf\u03c5 (\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1\u03c4\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)." diff --git a/homeassistant/components/nobo_hub/translations/lv.json b/homeassistant/components/nobo_hub/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nobo_hub/translations/uk.json b/homeassistant/components/nobo_hub/translations/uk.json new file mode 100644 index 00000000000..253bad1466d --- /dev/null +++ b/homeassistant/components/nobo_hub/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f - \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u0439 \u043d\u043e\u043c\u0435\u0440" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notify/legacy.py b/homeassistant/components/notify/legacy.py index a5d58451c5e..6046039aab1 100644 --- a/homeassistant/components/notify/legacy.py +++ b/homeassistant/components/notify/legacy.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Coroutine from functools import partial -from typing import Any, Optional, Protocol, cast +from typing import Any, Protocol, cast from homeassistant.const import CONF_DESCRIPTION, CONF_NAME from homeassistant.core import HomeAssistant, ServiceCall, callback @@ -71,7 +71,7 @@ def async_setup_legacy( p_config = {} platform = cast( - Optional[LegacyNotifyPlatform], + LegacyNotifyPlatform | None, await async_prepare_setup_platform(hass, config, DOMAIN, integration_name), ) diff --git a/homeassistant/components/notion/translations/lt.json b/homeassistant/components/notion/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/notion/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/uk.json b/homeassistant/components/notion/translations/uk.json index 1dd26aaf2a7..ef614a268bd 100644 --- a/homeassistant/components/notion/translations/uk.json +++ b/homeassistant/components/notion/translations/uk.json @@ -7,6 +7,12 @@ "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." }, "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}:" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json index 97e67f1f0a4..b552f4aa9b7 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json +++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json @@ -2,7 +2,7 @@ "domain": "nsw_rural_fire_service_feed", "name": "NSW Rural Fire Service Incidents", "documentation": "https://www.home-assistant.io/integrations/nsw_rural_fire_service_feed", - "requirements": ["aio_geojson_nsw_rfs_incidents==0.4"], + "requirements": ["aio_geojson_nsw_rfs_incidents==0.6"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", "loggers": ["aio_geojson_nsw_rfs_incidents"], diff --git a/homeassistant/components/nuheat/translations/bg.json b/homeassistant/components/nuheat/translations/bg.json index 47f6792c3b0..692ca8396fc 100644 --- a/homeassistant/components/nuheat/translations/bg.json +++ b/homeassistant/components/nuheat/translations/bg.json @@ -1,9 +1,22 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "invalid_thermostat": "\u0421\u0435\u0440\u0438\u0439\u043d\u0438\u044f\u0442 \u043d\u043e\u043c\u0435\u0440 \u043d\u0430 \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430 \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d.", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "serial_number": "\u0421\u0435\u0440\u0438\u0435\u043d \u043d\u043e\u043c\u0435\u0440 \u043d\u0430 \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\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/nuheat/translations/lt.json b/homeassistant/components/nuheat/translations/lt.json new file mode 100644 index 00000000000..f25fb2eed3c --- /dev/null +++ b/homeassistant/components/nuheat/translations/lt.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_thermostat": "Termostato serijinis numeris negalioja." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/lv.json b/homeassistant/components/nuheat/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/nuheat/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index dcb359f32d2..20309339451 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -13,7 +13,7 @@ from homeassistant import exceptions from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -33,7 +33,7 @@ from .helpers import parse_id _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.LOCK] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.LOCK, Platform.SENSOR] UPDATE_INTERVAL = timedelta(seconds=30) @@ -41,37 +41,6 @@ def _get_bridge_devices(bridge: NukiBridge) -> tuple[list[NukiLock], list[NukiOp return bridge.locks, bridge.openers -def _update_devices(devices: list[NukiDevice]) -> dict[str, set[str]]: - """ - Update the Nuki devices. - - Returns: - A dict with the events to be fired. The event type is the key and the device ids are the value - """ - - events: dict[str, set[str]] = defaultdict(set) - - for device in devices: - for level in (False, True): - try: - if isinstance(device, NukiOpener): - last_ring_action_state = device.ring_action_state - - device.update(level) - - if not last_ring_action_state and device.ring_action_state: - events["ring"].add(device.nuki_id) - else: - device.update(level) - except RequestException: - continue - - if device.state not in ERROR_STATES: - break - - return events - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Nuki entry.""" @@ -101,42 +70,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except RequestException as err: raise exceptions.ConfigEntryNotReady from err - async def async_update_data(): - """Fetch data from Nuki bridge.""" - try: - # Note: asyncio.TimeoutError and aiohttp.ClientError are already - # handled by the data update coordinator. - async with async_timeout.timeout(10): - events = await hass.async_add_executor_job( - _update_devices, locks + openers - ) - except InvalidCredentialsException as err: - raise UpdateFailed(f"Invalid credentials for Bridge: {err}") from err - except RequestException as err: - raise UpdateFailed(f"Error communicating with Bridge: {err}") from err - - ent_reg = er.async_get(hass) - for event, device_ids in events.items(): - for device_id in device_ids: - entity_id = ent_reg.async_get_entity_id( - Platform.LOCK, DOMAIN, device_id - ) - event_data = { - "entity_id": entity_id, - "type": event, - } - hass.bus.async_fire("nuki_event", event_data) - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - # Name of the data. For logging purposes. - name="nuki devices", - update_method=async_update_data, - # Polling interval. Will only be polled if there are subscribers. - update_interval=UPDATE_INTERVAL, + # Device registration for the bridge + info = bridge.info() + bridge_id = parse_id(info["ids"]["hardwareId"]) + dev_reg = device_registry.async_get(hass) + dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, bridge_id)}, + manufacturer="Nuki Home Solutions GmbH", + name=f"Nuki Bridge {bridge_id}", + model="Hardware Bridge", + sw_version=info["versions"]["firmwareVersion"], ) + coordinator = NukiCoordinator(hass, bridge, locks, openers) + hass.data[DOMAIN][entry.entry_id] = { DATA_COORDINATOR: coordinator, DATA_BRIDGE: bridge, @@ -161,7 +109,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class NukiEntity(CoordinatorEntity): +class NukiEntity(CoordinatorEntity[DataUpdateCoordinator[None]]): """An entity using CoordinatorEntity. The CoordinatorEntity class provides: @@ -178,3 +126,94 @@ class NukiEntity(CoordinatorEntity): """Pass coordinator to CoordinatorEntity.""" super().__init__(coordinator) self._nuki_device = nuki_device + + @property + def device_info(self): + """Device info for Nuki entities.""" + return { + "identifiers": {(DOMAIN, parse_id(self._nuki_device.nuki_id))}, + "name": self._nuki_device.name, + "manufacturer": "Nuki Home Solutions GmbH", + "model": self._nuki_device.device_type_str.capitalize(), + "sw_version": self._nuki_device.firmware_version, + "via_device": (DOMAIN, self.coordinator.bridge_id), + } + + +class NukiCoordinator(DataUpdateCoordinator): + """Data Update Coordinator for the Nuki integration.""" + + def __init__(self, hass, bridge, locks, openers): + """Initialize my coordinator.""" + super().__init__( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="nuki devices", + # Polling interval. Will only be polled if there are subscribers. + update_interval=UPDATE_INTERVAL, + ) + self.bridge = bridge + self.locks = locks + self.openers = openers + + @property + def bridge_id(self): + """Return the parsed id of the Nuki bridge.""" + return parse_id(self.bridge.info()["ids"]["hardwareId"]) + + async def _async_update_data(self) -> None: + """Fetch data from Nuki bridge.""" + try: + # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # handled by the data update coordinator. + async with async_timeout.timeout(10): + events = await self.hass.async_add_executor_job( + self.update_devices, self.locks + self.openers + ) + except InvalidCredentialsException as err: + raise UpdateFailed(f"Invalid credentials for Bridge: {err}") from err + except RequestException as err: + raise UpdateFailed(f"Error communicating with Bridge: {err}") from err + + ent_reg = entity_registry.async_get(self.hass) + for event, device_ids in events.items(): + for device_id in device_ids: + entity_id = ent_reg.async_get_entity_id( + Platform.LOCK, DOMAIN, device_id + ) + event_data = { + "entity_id": entity_id, + "type": event, + } + self.hass.bus.async_fire("nuki_event", event_data) + + def update_devices(self, devices: list[NukiDevice]) -> dict[str, set[str]]: + """ + Update the Nuki devices. + + Returns: + A dict with the events to be fired. The event type is the key and the device ids are the value + """ + + events: dict[str, set[str]] = defaultdict(set) + + for device in devices: + for level in (False, True): + try: + if isinstance(device, NukiOpener): + last_ring_action_state = device.ring_action_state + + device.update(level) + + if not last_ring_action_state and device.ring_action_state: + events["ring"].add(device.nuki_id) + else: + device.update(level) + except RequestException: + continue + + if device.state not in ERROR_STATES: + break + + return events diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py index 69c48133533..6e73d9c3208 100644 --- a/homeassistant/components/nuki/binary_sensor.py +++ b/homeassistant/components/nuki/binary_sensor.py @@ -9,6 +9,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import NukiEntity from .const import ATTR_NUKI_ID, DATA_COORDINATOR, DATA_LOCKS, DOMAIN as NUKI_DOMAIN @@ -19,7 +20,7 @@ async def async_setup_entry( ) -> None: """Set up the Nuki lock binary sensor.""" data = hass.data[NUKI_DOMAIN][entry.entry_id] - coordinator = data[DATA_COORDINATOR] + coordinator: DataUpdateCoordinator[None] = data[DATA_COORDINATOR] entities = [] @@ -33,13 +34,10 @@ async def async_setup_entry( class NukiDoorsensorEntity(NukiEntity, BinarySensorEntity): """Representation of a Nuki Lock Doorsensor.""" + _attr_has_entity_name = True + _attr_name = "Door sensor" _attr_device_class = BinarySensorDeviceClass.DOOR - @property - def name(self): - """Return the name of the lock.""" - return self._nuki_device.name - @property def unique_id(self) -> str: """Return a unique ID.""" diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 4b89b0d3535..45fbf726e7a 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import NukiEntity from .const import ( @@ -35,7 +36,7 @@ async def async_setup_entry( ) -> None: """Set up the Nuki lock platform.""" data = hass.data[NUKI_DOMAIN][entry.entry_id] - coordinator = data[DATA_COORDINATOR] + coordinator: DataUpdateCoordinator[None] = data[DATA_COORDINATOR] entities: list[NukiDeviceEntity] = [ NukiLockEntity(coordinator, lock) for lock in data[DATA_LOCKS] @@ -66,13 +67,9 @@ async def async_setup_entry( class NukiDeviceEntity(NukiEntity, LockEntity, ABC): """Representation of a Nuki device.""" + _attr_has_entity_name = True _attr_supported_features = LockEntityFeature.OPEN - @property - def name(self) -> str | None: - """Return the name of the lock.""" - return self._nuki_device.name - @property def unique_id(self) -> str | None: """Return a unique ID.""" diff --git a/homeassistant/components/nuki/sensor.py b/homeassistant/components/nuki/sensor.py new file mode 100644 index 00000000000..f4a9103eecb --- /dev/null +++ b/homeassistant/components/nuki/sensor.py @@ -0,0 +1,48 @@ +"""Battery sensor for the Nuki Lock.""" + +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +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 . import NukiEntity +from .const import ATTR_NUKI_ID, DATA_COORDINATOR, DATA_LOCKS, DOMAIN as NUKI_DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Nuki lock sensor.""" + data = hass.data[NUKI_DOMAIN][entry.entry_id] + coordinator = data[DATA_COORDINATOR] + + async_add_entities( + NukiBatterySensor(coordinator, lock) for lock in data[DATA_LOCKS] + ) + + +class NukiBatterySensor(NukiEntity, SensorEntity): + """Representation of a Nuki Lock Battery sensor.""" + + _attr_has_entity_name = True + _attr_name = "Battery" + _attr_native_unit_of_measurement = PERCENTAGE + _attr_device_class = SensorDeviceClass.BATTERY + _attr_entity_category = EntityCategory.DIAGNOSTIC + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self._nuki_device.nuki_id}_battery_level" + + @property + def extra_state_attributes(self): + """Return the device specific state attributes.""" + return {ATTR_NUKI_ID: self._nuki_device.nuki_id} + + @property + def native_value(self) -> float: + """Return the state of the sensor.""" + return self._nuki_device.battery_charge diff --git a/homeassistant/components/nuki/strings.json b/homeassistant/components/nuki/strings.json index 6552f08721e..32b72c74252 100644 --- a/homeassistant/components/nuki/strings.json +++ b/homeassistant/components/nuki/strings.json @@ -22,6 +22,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } diff --git a/homeassistant/components/nuki/translations/bg.json b/homeassistant/components/nuki/translations/bg.json index d97503db4ad..82fe0aee55d 100644 --- a/homeassistant/components/nuki/translations/bg.json +++ b/homeassistant/components/nuki/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { diff --git a/homeassistant/components/nuki/translations/ca.json b/homeassistant/components/nuki/translations/ca.json index e7b149349db..ca5c178dc6c 100644 --- a/homeassistant/components/nuki/translations/ca.json +++ b/homeassistant/components/nuki/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { diff --git a/homeassistant/components/nuki/translations/de.json b/homeassistant/components/nuki/translations/de.json index 0c11580b984..ac211c40ab3 100644 --- a/homeassistant/components/nuki/translations/de.json +++ b/homeassistant/components/nuki/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { diff --git a/homeassistant/components/nuki/translations/en.json b/homeassistant/components/nuki/translations/en.json index 99c43859eb0..411577c3fac 100644 --- a/homeassistant/components/nuki/translations/en.json +++ b/homeassistant/components/nuki/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Device is already configured", "reauth_successful": "Re-authentication was successful" }, "error": { diff --git a/homeassistant/components/nuki/translations/et.json b/homeassistant/components/nuki/translations/et.json index e587458bbf0..a45c6a865cb 100644 --- a/homeassistant/components/nuki/translations/et.json +++ b/homeassistant/components/nuki/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { diff --git a/homeassistant/components/nuki/translations/no.json b/homeassistant/components/nuki/translations/no.json index 0cfb713ba6a..b13365212e8 100644 --- a/homeassistant/components/nuki/translations/no.json +++ b/homeassistant/components/nuki/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", "reauth_successful": "Re-autentisering var vellykket" }, "error": { diff --git a/homeassistant/components/nuki/translations/ru.json b/homeassistant/components/nuki/translations/ru.json index a39f1429e14..4c0e9ee3ca4 100644 --- a/homeassistant/components/nuki/translations/ru.json +++ b/homeassistant/components/nuki/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "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": { diff --git a/homeassistant/components/nuki/translations/zh-Hant.json b/homeassistant/components/nuki/translations/zh-Hant.json index fb486faced1..3a2df68ef39 100644 --- a/homeassistant/components/nuki/translations/zh-Hant.json +++ b/homeassistant/components/nuki/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index dfb923a8d01..0cdb9465360 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -24,7 +24,6 @@ from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity from homeassistant.helpers.typing import ConfigType -from homeassistant.util.unit_conversion import BaseUnitConverter, TemperatureConverter from .const import ( ATTR_MAX, @@ -36,7 +35,10 @@ from .const import ( DEFAULT_STEP, DOMAIN, SERVICE_SET_VALUE, + UNIT_CONVERTERS, + NumberDeviceClass, ) +from .websocket_api import async_setup as async_setup_ws_api SCAN_INTERVAL = timedelta(seconds=30) @@ -46,297 +48,6 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) _LOGGER = logging.getLogger(__name__) - -class NumberDeviceClass(StrEnum): - """Device class for numbers.""" - - # NumberDeviceClass should be aligned with SensorDeviceClass - - APPARENT_POWER = "apparent_power" - """Apparent power. - - Unit of measurement: `VA` - """ - - AQI = "aqi" - """Air Quality Index. - - Unit of measurement: `None` - """ - - ATMOSPHERIC_PRESSURE = "atmospheric_pressure" - """Atmospheric pressure. - - Unit of measurement: `UnitOfPressure` units - """ - - BATTERY = "battery" - """Percentage of battery that is left. - - Unit of measurement: `%` - """ - - CO = "carbon_monoxide" - """Carbon Monoxide gas concentration. - - Unit of measurement: `ppm` (parts per million) - """ - - CO2 = "carbon_dioxide" - """Carbon Dioxide gas concentration. - - Unit of measurement: `ppm` (parts per million) - """ - - CURRENT = "current" - """Current. - - Unit of measurement: `A`, `mA` - """ - - DATA_RATE = "data_rate" - """Data rate. - - Unit of measurement: UnitOfDataRate - """ - - DATA_SIZE = "data_size" - """Data size. - - Unit of measurement: UnitOfInformation - """ - - DISTANCE = "distance" - """Generic distance. - - Unit of measurement: `LENGTH_*` units - - SI /metric: `mm`, `cm`, `m`, `km` - - USCS / imperial: `in`, `ft`, `yd`, `mi` - """ - - ENERGY = "energy" - """Energy. - - Unit of measurement: `Wh`, `kWh`, `MWh`, `GJ` - """ - - FREQUENCY = "frequency" - """Frequency. - - Unit of measurement: `Hz`, `kHz`, `MHz`, `GHz` - """ - - GAS = "gas" - """Gas. - - Unit of measurement: - - SI / metric: `m³` - - USCS / imperial: `ft³`, `CCF` - """ - - HUMIDITY = "humidity" - """Relative humidity. - - Unit of measurement: `%` - """ - - ILLUMINANCE = "illuminance" - """Illuminance. - - Unit of measurement: `lx` - """ - - IRRADIANCE = "irradiance" - """Irradiance. - - Unit of measurement: - - SI / metric: `W/m²` - - USCS / imperial: `BTU/(h⋅ft²)` - """ - - MOISTURE = "moisture" - """Moisture. - - Unit of measurement: `%` - """ - - MONETARY = "monetary" - """Amount of money. - - Unit of measurement: ISO4217 currency code - - See https://en.wikipedia.org/wiki/ISO_4217#Active_codes for active codes - """ - - NITROGEN_DIOXIDE = "nitrogen_dioxide" - """Amount of NO2. - - Unit of measurement: `µg/m³` - """ - - NITROGEN_MONOXIDE = "nitrogen_monoxide" - """Amount of NO. - - Unit of measurement: `µg/m³` - """ - - NITROUS_OXIDE = "nitrous_oxide" - """Amount of N2O. - - Unit of measurement: `µg/m³` - """ - - OZONE = "ozone" - """Amount of O3. - - Unit of measurement: `µg/m³` - """ - - PM1 = "pm1" - """Particulate matter <= 0.1 μm. - - Unit of measurement: `µg/m³` - """ - - PM10 = "pm10" - """Particulate matter <= 10 μm. - - Unit of measurement: `µg/m³` - """ - - PM25 = "pm25" - """Particulate matter <= 2.5 μm. - - Unit of measurement: `µg/m³` - """ - - POWER_FACTOR = "power_factor" - """Power factor. - - Unit of measurement: `%`, `None` - """ - - POWER = "power" - """Power. - - Unit of measurement: `W`, `kW` - """ - - PRECIPITATION = "precipitation" - """Precipitation. - - Unit of measurement: UnitOfPrecipitationDepth - - SI / metric: `cm`, `mm` - - USCS / imperial: `in` - """ - - PRECIPITATION_INTENSITY = "precipitation_intensity" - """Precipitation intensity. - - Unit of measurement: UnitOfVolumetricFlux - - SI /metric: `mm/d`, `mm/h` - - USCS / imperial: `in/d`, `in/h` - """ - - PRESSURE = "pressure" - """Pressure. - - Unit of measurement: - - `mbar`, `cbar`, `bar` - - `Pa`, `hPa`, `kPa` - - `inHg` - - `psi` - """ - - REACTIVE_POWER = "reactive_power" - """Reactive power. - - Unit of measurement: `var` - """ - - SIGNAL_STRENGTH = "signal_strength" - """Signal strength. - - Unit of measurement: `dB`, `dBm` - """ - - SOUND_PRESSURE = "sound_pressure" - """Sound pressure. - - Unit of measurement: `dB`, `dBA` - """ - - SPEED = "speed" - """Generic speed. - - Unit of measurement: `SPEED_*` units or `UnitOfVolumetricFlux` - - SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h` - - USCS / imperial: `in/d`, `in/h`, `ft/s`, `mph` - - Nautical: `kn` - """ - - SULPHUR_DIOXIDE = "sulphur_dioxide" - """Amount of SO2. - - Unit of measurement: `µg/m³` - """ - - TEMPERATURE = "temperature" - """Temperature. - - Unit of measurement: `°C`, `°F` - """ - - VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" - """Amount of VOC. - - Unit of measurement: `µg/m³` - """ - - VOLTAGE = "voltage" - """Voltage. - - Unit of measurement: `V`, `mV` - """ - - VOLUME = "volume" - """Generic volume. - - Unit of measurement: `VOLUME_*` units - - SI / metric: `mL`, `L`, `m³` - - USCS / imperial: `ft³`, `CCF`, `fl. oz.`, `gal` (warning: volumes expressed in - USCS/imperial units are currently assumed to be US volumes) - """ - - WATER = "water" - """Water. - - Unit of measurement: - - SI / metric: `m³`, `L` - - USCS / imperial: `ft³`, `CCF`, `gal` (warning: volumes expressed in - USCS/imperial units are currently assumed to be US volumes) - """ - - WEIGHT = "weight" - """Generic weight, represents a measurement of an object's mass. - - Weight is used instead of mass to fit with every day language. - - Unit of measurement: `MASS_*` units - - SI / metric: `µg`, `mg`, `g`, `kg` - - USCS / imperial: `oz`, `lb` - """ - - WIND_SPEED = "wind_speed" - """Wind speed. - - Unit of measurement: `SPEED_*` units - - SI /metric: `m/s`, `km/h` - - USCS / imperial: `ft/s`, `mph` - - Nautical: `kn` - """ - - DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(NumberDeviceClass)) @@ -348,10 +59,6 @@ class NumberMode(StrEnum): SLIDER = "slider" -UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = { - NumberDeviceClass.TEMPERATURE: TemperatureConverter, -} - # mypy: disallow-any-generics @@ -360,6 +67,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: component = hass.data[DOMAIN] = EntityComponent[NumberEntity]( _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) + async_setup_ws_api(hass) await component.async_setup(config) component.async_register_entity_service( @@ -424,7 +132,9 @@ class NumberEntityDescription(EntityDescription): or self.step is not None or self.unit_of_measurement is not None ): - if self.__class__.__name__ == "NumberEntityDescription": # type: ignore[unreachable] + if ( # type: ignore[unreachable] + self.__class__.__name__ == "NumberEntityDescription" + ): caller = inspect.stack()[2] module = inspect.getmodule(caller[0]) else: @@ -668,7 +378,9 @@ class NumberEntity(Entity): hasattr(self, "entity_description") and self.entity_description.unit_of_measurement is not None ): - return self.entity_description.unit_of_measurement # type: ignore[unreachable] + return ( # type: ignore[unreachable] + self.entity_description.unit_of_measurement + ) native_unit_of_measurement = self.native_unit_of_measurement diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 50390e7ab81..b3f31ac23fe 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -1,7 +1,38 @@ """Provides the constants needed for the component.""" +from __future__ import annotations from typing import Final +from homeassistant.backports.enum import StrEnum +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + LIGHT_LUX, + PERCENTAGE, + POWER_VOLT_AMPERE_REACTIVE, + SIGNAL_STRENGTH_DECIBELS, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + UnitOfApparentPower, + UnitOfDataRate, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfFrequency, + UnitOfInformation, + UnitOfIrradiance, + UnitOfLength, + UnitOfMass, + UnitOfPower, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSoundPressure, + UnitOfSpeed, + UnitOfTemperature, + UnitOfVolume, + UnitOfVolumetricFlux, +) +from homeassistant.util.unit_conversion import BaseUnitConverter, TemperatureConverter + ATTR_VALUE = "value" ATTR_MIN = "min" ATTR_MAX = "max" @@ -19,3 +50,357 @@ SERVICE_SET_VALUE = "set_value" MODE_AUTO: Final = "auto" MODE_BOX: Final = "box" MODE_SLIDER: Final = "slider" + + +class NumberDeviceClass(StrEnum): + """Device class for numbers.""" + + # NumberDeviceClass should be aligned with NumberDeviceClass + + APPARENT_POWER = "apparent_power" + """Apparent power. + + Unit of measurement: `VA` + """ + + AQI = "aqi" + """Air Quality Index. + + Unit of measurement: `None` + """ + + ATMOSPHERIC_PRESSURE = "atmospheric_pressure" + """Atmospheric pressure. + + Unit of measurement: `UnitOfPressure` units + """ + + BATTERY = "battery" + """Percentage of battery that is left. + + Unit of measurement: `%` + """ + + CO = "carbon_monoxide" + """Carbon Monoxide gas concentration. + + Unit of measurement: `ppm` (parts per million) + """ + + CO2 = "carbon_dioxide" + """Carbon Dioxide gas concentration. + + Unit of measurement: `ppm` (parts per million) + """ + + CURRENT = "current" + """Current. + + Unit of measurement: `A`, `mA` + """ + + DATA_RATE = "data_rate" + """Data rate. + + Unit of measurement: UnitOfDataRate + """ + + DATA_SIZE = "data_size" + """Data size. + + Unit of measurement: UnitOfInformation + """ + + DISTANCE = "distance" + """Generic distance. + + Unit of measurement: `LENGTH_*` units + - SI /metric: `mm`, `cm`, `m`, `km` + - USCS / imperial: `in`, `ft`, `yd`, `mi` + """ + + ENERGY = "energy" + """Energy. + + Unit of measurement: `Wh`, `kWh`, `MWh`, `MJ`, `GJ` + """ + + FREQUENCY = "frequency" + """Frequency. + + Unit of measurement: `Hz`, `kHz`, `MHz`, `GHz` + """ + + GAS = "gas" + """Gas. + + Unit of measurement: + - SI / metric: `m³` + - USCS / imperial: `ft³`, `CCF` + """ + + HUMIDITY = "humidity" + """Relative humidity. + + Unit of measurement: `%` + """ + + ILLUMINANCE = "illuminance" + """Illuminance. + + Unit of measurement: `lx` + """ + + IRRADIANCE = "irradiance" + """Irradiance. + + Unit of measurement: + - SI / metric: `W/m²` + - USCS / imperial: `BTU/(h⋅ft²)` + """ + + MOISTURE = "moisture" + """Moisture. + + Unit of measurement: `%` + """ + + MONETARY = "monetary" + """Amount of money. + + Unit of measurement: ISO4217 currency code + + See https://en.wikipedia.org/wiki/ISO_4217#Active_codes for active codes + """ + + NITROGEN_DIOXIDE = "nitrogen_dioxide" + """Amount of NO2. + + Unit of measurement: `µg/m³` + """ + + NITROGEN_MONOXIDE = "nitrogen_monoxide" + """Amount of NO. + + Unit of measurement: `µg/m³` + """ + + NITROUS_OXIDE = "nitrous_oxide" + """Amount of N2O. + + Unit of measurement: `µg/m³` + """ + + OZONE = "ozone" + """Amount of O3. + + Unit of measurement: `µg/m³` + """ + + PM1 = "pm1" + """Particulate matter <= 0.1 μm. + + Unit of measurement: `µg/m³` + """ + + PM10 = "pm10" + """Particulate matter <= 10 μm. + + Unit of measurement: `µg/m³` + """ + + PM25 = "pm25" + """Particulate matter <= 2.5 μm. + + Unit of measurement: `µg/m³` + """ + + POWER_FACTOR = "power_factor" + """Power factor. + + Unit of measurement: `%`, `None` + """ + + POWER = "power" + """Power. + + Unit of measurement: `W`, `kW` + """ + + PRECIPITATION = "precipitation" + """Accumulated precipitation. + + Unit of measurement: UnitOfPrecipitationDepth + - SI / metric: `cm`, `mm` + - USCS / imperial: `in` + """ + + PRECIPITATION_INTENSITY = "precipitation_intensity" + """Precipitation intensity. + + Unit of measurement: UnitOfVolumetricFlux + - SI /metric: `mm/d`, `mm/h` + - USCS / imperial: `in/d`, `in/h` + """ + + PRESSURE = "pressure" + """Pressure. + + Unit of measurement: + - `mbar`, `cbar`, `bar` + - `Pa`, `hPa`, `kPa` + - `inHg` + - `psi` + """ + + REACTIVE_POWER = "reactive_power" + """Reactive power. + + Unit of measurement: `var` + """ + + SIGNAL_STRENGTH = "signal_strength" + """Signal strength. + + Unit of measurement: `dB`, `dBm` + """ + + SOUND_PRESSURE = "sound_pressure" + """Sound pressure. + + Unit of measurement: `dB`, `dBA` + """ + + SPEED = "speed" + """Generic speed. + + Unit of measurement: `SPEED_*` units or `UnitOfVolumetricFlux` + - SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h` + - USCS / imperial: `in/d`, `in/h`, `ft/s`, `mph` + - Nautical: `kn` + """ + + SULPHUR_DIOXIDE = "sulphur_dioxide" + """Amount of SO2. + + Unit of measurement: `µg/m³` + """ + + TEMPERATURE = "temperature" + """Temperature. + + Unit of measurement: `°C`, `°F`, `K` + """ + + VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" + """Amount of VOC. + + Unit of measurement: `µg/m³` + """ + + VOLTAGE = "voltage" + """Voltage. + + Unit of measurement: `V`, `mV` + """ + + VOLUME = "volume" + """Generic volume. + + Unit of measurement: `VOLUME_*` units + - SI / metric: `mL`, `L`, `m³` + - USCS / imperial: `ft³`, `CCF`, `fl. oz.`, `gal` (warning: volumes expressed in + USCS/imperial units are currently assumed to be US volumes) + """ + + WATER = "water" + """Water. + + Unit of measurement: + - SI / metric: `m³`, `L` + - USCS / imperial: `ft³`, `CCF`, `gal` (warning: volumes expressed in + USCS/imperial units are currently assumed to be US volumes) + """ + + WEIGHT = "weight" + """Generic weight, represents a measurement of an object's mass. + + Weight is used instead of mass to fit with every day language. + + Unit of measurement: `MASS_*` units + - SI / metric: `µg`, `mg`, `g`, `kg` + - USCS / imperial: `oz`, `lb` + """ + + WIND_SPEED = "wind_speed" + """Wind speed. + + Unit of measurement: `SPEED_*` units + - SI /metric: `m/s`, `km/h` + - USCS / imperial: `ft/s`, `mph` + - Nautical: `kn` + """ + + +DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = { + NumberDeviceClass.APPARENT_POWER: set(UnitOfApparentPower), + NumberDeviceClass.AQI: {None}, + NumberDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure), + NumberDeviceClass.BATTERY: {PERCENTAGE}, + NumberDeviceClass.CO: {CONCENTRATION_PARTS_PER_MILLION}, + NumberDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION}, + NumberDeviceClass.CURRENT: set(UnitOfElectricCurrent), + NumberDeviceClass.DATA_RATE: set(UnitOfDataRate), + NumberDeviceClass.DATA_SIZE: set(UnitOfInformation), + NumberDeviceClass.DISTANCE: set(UnitOfLength), + NumberDeviceClass.ENERGY: set(UnitOfEnergy), + NumberDeviceClass.FREQUENCY: set(UnitOfFrequency), + NumberDeviceClass.GAS: { + UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, + UnitOfVolume.CUBIC_METERS, + }, + NumberDeviceClass.HUMIDITY: {PERCENTAGE}, + NumberDeviceClass.ILLUMINANCE: {LIGHT_LUX}, + NumberDeviceClass.IRRADIANCE: set(UnitOfIrradiance), + NumberDeviceClass.MOISTURE: {PERCENTAGE}, + NumberDeviceClass.NITROGEN_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + NumberDeviceClass.NITROGEN_MONOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + NumberDeviceClass.NITROUS_OXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + NumberDeviceClass.OZONE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + NumberDeviceClass.PM1: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + NumberDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + NumberDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + NumberDeviceClass.POWER_FACTOR: {PERCENTAGE, None}, + NumberDeviceClass.POWER: {UnitOfPower.WATT, UnitOfPower.KILO_WATT}, + NumberDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth), + NumberDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux), + NumberDeviceClass.PRESSURE: set(UnitOfPressure), + NumberDeviceClass.REACTIVE_POWER: {POWER_VOLT_AMPERE_REACTIVE}, + NumberDeviceClass.SIGNAL_STRENGTH: { + SIGNAL_STRENGTH_DECIBELS, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + }, + NumberDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure), + NumberDeviceClass.SPEED: set(UnitOfSpeed).union(set(UnitOfVolumetricFlux)), + NumberDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + NumberDeviceClass.TEMPERATURE: set(UnitOfTemperature), + NumberDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: { + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + }, + NumberDeviceClass.VOLTAGE: set(UnitOfElectricPotential), + NumberDeviceClass.VOLUME: set(UnitOfVolume), + NumberDeviceClass.WATER: { + UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, + UnitOfVolume.CUBIC_METERS, + UnitOfVolume.GALLONS, + UnitOfVolume.LITERS, + }, + NumberDeviceClass.WEIGHT: set(UnitOfMass), + NumberDeviceClass.WIND_SPEED: set(UnitOfSpeed), +} + +UNIT_CONVERTERS: dict[str, type[BaseUnitConverter]] = { + NumberDeviceClass.TEMPERATURE: TemperatureConverter, +} diff --git a/homeassistant/components/number/websocket_api.py b/homeassistant/components/number/websocket_api.py new file mode 100644 index 00000000000..eca280d7d43 --- /dev/null +++ b/homeassistant/components/number/websocket_api.py @@ -0,0 +1,35 @@ +"""The sensor websocket API.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant, callback + +from .const import DEVICE_CLASS_UNITS, UNIT_CONVERTERS + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Set up the number websocket API.""" + websocket_api.async_register_command(hass, ws_device_class_units) + + +@callback +@websocket_api.websocket_command( + { + vol.Required("type"): "number/device_class_convertible_units", + vol.Required("device_class"): str, + } +) +def ws_device_class_units( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: + """Return supported units for a device class.""" + device_class = msg["device_class"] + convertible_units = set() + if device_class in UNIT_CONVERTERS and device_class in DEVICE_CLASS_UNITS: + convertible_units = DEVICE_CLASS_UNITS[device_class] + connection.send_result(msg["id"], {"units": convertible_units}) diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 9aaeb830173..50f05503d0f 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -514,6 +514,55 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), + "input.bypass.frequency": SensorEntityDescription( + key="input.bypass.frequency", + name="Input Bypass Frequency", + native_unit_of_measurement=UnitOfFrequency.HERTZ, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "input.bypass.phases": SensorEntityDescription( + key="input.bypass.phases", + name="Input Bypass Phases", + icon="mdi:information-outline", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "input.current": SensorEntityDescription( + key="input.current", + name="Input Current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "input.phases": SensorEntityDescription( + key="input.phases", + name="Input Phases", + icon="mdi:information-outline", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "input.realpower": SensorEntityDescription( + key="input.realpower", + name="Current Input Real Power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "output.power.nominal": SensorEntityDescription( + key="output.power.nominal", + name="Nominal Output Power", + native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, + device_class=SensorDeviceClass.APPARENT_POWER, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), "output.current": SensorEntityDescription( key="output.current", name="Output Current", @@ -563,6 +612,39 @@ SENSOR_TYPES: Final[dict[str, SensorEntityDescription]] = { entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), + "output.phases": SensorEntityDescription( + key="output.phases", + name="Output Phases", + icon="mdi:information-outline", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "output.power": SensorEntityDescription( + key="output.power", + name="Output Apparent Power", + native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, + device_class=SensorDeviceClass.APPARENT_POWER, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "output.realpower": SensorEntityDescription( + key="output.realpower", + name="Current Output Real Power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), + "output.realpower.nominal": SensorEntityDescription( + key="output.realpower.nominal", + name="Nominal Output Real Power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + ), "ambient.humidity": SensorEntityDescription( key="ambient.humidity", name="Ambient Humidity", diff --git a/homeassistant/components/nut/translations/bg.json b/homeassistant/components/nut/translations/bg.json index 0ea2b4d6cb3..7d0afe648a9 100644 --- a/homeassistant/components/nut/translations/bg.json +++ b/homeassistant/components/nut/translations/bg.json @@ -4,6 +4,11 @@ "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" }, "step": { + "ups": { + "data": { + "alias": "\u041f\u0441\u0435\u0432\u0434\u043e\u043d\u0438\u043c" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/nut/translations/lv.json b/homeassistant/components/nut/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/nut/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py index 051402af9fe..fb8f9bf0c76 100644 --- a/homeassistant/components/nws/__init__.py +++ b/homeassistant/components/nws/__init__.py @@ -37,7 +37,7 @@ FAILED_SCAN_INTERVAL = datetime.timedelta(minutes=1) DEBOUNCE_TIME = 60 # in seconds -def base_unique_id(latitude, longitude): +def base_unique_id(latitude: float, longitude: float) -> str: """Return unique id for entries in configuration.""" return f"{latitude}_{longitude}" @@ -174,7 +174,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -def device_info(latitude, longitude) -> DeviceInfo: +def device_info(latitude: float, longitude: float) -> DeviceInfo: """Return device registry information.""" return DeviceInfo( entry_type=DeviceEntryType.SERVICE, diff --git a/homeassistant/components/nws/config_flow.py b/homeassistant/components/nws/config_flow.py index 517062e23d8..10eab390917 100644 --- a/homeassistant/components/nws/config_flow.py +++ b/homeassistant/components/nws/config_flow.py @@ -1,5 +1,8 @@ """Config flow for National Weather Service (NWS) integration.""" +from __future__ import annotations + import logging +from typing import Any import aiohttp from pynws import SimpleNWS @@ -7,6 +10,7 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -16,7 +20,9 @@ from .const import CONF_STATION, DOMAIN _LOGGER = logging.getLogger(__name__) -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. @@ -44,9 +50,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - 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] = {} if user_input is not None: await self.async_set_unique_id( base_unique_id(user_input[CONF_LATITUDE], user_input[CONF_LONGITUDE]) diff --git a/homeassistant/components/nws/const.py b/homeassistant/components/nws/const.py index 7a07c1cf7c0..96844edd800 100644 --- a/homeassistant/components/nws/const.py +++ b/homeassistant/components/nws/const.py @@ -28,7 +28,7 @@ ATTRIBUTION = "Data from National Weather Service/NOAA" ATTR_FORECAST_DETAILED_DESCRIPTION = "detailed_description" ATTR_FORECAST_DAYTIME = "daytime" -CONDITION_CLASSES = { +CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_EXCEPTIONAL: [ "Tornado", "Hurricane conditions", diff --git a/homeassistant/components/nws/sensor.py b/homeassistant/components/nws/sensor.py index 06d29c830a0..61f823de8e6 100644 --- a/homeassistant/components/nws/sensor.py +++ b/homeassistant/components/nws/sensor.py @@ -2,6 +2,8 @@ from __future__ import annotations from dataclasses import dataclass +from types import MappingProxyType +from typing import Any from pynws import SimpleNWS @@ -174,11 +176,11 @@ class NWSSensor(CoordinatorEntity[NwsDataUpdateCoordinator], SensorEntity): def __init__( self, hass: HomeAssistant, - entry_data, - hass_data, + entry_data: MappingProxyType[str, Any], + hass_data: dict[str, Any], description: NWSSensorEntityDescription, - station, - ): + station: str, + ) -> None: """Initialise the platform with a data instance.""" super().__init__(hass_data[COORDINATOR_OBSERVATION]) self._nws: SimpleNWS = hass_data[NWS_DATA] @@ -191,7 +193,7 @@ class NWSSensor(CoordinatorEntity[NwsDataUpdateCoordinator], SensorEntity): self._attr_native_unit_of_measurement = description.unit_convert @property - def native_value(self): + def native_value(self) -> float | None: """Return the state.""" value = self._nws.observation.get(self.entity_description.key) if value is None: @@ -224,7 +226,7 @@ class NWSSensor(CoordinatorEntity[NwsDataUpdateCoordinator], SensorEntity): return value @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique_id for this entity.""" return f"{base_unique_id(self._latitude, self._longitude)}_{self.entity_description.key}" diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 341f35353ef..b7982247ab2 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -1,4 +1,9 @@ """Support for NWS weather service.""" +from __future__ import annotations + +from types import MappingProxyType +from typing import TYPE_CHECKING, Any + from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, @@ -8,6 +13,7 @@ from homeassistant.components.weather import ( ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + Forecast, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry @@ -24,6 +30,7 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow from homeassistant.util.unit_conversion import SpeedConverter, TemperatureConverter +from homeassistant.util.unit_system import UnitSystem from . import base_unique_id, device_info from .const import ( @@ -45,14 +52,16 @@ from .const import ( PARALLEL_UPDATES = 0 -def convert_condition(time, weather): +def convert_condition( + time: str, weather: tuple[tuple[str, int | None], ...] +) -> tuple[str, int | None]: """ Convert NWS codes to HA condition. Choose first condition in CONDITION_CLASSES that exists in weather code. If no match is found, return first condition from NWS """ - conditions = [w[0] for w in weather] + conditions: list[str] = [w[0] for w in weather] prec_probs = [w[1] or 0 for w in weather] # Choose condition with highest priority. @@ -88,12 +97,27 @@ async def async_setup_entry( ) +if TYPE_CHECKING: + + class NWSForecast(Forecast): + """Forecast with extra fields needed for NWS.""" + + detailed_description: str | None + daytime: bool | None + + class NWSWeather(WeatherEntity): """Representation of a weather condition.""" _attr_should_poll = False - def __init__(self, entry_data, hass_data, mode, units): + def __init__( + self, + entry_data: MappingProxyType[str, Any], + hass_data: dict[str, Any], + mode: str, + units: UnitSystem, + ) -> None: """Initialise the platform with a data instance and station name.""" self.nws = hass_data[NWS_DATA] self.latitude = entry_data[CONF_LATITUDE] @@ -132,67 +156,67 @@ class NWSWeather(WeatherEntity): self.async_write_ha_state() @property - def attribution(self): + def attribution(self) -> str: """Return the attribution.""" return ATTRIBUTION @property - def name(self): + def name(self) -> str: """Return the name of the station.""" return f"{self.station} {self.mode.title()}" @property - def native_temperature(self): + def native_temperature(self) -> float | None: """Return the current temperature.""" if self.observation: return self.observation.get("temperature") return None @property - def native_temperature_unit(self): + def native_temperature_unit(self) -> str: """Return the current temperature unit.""" return UnitOfTemperature.CELSIUS @property - def native_pressure(self): + def native_pressure(self) -> int | None: """Return the current pressure.""" if self.observation: return self.observation.get("seaLevelPressure") return None @property - def native_pressure_unit(self): + def native_pressure_unit(self) -> str: """Return the current pressure unit.""" return UnitOfPressure.PA @property - def humidity(self): + def humidity(self) -> float | None: """Return the name of the sensor.""" if self.observation: return self.observation.get("relativeHumidity") return None @property - def native_wind_speed(self): + def native_wind_speed(self) -> float | None: """Return the current windspeed.""" if self.observation: return self.observation.get("windSpeed") return None @property - def native_wind_speed_unit(self): + def native_wind_speed_unit(self) -> str: """Return the current windspeed.""" return UnitOfSpeed.KILOMETERS_PER_HOUR @property - def wind_bearing(self): + def wind_bearing(self) -> int | None: """Return the current wind bearing (degrees).""" if self.observation: return self.observation.get("windDirection") return None @property - def condition(self): + def condition(self) -> str | None: """Return current condition.""" weather = None if self.observation: @@ -205,23 +229,23 @@ class NWSWeather(WeatherEntity): return None @property - def native_visibility(self): + def native_visibility(self) -> int | None: """Return visibility.""" if self.observation: return self.observation.get("visibility") return None @property - def native_visibility_unit(self): + def native_visibility_unit(self) -> str: """Return visibility unit.""" return UnitOfLength.METERS @property - def forecast(self): + def forecast(self) -> list[Forecast] | None: """Return forecast.""" if self._forecast is None: return None - forecast = [] + forecast: list[NWSForecast] = [] for forecast_entry in self._forecast: data = { ATTR_FORECAST_DETAILED_DESCRIPTION: forecast_entry.get( @@ -262,7 +286,7 @@ class NWSWeather(WeatherEntity): return forecast @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique_id for this entity.""" return f"{base_unique_id(self.latitude, self.longitude)}_{self.mode}" diff --git a/homeassistant/components/octoprint/config_flow.py b/homeassistant/components/octoprint/config_flow.py index c1bdc623291..33aaff8976e 100644 --- a/homeassistant/components/octoprint/config_flow.py +++ b/homeassistant/components/octoprint/config_flow.py @@ -1,6 +1,7 @@ """Config flow for OctoPrint integration.""" from __future__ import annotations +import asyncio from collections.abc import Mapping import logging from typing import Any @@ -50,8 +51,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - api_key_task = None - _reauth_data = None + api_key_task: asyncio.Task[None] | None = None + _reauth_data: dict[str, Any] | None = None def __init__(self) -> None: """Handle a config flow for OctoPrint.""" diff --git a/homeassistant/components/octoprint/translations/lv.json b/homeassistant/components/octoprint/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/octoprint/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/octoprint/translations/uk.json b/homeassistant/components/octoprint/translations/uk.json new file mode 100644 index 00000000000..eda83e2be85 --- /dev/null +++ b/homeassistant/components/octoprint/translations/uk.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/strings.json b/homeassistant/components/omnilogic/strings.json index 9c55877b3b0..2bbb927fd27 100644 --- a/homeassistant/components/omnilogic/strings.json +++ b/homeassistant/components/omnilogic/strings.json @@ -14,6 +14,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, diff --git a/homeassistant/components/omnilogic/translations/bg.json b/homeassistant/components/omnilogic/translations/bg.json index 10a7388a24c..2dc13a778b9 100644 --- a/homeassistant/components/omnilogic/translations/bg.json +++ b/homeassistant/components/omnilogic/translations/bg.json @@ -1,6 +1,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", "single_instance_allowed": "\u0412\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e. \u0412\u044a\u0437\u043c\u043e\u0436\u043d\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." }, "error": { diff --git a/homeassistant/components/omnilogic/translations/ca.json b/homeassistant/components/omnilogic/translations/ca.json index 7425fbcead6..fa193366621 100644 --- a/homeassistant/components/omnilogic/translations/ca.json +++ b/homeassistant/components/omnilogic/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El compte ja est\u00e0 configurat", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "error": { diff --git a/homeassistant/components/omnilogic/translations/de.json b/homeassistant/components/omnilogic/translations/de.json index 612793ca059..4dd08c6eae5 100644 --- a/homeassistant/components/omnilogic/translations/de.json +++ b/homeassistant/components/omnilogic/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Konto wurde bereits konfiguriert", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { diff --git a/homeassistant/components/omnilogic/translations/en.json b/homeassistant/components/omnilogic/translations/en.json index 809f8a0ec28..3c778d37d1d 100644 --- a/homeassistant/components/omnilogic/translations/en.json +++ b/homeassistant/components/omnilogic/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Account is already configured", "single_instance_allowed": "Already configured. Only a single configuration possible." }, "error": { diff --git a/homeassistant/components/omnilogic/translations/et.json b/homeassistant/components/omnilogic/translations/et.json index d9803ffd352..6ee4525f5c4 100644 --- a/homeassistant/components/omnilogic/translations/et.json +++ b/homeassistant/components/omnilogic/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Kasutaja on juba seadistatud", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." }, "error": { diff --git a/homeassistant/components/omnilogic/translations/no.json b/homeassistant/components/omnilogic/translations/no.json index 15b44be91a8..caa603d5b36 100644 --- a/homeassistant/components/omnilogic/translations/no.json +++ b/homeassistant/components/omnilogic/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Kontoen er allerede konfigurert", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { diff --git a/homeassistant/components/omnilogic/translations/ru.json b/homeassistant/components/omnilogic/translations/ru.json index 51111556fb3..1ccb2f09be6 100644 --- a/homeassistant/components/omnilogic/translations/ru.json +++ b/homeassistant/components/omnilogic/translations/ru.json @@ -1,6 +1,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.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "error": { diff --git a/homeassistant/components/omnilogic/translations/zh-Hant.json b/homeassistant/components/omnilogic/translations/zh-Hant.json index 0a25890fd8a..12e954c5533 100644 --- a/homeassistant/components/omnilogic/translations/zh-Hant.json +++ b/homeassistant/components/omnilogic/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "single_instance_allowed": "\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3001\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index 43d942c8912..51817be35b8 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -166,7 +166,7 @@ class UserOnboardingView(_BaseOnboardingView): # Return authorization code for fetching tokens and connect # during onboarding. - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.components.auth import create_auth_code auth_code = create_auth_code(hass, data["client_id"], credentials) @@ -195,7 +195,7 @@ class CoreConfigOnboardingView(_BaseOnboardingView): # Integrations to set up when finishing onboarding onboard_integrations = ["met", "radio_browser"] - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.components import hassio if ( @@ -255,7 +255,7 @@ class IntegrationOnboardingView(_BaseOnboardingView): ) # Return authorization code so we can redirect user and log them in - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.components.auth import create_auth_code auth_code = create_auth_code( diff --git a/homeassistant/components/oncue/binary_sensor.py b/homeassistant/components/oncue/binary_sensor.py index 8ea637377b0..ec4eb1e6c84 100644 --- a/homeassistant/components/oncue/binary_sensor.py +++ b/homeassistant/components/oncue/binary_sensor.py @@ -34,9 +34,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensors.""" - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator: DataUpdateCoordinator[dict[str, OncueDevice]] = hass.data[DOMAIN][ + config_entry.entry_id + ] entities: list[OncueBinarySensorEntity] = [] - devices: dict[str, OncueDevice] = coordinator.data + devices = coordinator.data for device_id, device in devices.items(): entities.extend( OncueBinarySensorEntity( diff --git a/homeassistant/components/oncue/entity.py b/homeassistant/components/oncue/entity.py index 60a3826df42..f29caa2363b 100644 --- a/homeassistant/components/oncue/entity.py +++ b/homeassistant/components/oncue/entity.py @@ -14,12 +14,14 @@ from homeassistant.helpers.update_coordinator import ( from .const import CONNECTION_ESTABLISHED_KEY, DOMAIN, VALUE_UNAVAILABLE -class OncueEntity(CoordinatorEntity, Entity): +class OncueEntity( + CoordinatorEntity[DataUpdateCoordinator[dict[str, OncueDevice]]], Entity +): """Representation of an Oncue entity.""" def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[dict[str, OncueDevice]], device_id: str, device: OncueDevice, sensor: OncueSensor, diff --git a/homeassistant/components/oncue/sensor.py b/homeassistant/components/oncue/sensor.py index 0a7f8910775..01d8cb28441 100644 --- a/homeassistant/components/oncue/sensor.py +++ b/homeassistant/components/oncue/sensor.py @@ -183,9 +183,11 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up sensors.""" - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator: DataUpdateCoordinator[dict[str, OncueDevice]] = hass.data[DOMAIN][ + config_entry.entry_id + ] entities: list[OncueSensorEntity] = [] - devices: dict[str, OncueDevice] = coordinator.data + devices = coordinator.data for device_id, device in devices.items(): entities.extend( OncueSensorEntity(coordinator, device_id, device, sensor, SENSOR_MAP[key]) @@ -201,7 +203,7 @@ class OncueSensorEntity(OncueEntity, SensorEntity): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[dict[str, OncueDevice]], device_id: str, device: OncueDevice, sensor: OncueSensor, diff --git a/homeassistant/components/oncue/translations/uk.json b/homeassistant/components/oncue/translations/uk.json new file mode 100644 index 00000000000..e9180b28e78 --- /dev/null +++ b/homeassistant/components/oncue/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/api.py b/homeassistant/components/ondilo_ico/api.py index f698dcc693e..e0c6f9001c4 100644 --- a/homeassistant/components/ondilo_ico/api.py +++ b/homeassistant/components/ondilo_ico/api.py @@ -1,6 +1,7 @@ """API for Ondilo ICO bound to Home Assistant OAuth.""" from asyncio import run_coroutine_threadsafe import logging +from typing import Any from ondilo import Ondilo @@ -35,7 +36,7 @@ class OndiloClient(Ondilo): return self.session.token - def get_all_pools_data(self) -> dict: + def get_all_pools_data(self) -> list[dict[str, Any]]: """Fetch pools and add pool details and last measures to pool data.""" pools = self.get_pools() diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py index b6a3b25f3f2..129cdf50979 100644 --- a/homeassistant/components/ondilo_ico/sensor.py +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any from ondilo import OndiloError @@ -28,6 +29,7 @@ from homeassistant.helpers.update_coordinator import ( UpdateFailed, ) +from .api import OndiloClient from .const import DOMAIN SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( @@ -91,9 +93,9 @@ async def async_setup_entry( ) -> None: """Set up the Ondilo ICO sensors.""" - api = hass.data[DOMAIN][entry.entry_id] + api: OndiloClient = hass.data[DOMAIN][entry.entry_id] - async def async_update_data(): + async def async_update_data() -> list[dict[str, Any]]: """Fetch data from API endpoint. This is the place to pre-process the data to lookup tables @@ -132,12 +134,14 @@ async def async_setup_entry( async_add_entities(entities) -class OndiloICO(CoordinatorEntity, SensorEntity): +class OndiloICO( + CoordinatorEntity[DataUpdateCoordinator[list[dict[str, Any]]]], SensorEntity +): """Representation of a Sensor.""" def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[list[dict[str, Any]]], poolidx: int, description: SensorEntityDescription, ) -> None: diff --git a/homeassistant/components/onewire/manifest.json b/homeassistant/components/onewire/manifest.json index a40b19f2055..ef6f454245c 100644 --- a/homeassistant/components/onewire/manifest.json +++ b/homeassistant/components/onewire/manifest.json @@ -7,5 +7,6 @@ "requirements": ["pyownet==0.10.0.post1"], "codeowners": ["@garbled1", "@epenet"], "iot_class": "local_polling", - "loggers": ["pyownet"] + "loggers": ["pyownet"], + "quality_scale": "gold" } diff --git a/homeassistant/components/onewire/onewire_entities.py b/homeassistant/components/onewire/onewire_entities.py index 98287c01ce1..59ceb34d6fd 100644 --- a/homeassistant/components/onewire/onewire_entities.py +++ b/homeassistant/components/onewire/onewire_entities.py @@ -39,6 +39,7 @@ class OneWireEntity(Entity): ) -> None: """Initialize the entity.""" self.entity_description = description + self._last_update_success = True self._attr_unique_id = f"/{device_id}/{description.key}" self._attr_device_info = device_info self._attr_name = name @@ -69,9 +70,14 @@ class OneWireEntity(Entity): try: self._value_raw = float(self._read_value()) except protocol.Error as exc: - _LOGGER.error("Failure to read server value, got: %s", exc) + if self._last_update_success: + _LOGGER.error("Error fetching %s data: %s", self.name, exc) + self._last_update_success = False self._state = None else: + if not self._last_update_success: + self._last_update_success = True + _LOGGER.info("Fetching %s data recovered", self.name) if self.entity_description.read_mode == READ_MODE_INT: self._state = int(self._value_raw) elif self.entity_description.read_mode == READ_MODE_BOOL: diff --git a/homeassistant/components/onewire/translations/el.json b/homeassistant/components/onewire/translations/el.json index 1f70ecff348..43d4de9708d 100644 --- a/homeassistant/components/onewire/translations/el.json +++ b/homeassistant/components/onewire/translations/el.json @@ -12,7 +12,7 @@ "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 1-Wire" + "title": "\u039f\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03c9\u03bd \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae" } } }, diff --git a/homeassistant/components/onewire/translations/lv.json b/homeassistant/components/onewire/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/onewire/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/pl.json b/homeassistant/components/onewire/translations/pl.json index 523afbd98d9..d5a91584915 100644 --- a/homeassistant/components/onewire/translations/pl.json +++ b/homeassistant/components/onewire/translations/pl.json @@ -21,6 +21,14 @@ "device_not_selected": "Wybierz urz\u0105dzenia do skonfigurowania" }, "step": { + "ack_no_options": { + "data": { + "few": "Pustych", + "many": "Pustych", + "one": "Pusty", + "other": "Puste" + } + }, "configure_device": { "data": { "precision": "Dok\u0142adno\u015b\u0107 sensora" diff --git a/homeassistant/components/onvif/event.py b/homeassistant/components/onvif/event.py index 4766bf0b002..54c5b3b007b 100644 --- a/homeassistant/components/onvif/event.py +++ b/homeassistant/components/onvif/event.py @@ -19,7 +19,7 @@ from .const import LOGGER from .models import Event from .parsers import PARSERS -UNHANDLED_TOPICS = set() +UNHANDLED_TOPICS: set[str] = set() SUBSCRIPTION_ERRORS = ( Fault, asyncio.TimeoutError, diff --git a/homeassistant/components/onvif/translations/lt.json b/homeassistant/components/onvif/translations/lt.json new file mode 100644 index 00000000000..ec2d1fbbc0e --- /dev/null +++ b/homeassistant/components/onvif/translations/lt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "configure_profile": { + "data": { + "include": "Sukurti kameros subjekt\u0105" + }, + "description": "Sukurti kameros subjekt\u0105 {profile} su {resolution} skiriam\u0105ja geba?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/lv.json b/homeassistant/components/onvif/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/onvif/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/tr.json b/homeassistant/components/onvif/translations/tr.json index 3659586fe44..3aed1e0bf82 100644 --- a/homeassistant/components/onvif/translations/tr.json +++ b/homeassistant/components/onvif/translations/tr.json @@ -48,7 +48,8 @@ "onvif_devices": { "data": { "extra_arguments": "Ekstra FFMPEG arg\u00fcmanlar\u0131", - "rtsp_transport": "RTSP ta\u015f\u0131ma mekanizmas\u0131" + "rtsp_transport": "RTSP ta\u015f\u0131ma mekanizmas\u0131", + "use_wallclock_as_timestamps": "Duvar saatini zaman damgas\u0131 olarak kullanma" }, "title": "ONVIF Cihaz Se\u00e7enekleri" } diff --git a/homeassistant/components/onvif/translations/uk.json b/homeassistant/components/onvif/translations/uk.json index 30381d900e3..29408ca5471 100644 --- a/homeassistant/components/onvif/translations/uk.json +++ b/homeassistant/components/onvif/translations/uk.json @@ -11,6 +11,12 @@ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" }, "step": { + "configure": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, "configure_profile": { "data": { "include": "\u0421\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043e\u0431'\u0454\u043a\u0442 \u043a\u0430\u043c\u0435\u0440\u0438" diff --git a/homeassistant/components/open_meteo/strings.json b/homeassistant/components/open_meteo/strings.json index f2f22413403..4dd0270376b 100644 --- a/homeassistant/components/open_meteo/strings.json +++ b/homeassistant/components/open_meteo/strings.json @@ -7,6 +7,9 @@ "zone": "Zone" } } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" } } } diff --git a/homeassistant/components/open_meteo/translations/bg.json b/homeassistant/components/open_meteo/translations/bg.json index 2675f2ca117..167fc16fbb8 100644 --- a/homeassistant/components/open_meteo/translations/bg.json +++ b/homeassistant/components/open_meteo/translations/bg.json @@ -1,5 +1,8 @@ { "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": { "user": { "data": { diff --git a/homeassistant/components/open_meteo/translations/ca.json b/homeassistant/components/open_meteo/translations/ca.json index a401eeaa99e..2d0827e47e1 100644 --- a/homeassistant/components/open_meteo/translations/ca.json +++ b/homeassistant/components/open_meteo/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/open_meteo/translations/de.json b/homeassistant/components/open_meteo/translations/de.json index b73563ab5b3..8e899321359 100644 --- a/homeassistant/components/open_meteo/translations/de.json +++ b/homeassistant/components/open_meteo/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/open_meteo/translations/en.json b/homeassistant/components/open_meteo/translations/en.json index 7736b1da63e..e21cef3d920 100644 --- a/homeassistant/components/open_meteo/translations/en.json +++ b/homeassistant/components/open_meteo/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Service is already configured" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/open_meteo/translations/et.json b/homeassistant/components/open_meteo/translations/et.json index 9a59666f7f4..111ec81d1f6 100644 --- a/homeassistant/components/open_meteo/translations/et.json +++ b/homeassistant/components/open_meteo/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/open_meteo/translations/no.json b/homeassistant/components/open_meteo/translations/no.json index 8952abdfef6..778e89d36a2 100644 --- a/homeassistant/components/open_meteo/translations/no.json +++ b/homeassistant/components/open_meteo/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/open_meteo/translations/ru.json b/homeassistant/components/open_meteo/translations/ru.json index c1784e02f41..bf1b3392138 100644 --- a/homeassistant/components/open_meteo/translations/ru.json +++ b/homeassistant/components/open_meteo/translations/ru.json @@ -1,5 +1,8 @@ { "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." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/open_meteo/translations/zh-Hant.json b/homeassistant/components/open_meteo/translations/zh-Hant.json index 1e49e2d2224..ef1b658813d 100644 --- a/homeassistant/components/open_meteo/translations/zh-Hant.json +++ b/homeassistant/components/open_meteo/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/openai_conversation/__init__.py b/homeassistant/components/openai_conversation/__init__.py new file mode 100644 index 00000000000..c9d92f554ee --- /dev/null +++ b/homeassistant/components/openai_conversation/__init__.py @@ -0,0 +1,141 @@ +"""The OpenAI Conversation integration.""" +from __future__ import annotations + +from functools import partial +import logging + +import openai +from openai import error + +from homeassistant.components import conversation +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady, TemplateError +from homeassistant.helpers import area_registry, intent, template +from homeassistant.util import ulid + +from .const import DEFAULT_MODEL, DEFAULT_PROMPT + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up OpenAI Conversation from a config entry.""" + openai.api_key = entry.data[CONF_API_KEY] + + try: + await hass.async_add_executor_job( + partial(openai.Engine.list, request_timeout=10) + ) + except error.AuthenticationError as err: + _LOGGER.error("Invalid API key: %s", err) + return False + except error.OpenAIError as err: + raise ConfigEntryNotReady(err) from err + + conversation.async_set_agent(hass, entry, OpenAIAgent(hass, entry)) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload OpenAI.""" + openai.api_key = None + conversation.async_unset_agent(hass, entry) + return True + + +class OpenAIAgent(conversation.AbstractConversationAgent): + """OpenAI conversation agent.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the agent.""" + self.hass = hass + self.entry = entry + self.history: dict[str, str] = {} + + @property + def attribution(self): + """Return the attribution.""" + return {"name": "Powered by OpenAI", "url": "https://www.openai.com"} + + async def async_process( + self, user_input: conversation.ConversationInput + ) -> conversation.ConversationResult: + """Process a sentence.""" + model = DEFAULT_MODEL + + if user_input.conversation_id in self.history: + conversation_id = user_input.conversation_id + prompt = self.history[conversation_id] + else: + conversation_id = ulid.ulid() + try: + prompt = self._async_generate_prompt() + except TemplateError as err: + _LOGGER.error("Error rendering prompt: %s", err) + intent_response = intent.IntentResponse(language=user_input.language) + intent_response.async_set_error( + intent.IntentResponseErrorCode.UNKNOWN, + f"Sorry, I had a problem with my template: {err}", + ) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + + user_name = "User" + if ( + user_input.context.user_id + and ( + user := await self.hass.auth.async_get_user(user_input.context.user_id) + ) + and user.name + ): + user_name = user.name + + prompt += f"\n{user_name}: {user_input.text}\nSmart home: " + + _LOGGER.debug("Prompt for %s: %s", model, prompt) + + try: + result = await self.hass.async_add_executor_job( + partial( + openai.Completion.create, + engine=model, + prompt=prompt, + max_tokens=150, + user=conversation_id, + ) + ) + except error.OpenAIError as err: + intent_response = intent.IntentResponse(language=user_input.language) + intent_response.async_set_error( + intent.IntentResponseErrorCode.UNKNOWN, + f"Sorry, I had a problem talking to OpenAI: {err}", + ) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + + _LOGGER.debug("Response %s", result) + response = result["choices"][0]["text"].strip() + self.history[conversation_id] = prompt + response + + stripped_response = response + if response.startswith("Smart home:"): + stripped_response = response[11:].strip() + + intent_response = intent.IntentResponse(language=user_input.language) + intent_response.async_set_speech(stripped_response) + return conversation.ConversationResult( + response=intent_response, conversation_id=conversation_id + ) + + def _async_generate_prompt(self) -> str: + """Generate a prompt for the user.""" + return template.Template(DEFAULT_PROMPT, self.hass).async_render( + { + "ha_name": self.hass.config.location_name, + "areas": list(area_registry.async_get(self.hass).areas.values()), + } + ) diff --git a/homeassistant/components/openai_conversation/config_flow.py b/homeassistant/components/openai_conversation/config_flow.py new file mode 100644 index 00000000000..88253d63a44 --- /dev/null +++ b/homeassistant/components/openai_conversation/config_flow.py @@ -0,0 +1,70 @@ +"""Config flow for OpenAI Conversation integration.""" +from __future__ import annotations + +from functools import partial +import logging +from typing import Any + +import openai +from openai import error +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_KEY): str, + } +) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: + """Validate the user input allows us to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + openai.api_key = data[CONF_API_KEY] + await hass.async_add_executor_job(partial(openai.Engine.list, request_timeout=10)) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for OpenAI Conversation.""" + + 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 None: + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + try: + await validate_input(self.hass, user_input) + except error.APIConnectionError: + errors["base"] = "cannot_connect" + except error.AuthenticationError: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title="OpenAI Conversation", data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/openai_conversation/const.py b/homeassistant/components/openai_conversation/const.py new file mode 100644 index 00000000000..378548173b0 --- /dev/null +++ b/homeassistant/components/openai_conversation/const.py @@ -0,0 +1,30 @@ +"""Constants for the OpenAI Conversation integration.""" + +DOMAIN = "openai_conversation" +CONF_PROMPT = "prompt" +DEFAULT_MODEL = "text-davinci-003" +DEFAULT_PROMPT = """This smart home is controlled by Home Assistant. + +An overview of the areas and the devices in this smart home: +{%- for area in areas %} + {%- set area_info = namespace(printed=false) %} + {%- for device in area_devices(area.name) -%} + {%- if not device_attr(device, "disabled_by") and not device_attr(device, "entry_type") %} + {%- if not area_info.printed %} + +{{ area.name }}: + {%- set area_info.printed = true %} + {%- endif %} +- {{ device_attr(device, "name") }}{% if device_attr(device, "model") and device_attr(device, "model") not in device_attr(device, "name") %} ({{ device_attr(device, "model") }}){% endif %} + {%- endif %} + {%- endfor %} +{%- endfor %} + +Answer the users questions about the world truthfully. + +If the user wants to control a device, reject the request and suggest using the Home Assistant app. + +Now finish this conversation: + +Smart home: How can I assist? +""" diff --git a/homeassistant/components/openai_conversation/manifest.json b/homeassistant/components/openai_conversation/manifest.json new file mode 100644 index 00000000000..233f7b5a5b2 --- /dev/null +++ b/homeassistant/components/openai_conversation/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "openai_conversation", + "name": "OpenAI Conversation", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/openai_conversation", + "requirements": ["openai==0.26.2"], + "dependencies": ["conversation"], + "codeowners": ["@balloob"], + "iot_class": "cloud_polling", + "integration_type": "service" +} diff --git a/homeassistant/components/openai_conversation/strings.json b/homeassistant/components/openai_conversation/strings.json new file mode 100644 index 00000000000..9ebf1c64a21 --- /dev/null +++ b/homeassistant/components/openai_conversation/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + } +} diff --git a/homeassistant/components/openai_conversation/translations/en.json b/homeassistant/components/openai_conversation/translations/en.json new file mode 100644 index 00000000000..7665a5535ab --- /dev/null +++ b/homeassistant/components/openai_conversation/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_key": "API Key" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openalpr_cloud/image_processing.py b/homeassistant/components/openalpr_cloud/image_processing.py index 4440a2a9063..aa1e5ecbc0a 100644 --- a/homeassistant/components/openalpr_cloud/image_processing.py +++ b/homeassistant/components/openalpr_cloud/image_processing.py @@ -10,25 +10,36 @@ import aiohttp import async_timeout import voluptuous as vol -from homeassistant.components.image_processing import CONF_CONFIDENCE, PLATFORM_SCHEMA -from homeassistant.components.openalpr_local.image_processing import ( - ImageProcessingAlprEntity, +from homeassistant.components.image_processing import ( + ATTR_CONFIDENCE, + CONF_CONFIDENCE, + PLATFORM_SCHEMA, + ImageProcessingDeviceClass, + ImageProcessingEntity, ) from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_API_KEY, CONF_ENTITY_ID, CONF_NAME, CONF_REGION, CONF_SOURCE, ) -from homeassistant.core import HomeAssistant, split_entity_id +from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) +ATTR_PLATE = "plate" +ATTR_PLATES = "plates" +ATTR_VEHICLES = "vehicles" + +EVENT_FOUND_PLATE = "image_processing.found_plate" + OPENALPR_API_URL = "https://api.openalpr.com/v1/recognize" OPENALPR_REGIONS = [ @@ -80,6 +91,72 @@ async def async_setup_platform( async_add_entities(entities) +class ImageProcessingAlprEntity(ImageProcessingEntity): + """Base entity class for ALPR image processing.""" + + _attr_device_class = ImageProcessingDeviceClass.ALPR + + def __init__(self) -> None: + """Initialize base ALPR entity.""" + self.plates: dict[str, float] = {} + self.vehicles = 0 + + @property + def state(self): + """Return the state of the entity.""" + confidence = 0 + plate = None + + # search high plate + for i_pl, i_co in self.plates.items(): + if i_co > confidence: + confidence = i_co + plate = i_pl + return plate + + @property + def extra_state_attributes(self): + """Return device specific state attributes.""" + return {ATTR_PLATES: self.plates, ATTR_VEHICLES: self.vehicles} + + def process_plates(self, plates: dict[str, float], vehicles: int) -> None: + """Send event with new plates and store data.""" + run_callback_threadsafe( + self.hass.loop, self.async_process_plates, plates, vehicles + ).result() + + @callback + def async_process_plates(self, plates: dict[str, float], vehicles: int) -> None: + """Send event with new plates and store data. + + Plates are a dict in follow format: + { '': confidence } + This method must be run in the event loop. + """ + plates = { + plate: confidence + for plate, confidence in plates.items() + if self.confidence is None or confidence >= self.confidence + } + new_plates = set(plates) - set(self.plates) + + # Send events + for i_plate in new_plates: + self.hass.async_add_job( + self.hass.bus.async_fire, + EVENT_FOUND_PLATE, + { + ATTR_PLATE: i_plate, + ATTR_ENTITY_ID: self.entity_id, + ATTR_CONFIDENCE: plates.get(i_plate), + }, + ) + + # Update entity store + self.plates = plates + self.vehicles = vehicles + + class OpenAlprCloudEntity(ImageProcessingAlprEntity): """Representation of an OpenALPR cloud entity.""" diff --git a/homeassistant/components/openalpr_local/__init__.py b/homeassistant/components/openalpr_local/__init__.py deleted file mode 100644 index 436f15baeeb..00000000000 --- a/homeassistant/components/openalpr_local/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The openalpr_local component.""" diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py deleted file mode 100644 index 0237bc1f60c..00000000000 --- a/homeassistant/components/openalpr_local/image_processing.py +++ /dev/null @@ -1,240 +0,0 @@ -"""Component that will help set the OpenALPR local for ALPR processing.""" -from __future__ import annotations - -import asyncio -import io -import logging -import re - -import voluptuous as vol - -from homeassistant.components.image_processing import ( - ATTR_CONFIDENCE, - CONF_CONFIDENCE, - PLATFORM_SCHEMA, - ImageProcessingDeviceClass, - ImageProcessingEntity, -) -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_ENTITY_ID, - CONF_NAME, - CONF_REGION, - CONF_SOURCE, -) -from homeassistant.core import HomeAssistant, callback, split_entity_id -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.issue_registry import IssueSeverity, create_issue -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.util.async_ import run_callback_threadsafe - -_LOGGER = logging.getLogger(__name__) - -RE_ALPR_PLATE = re.compile(r"^plate\d*:") -RE_ALPR_RESULT = re.compile(r"- (\w*)\s*confidence: (\d*.\d*)") - -EVENT_FOUND_PLATE = "image_processing.found_plate" - -ATTR_PLATE = "plate" -ATTR_PLATES = "plates" -ATTR_VEHICLES = "vehicles" - -OPENALPR_REGIONS = [ - "au", - "auwide", - "br", - "eu", - "fr", - "gb", - "kr", - "kr2", - "mx", - "sg", - "us", - "vn2", -] - -CONF_ALPR_BIN = "alpr_bin" - -DEFAULT_BINARY = "alpr" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_REGION): vol.All(vol.Lower, vol.In(OPENALPR_REGIONS)), - vol.Optional(CONF_ALPR_BIN, default=DEFAULT_BINARY): cv.string, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the OpenALPR local platform.""" - create_issue( - hass, - "openalpr_local", - "pending_removal", - breaks_in_ha_version="2022.10.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="pending_removal", - ) - _LOGGER.warning( - "The OpenALPR Local is deprecated and will be removed in Home Assistant 2022.10" - ) - command = [config[CONF_ALPR_BIN], "-c", config[CONF_REGION], "-"] - confidence = config[CONF_CONFIDENCE] - - entities = [] - for camera in config[CONF_SOURCE]: - entities.append( - OpenAlprLocalEntity( - camera[CONF_ENTITY_ID], command, confidence, camera.get(CONF_NAME) - ) - ) - - async_add_entities(entities) - - -class ImageProcessingAlprEntity(ImageProcessingEntity): - """Base entity class for ALPR image processing.""" - - _attr_device_class = ImageProcessingDeviceClass.ALPR - - def __init__(self) -> None: - """Initialize base ALPR entity.""" - self.plates: dict[str, float] = {} - self.vehicles = 0 - - @property - def state(self): - """Return the state of the entity.""" - confidence = 0 - plate = None - - # search high plate - for i_pl, i_co in self.plates.items(): - if i_co > confidence: - confidence = i_co - plate = i_pl - return plate - - @property - def extra_state_attributes(self): - """Return device specific state attributes.""" - return {ATTR_PLATES: self.plates, ATTR_VEHICLES: self.vehicles} - - def process_plates(self, plates: dict[str, float], vehicles: int) -> None: - """Send event with new plates and store data.""" - run_callback_threadsafe( - self.hass.loop, self.async_process_plates, plates, vehicles - ).result() - - @callback - def async_process_plates(self, plates: dict[str, float], vehicles: int) -> None: - """Send event with new plates and store data. - - plates are a dict in follow format: - { '': confidence } - - This method must be run in the event loop. - """ - plates = { - plate: confidence - for plate, confidence in plates.items() - if self.confidence is None or confidence >= self.confidence - } - new_plates = set(plates) - set(self.plates) - - # Send events - for i_plate in new_plates: - self.hass.async_add_job( - self.hass.bus.async_fire, - EVENT_FOUND_PLATE, - { - ATTR_PLATE: i_plate, - ATTR_ENTITY_ID: self.entity_id, - ATTR_CONFIDENCE: plates.get(i_plate), - }, - ) - - # Update entity store - self.plates = plates - self.vehicles = vehicles - - -class OpenAlprLocalEntity(ImageProcessingAlprEntity): - """OpenALPR local api entity.""" - - def __init__(self, camera_entity, command, confidence, name=None): - """Initialize OpenALPR local API.""" - super().__init__() - - self._cmd = command - self._camera = camera_entity - self._confidence = confidence - - if name: - self._name = name - else: - self._name = f"OpenAlpr {split_entity_id(camera_entity)[1]}" - - @property - def confidence(self): - """Return minimum confidence for send events.""" - return self._confidence - - @property - def camera_entity(self): - """Return camera entity id from process pictures.""" - return self._camera - - @property - def name(self): - """Return the name of the entity.""" - return self._name - - async def async_process_image(self, image): - """Process image. - - This method is a coroutine. - """ - result = {} - vehicles = 0 - - alpr = await asyncio.create_subprocess_exec( - *self._cmd, - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.DEVNULL, - ) - - # Send image - stdout, _ = await alpr.communicate(input=image) - stdout = io.StringIO(str(stdout, "utf-8")) - - while True: - line = stdout.readline() - if not line: - break - - new_plates = RE_ALPR_PLATE.search(line) - new_result = RE_ALPR_RESULT.search(line) - - # Found new vehicle - if new_plates: - vehicles += 1 - continue - - # Found plate result - if new_result: - try: - result.update({new_result.group(1): float(new_result.group(2))}) - except ValueError: - continue - - self.async_process_plates(result, vehicles) diff --git a/homeassistant/components/openalpr_local/manifest.json b/homeassistant/components/openalpr_local/manifest.json deleted file mode 100644 index 8837d79369d..00000000000 --- a/homeassistant/components/openalpr_local/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "openalpr_local", - "name": "OpenALPR Local", - "documentation": "https://www.home-assistant.io/integrations/openalpr_local", - "codeowners": [], - "iot_class": "local_push" -} diff --git a/homeassistant/components/openalpr_local/strings.json b/homeassistant/components/openalpr_local/strings.json deleted file mode 100644 index b0dc80c6d06..00000000000 --- a/homeassistant/components/openalpr_local/strings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "title": "The OpenALPR Local integration is being removed", - "description": "The OpenALPR Local integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - } - } -} diff --git a/homeassistant/components/openalpr_local/translations/ca.json b/homeassistant/components/openalpr_local/translations/ca.json deleted file mode 100644 index 3617117ac4a..00000000000 --- a/homeassistant/components/openalpr_local/translations/ca.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "La integraci\u00f3 d'OpenALPR Local s'eliminar\u00e0 de Home Assistant i deixar\u00e0 d'estar disponible a la versi\u00f3 de Home Assistant 2022.10.\n\nElimina la configuraci\u00f3 YAML corresponent del fitxer configuration.yaml i reinicia Home Assistant per arreglar aquest error.", - "title": "La integraci\u00f3 OpenALPR Local est\u00e0 sent eliminada" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/de.json b/homeassistant/components/openalpr_local/translations/de.json deleted file mode 100644 index 5b5dfb52773..00000000000 --- a/homeassistant/components/openalpr_local/translations/de.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "Die lokale OpenALPR-Integration wird derzeit aus dem Home Assistant entfernt und wird ab Home Assistant 2022.10 nicht mehr verf\u00fcgbar sein.\n\nEntferne die YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", - "title": "Die lokale OpenALPR Integration wird entfernt" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/el.json b/homeassistant/components/openalpr_local/translations/el.json deleted file mode 100644 index ba56c490298..00000000000 --- a/homeassistant/components/openalpr_local/translations/el.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "\u0397 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 OpenALPR \u03b5\u03ba\u03ba\u03c1\u03b5\u03bc\u03b5\u03af \u03ba\u03b1\u03c4\u03ac\u03c1\u03b3\u03b7\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf Home Assistant 2022.10. \n\n \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", - "title": "\u0397 \u03c4\u03bf\u03c0\u03b9\u03ba\u03ae \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 OpenALPR \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/en.json b/homeassistant/components/openalpr_local/translations/en.json deleted file mode 100644 index 9bc9035515b..00000000000 --- a/homeassistant/components/openalpr_local/translations/en.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "The OpenALPR Local integration is pending removal from Home Assistant and will no longer be available as of Home Assistant 2022.10.\n\nRemove the YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", - "title": "The OpenALPR Local integration is being removed" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/es.json b/homeassistant/components/openalpr_local/translations/es.json deleted file mode 100644 index 4c1b1de6e05..00000000000 --- a/homeassistant/components/openalpr_local/translations/es.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "La integraci\u00f3n OpenALPR Local est\u00e1 pendiente de eliminaci\u00f3n de Home Assistant y ya no estar\u00e1 disponible a partir de Home Assistant 2022.10. \n\nElimina la configuraci\u00f3n YAML de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", - "title": "Se va a eliminar la integraci\u00f3n OpenALPR Local" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/et.json b/homeassistant/components/openalpr_local/translations/et.json deleted file mode 100644 index aca98183950..00000000000 --- a/homeassistant/components/openalpr_local/translations/et.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "OpenALPRi kohalik integratsioon on Home Assistantist eemaldamisel ja see ei ole enam saadaval alates Home Assistant 2022.10.\n\nProbleemi lahendamiseks eemaldage YAML-konfiguratsioon failist configuration.yaml ja k\u00e4ivitage Home Assistant uuesti.", - "title": "OpenALPR Locali integratsioon eemaldatakse" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/hu.json b/homeassistant/components/openalpr_local/translations/hu.json deleted file mode 100644 index 30232ae48cb..00000000000 --- a/homeassistant/components/openalpr_local/translations/hu.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "Az OpenALPR Local integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra v\u00e1r a Home Assistantb\u00f3l, \u00e9s a 2022.10-es Home Assistant-t\u00f3l kezdve nem lesz el\u00e9rhet\u0151.\n\nA probl\u00e9ma megold\u00e1s\u00e1hoz t\u00e1vol\u00edtsa el a YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistantot.", - "title": "Az OpenALPR Local integr\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/id.json b/homeassistant/components/openalpr_local/translations/id.json deleted file mode 100644 index 1039c96daa1..00000000000 --- a/homeassistant/components/openalpr_local/translations/id.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "Integrasi OpenALPR Local sedang menunggu penghapusan dari Home Assistant dan tidak akan lagi tersedia pada Home Assistant 2022.10.\n\nHapus konfigurasi YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", - "title": "Integrasi OpenALPR dalam proses penghapusan" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/it.json b/homeassistant/components/openalpr_local/translations/it.json deleted file mode 100644 index d4227ca7e36..00000000000 --- a/homeassistant/components/openalpr_local/translations/it.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "L'integrazione OpenALPR Local \u00e8 in attesa di rimozione da Home Assistant e non sar\u00e0 pi\u00f9 disponibile a partire da Home Assistant 2022.10. \n\nRimuovi la configurazione YAML dal file configuration.yaml e riavvia Home Assistant per risolvere questo problema.", - "title": "L'integrazione OpenALPR Local sar\u00e0 rimossa" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/ja.json b/homeassistant/components/openalpr_local/translations/ja.json deleted file mode 100644 index 2353cd9e60f..00000000000 --- a/homeassistant/components/openalpr_local/translations/ja.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "OpenALPR \u30ed\u30fc\u30ab\u30eb\u7d71\u5408\u306f\u3001Home Assistant\u304b\u3089\u306e\u524a\u9664\u304c\u4fdd\u7559\u3055\u308c\u3066\u304a\u308a\u3001Home Assistant 2022.10\u4ee5\u964d\u306f\u5229\u7528\u3067\u304d\u306a\u304f\u306a\u308a\u307e\u3059\u3002 \n\n\u3053\u306e\u554f\u984c\u3092\u89e3\u6c7a\u3059\u308b\u306b\u306f\u3001configuration.yaml\u30d5\u30a1\u30a4\u30eb\u304b\u3089YAML\u8a2d\u5b9a\u3092\u524a\u9664\u3057\u3001Home Assistant\u3092\u518d\u8d77\u52d5\u3057\u307e\u3059\u3002", - "title": "OpenALPR Local\u306e\u7d71\u5408\u306f\u524a\u9664\u3055\u308c\u3066\u3044\u307e\u3059" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/nl.json b/homeassistant/components/openalpr_local/translations/nl.json deleted file mode 100644 index 06bd27e2d56..00000000000 --- a/homeassistant/components/openalpr_local/translations/nl.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "issues": { - "pending_removal": { - "title": "De OpenALPR Local-integratie wordt verwijderd" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/no.json b/homeassistant/components/openalpr_local/translations/no.json deleted file mode 100644 index 92e6841ef94..00000000000 --- a/homeassistant/components/openalpr_local/translations/no.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "OpenALPR Local-integrasjonen venter p\u00e5 fjerning fra Home Assistant og vil ikke lenger v\u00e6re tilgjengelig fra og med Home Assistant 2022.10. \n\n Fjern YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", - "title": "OpenALPR Local-integrasjonen blir fjernet" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/pl.json b/homeassistant/components/openalpr_local/translations/pl.json deleted file mode 100644 index ac367d20809..00000000000 --- a/homeassistant/components/openalpr_local/translations/pl.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "Integracja OpenALPR Local oczekuje na usuni\u0119cie z Home Assistanta i nie b\u0119dzie ju\u017c dost\u0119pna od Home Assistant 2022.10. \n\nUsu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", - "title": "Integracja OpenALPR Local zostanie usuni\u0119ta" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/pt-BR.json b/homeassistant/components/openalpr_local/translations/pt-BR.json deleted file mode 100644 index 96b2c244b5c..00000000000 --- a/homeassistant/components/openalpr_local/translations/pt-BR.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "A integra\u00e7\u00e3o do OpenALPR Local est\u00e1 pendente de remo\u00e7\u00e3o do Home Assistant e n\u00e3o estar\u00e1 mais dispon\u00edvel a partir do Home Assistant 2022.10. \n\n Remova a configura\u00e7\u00e3o YAML do arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", - "title": "A integra\u00e7\u00e3o do OpenALPR Local est\u00e1 sendo removida" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/ru.json b/homeassistant/components/openalpr_local/translations/ru.json deleted file mode 100644 index 171aaa8a5c9..00000000000 --- a/homeassistant/components/openalpr_local/translations/ru.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f OpenALPR Local \u043e\u0436\u0438\u0434\u0430\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant \u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0441 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2022.10. \n\n\u0423\u0434\u0430\u043b\u0438\u0442\u0435 YAML-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", - "title": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f OpenALPR Local \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/sk.json b/homeassistant/components/openalpr_local/translations/sk.json deleted file mode 100644 index 3b25e7762be..00000000000 --- a/homeassistant/components/openalpr_local/translations/sk.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "Integr\u00e1cia OpenALPR Local \u010dak\u00e1 na odstr\u00e1nenie z Home Assistant a od Home Assistant 2022.10 u\u017e nebude k dispoz\u00edcii. \n\n Ak chcete tento probl\u00e9m vyrie\u0161i\u0165, odstr\u00e1\u0148te konfigur\u00e1ciu YAML zo s\u00faboru configuration.yaml a re\u0161tartujte aplik\u00e1ciu Home Assistant.", - "title": "Lok\u00e1lna integr\u00e1cia OpenALPR sa odstra\u0148uje" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/sv.json b/homeassistant/components/openalpr_local/translations/sv.json deleted file mode 100644 index 2c02b30458d..00000000000 --- a/homeassistant/components/openalpr_local/translations/sv.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "OpenALPR Local integration v\u00e4ntar p\u00e5 borttagning fr\u00e5n Home Assistant och kommer inte l\u00e4ngre att vara tillg\u00e4nglig fr\u00e5n och med Home Assistant 2022.10. \n\n Ta bort YAML-konfigurationen fr\u00e5n filen configuration.yaml och starta om Home Assistant f\u00f6r att \u00e5tg\u00e4rda problemet.", - "title": "OpenALPR Local integrationen tas bort" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/tr.json b/homeassistant/components/openalpr_local/translations/tr.json deleted file mode 100644 index 479e5385980..00000000000 --- a/homeassistant/components/openalpr_local/translations/tr.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "OpenALPR Yerel entegrasyonu Home Assistant'tan kald\u0131r\u0131lmay\u0131 beklemektedir ve Home Assistant 2022.10'dan itibaren art\u0131k kullan\u0131lamayacakt\u0131r.\n\nYAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", - "title": "OpenALPR Yerel entegrasyonu kald\u0131r\u0131l\u0131yor" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openalpr_local/translations/zh-Hant.json b/homeassistant/components/openalpr_local/translations/zh-Hant.json deleted file mode 100644 index 8ec55e5a004..00000000000 --- a/homeassistant/components/openalpr_local/translations/zh-Hant.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "issues": { - "pending_removal": { - "description": "OpenALPR \u672c\u5730\u7aef\u6574\u5408\u5373\u5c07\u7531 Home Assistant \u4e2d\u79fb\u9664\u3001\u4e26\u65bc Home Assistant 2022.10 \u7248\u5f8c\u7121\u6cd5\u518d\u4f7f\u7528\u3002\n\n\u7531 configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 YAML \u8a2d\u5b9a\u4e26\u91cd\u555f Home Assistant to \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", - "title": "OpenALPR \u672c\u5730\u7aef\u6574\u5408\u5373\u5c07\u79fb\u9664" - } - } -} \ No newline at end of file diff --git a/homeassistant/components/openerz/manifest.json b/homeassistant/components/openerz/manifest.json index 9a050154969..a70e65de2af 100644 --- a/homeassistant/components/openerz/manifest.json +++ b/homeassistant/components/openerz/manifest.json @@ -3,7 +3,7 @@ "name": "Open ERZ", "documentation": "https://www.home-assistant.io/integrations/openerz", "codeowners": ["@misialq"], - "requirements": ["openerz-api==0.1.0"], + "requirements": ["openerz-api==0.2.0"], "iot_class": "cloud_polling", "loggers": ["openerz_api"] } diff --git a/homeassistant/components/openexchangerates/translations/id.json b/homeassistant/components/openexchangerates/translations/id.json index e59732c319f..20ceb8930da 100644 --- a/homeassistant/components/openexchangerates/translations/id.json +++ b/homeassistant/components/openexchangerates/translations/id.json @@ -4,12 +4,12 @@ "already_configured": "Layanan sudah dikonfigurasi", "cannot_connect": "Gagal terhubung", "reauth_successful": "Autentikasi ulang berhasil", - "timeout_connect": "Tenggang waktu membuat koneksi habis" + "timeout_connect": "Tenggang waktu pembuatan koneksi habis" }, "error": { "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", - "timeout_connect": "Tenggang waktu membuat koneksi habis", + "timeout_connect": "Tenggang waktu pembuatan koneksi habis", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/openexchangerates/translations/nl.json b/homeassistant/components/openexchangerates/translations/nl.json index 885a94f4c43..4613279a0dc 100644 --- a/homeassistant/components/openexchangerates/translations/nl.json +++ b/homeassistant/components/openexchangerates/translations/nl.json @@ -15,7 +15,8 @@ "step": { "user": { "data": { - "api_key": "API-sleutel" + "api_key": "API-sleutel", + "base": "Basisvaluta" } } } diff --git a/homeassistant/components/opengarage/__init__.py b/homeassistant/components/opengarage/__init__.py index 6b97e88df0b..c269ee53cf3 100644 --- a/homeassistant/components/opengarage/__init__.py +++ b/homeassistant/components/opengarage/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging +from typing import Any import opengarage @@ -11,6 +12,7 @@ from homeassistant.const import CONF_HOST, CONF_PORT, CONF_VERIFY_SSL, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import update_coordinator from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import CONF_DEVICE_KEY, DOMAIN @@ -50,7 +52,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class OpenGarageDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): +class OpenGarageDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Class to manage fetching Opengarage data.""" def __init__( @@ -69,7 +71,7 @@ class OpenGarageDataUpdateCoordinator(update_coordinator.DataUpdateCoordinator): update_interval=timedelta(seconds=5), ) - async def _async_update_data(self) -> None: + async def _async_update_data(self) -> dict[str, Any]: """Fetch data.""" data = await self.open_garage_connection.update_state() if data is None: diff --git a/homeassistant/components/opengarage/binary_sensor.py b/homeassistant/components/opengarage/binary_sensor.py index 423843b4e11..64bc7c83d20 100644 --- a/homeassistant/components/opengarage/binary_sensor.py +++ b/homeassistant/components/opengarage/binary_sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import cast from homeassistant.components.binary_sensor import ( BinarySensorEntity, @@ -11,6 +12,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import OpenGarageDataUpdateCoordinator from .const import DOMAIN from .entity import OpenGarageEntity @@ -28,12 +30,14 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the OpenGarage binary sensors.""" - open_garage_data_coordinator = hass.data[DOMAIN][entry.entry_id] + open_garage_data_coordinator: OpenGarageDataUpdateCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] async_add_entities( [ OpenGarageBinarySensor( open_garage_data_coordinator, - entry.unique_id, + cast(str, entry.unique_id), description, ) for description in SENSOR_TYPES @@ -44,10 +48,15 @@ async def async_setup_entry( class OpenGarageBinarySensor(OpenGarageEntity, BinarySensorEntity): """Representation of a OpenGarage binary sensor.""" - def __init__(self, open_garage_data_coordinator, device_id, description): + def __init__( + self, + coordinator: OpenGarageDataUpdateCoordinator, + device_id: str, + description: BinarySensorEntityDescription, + ) -> None: """Initialize the entity.""" self._available = False - super().__init__(open_garage_data_coordinator, device_id, description) + super().__init__(coordinator, device_id, description) @property def available(self) -> bool: diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index aff913cf205..15669a41736 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, cast from homeassistant.components.cover import ( CoverDeviceClass, @@ -14,6 +14,7 @@ from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_O from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import OpenGarageDataUpdateCoordinator from .const import DOMAIN from .entity import OpenGarageEntity @@ -27,7 +28,7 @@ async def async_setup_entry( ) -> None: """Set up the OpenGarage covers.""" async_add_entities( - [OpenGarageCover(hass.data[DOMAIN][entry.entry_id], entry.unique_id)] + [OpenGarageCover(hass.data[DOMAIN][entry.entry_id], cast(str, entry.unique_id))] ) @@ -37,12 +38,14 @@ class OpenGarageCover(OpenGarageEntity, CoverEntity): _attr_device_class = CoverDeviceClass.GARAGE _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE - def __init__(self, open_garage_data_coordinator, device_id): + def __init__( + self, coordinator: OpenGarageDataUpdateCoordinator, device_id: str + ) -> None: """Initialize the cover.""" - self._state = None - self._state_before_move = None + self._state: str | None = None + self._state_before_move: str | None = None - super().__init__(open_garage_data_coordinator, device_id) + super().__init__(coordinator, device_id) @property def is_closed(self) -> bool | None: @@ -87,7 +90,7 @@ class OpenGarageCover(OpenGarageEntity, CoverEntity): status = self.coordinator.data self._attr_name = status["name"] - state = STATES_MAP.get(status.get("door")) + state = STATES_MAP.get(status.get("door")) # type: ignore[arg-type] if self._state_before_move is not None: if self._state_before_move != state: self._state = state diff --git a/homeassistant/components/opengarage/entity.py b/homeassistant/components/opengarage/entity.py index 97a60d42c07..dec0d1daae8 100644 --- a/homeassistant/components/opengarage/entity.py +++ b/homeassistant/components/opengarage/entity.py @@ -1,17 +1,23 @@ """Entity for the opengarage.io component.""" +from __future__ import annotations from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DOMAIN +from . import DOMAIN, OpenGarageDataUpdateCoordinator -class OpenGarageEntity(CoordinatorEntity): +class OpenGarageEntity(CoordinatorEntity[OpenGarageDataUpdateCoordinator]): """Representation of a OpenGarage entity.""" - def __init__(self, open_garage_data_coordinator, device_id, description=None): + def __init__( + self, + open_garage_data_coordinator: OpenGarageDataUpdateCoordinator, + device_id: str, + description: EntityDescription | None = None, + ) -> None: """Initialize the entity.""" super().__init__(open_garage_data_coordinator) @@ -35,9 +41,9 @@ class OpenGarageEntity(CoordinatorEntity): self.async_write_ha_state() @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" - device_info = DeviceInfo( + return DeviceInfo( configuration_url=self.coordinator.open_garage_connection.device_url, connections={(CONNECTION_NETWORK_MAC, self.coordinator.data["mac"])}, identifiers={(DOMAIN, self._device_id)}, @@ -46,4 +52,3 @@ class OpenGarageEntity(CoordinatorEntity): suggested_area="Garage", sw_version=self.coordinator.data["fwv"], ) - return device_info diff --git a/homeassistant/components/opengarage/sensor.py b/homeassistant/components/opengarage/sensor.py index 386955dfd47..ba8ecc0c322 100644 --- a/homeassistant/components/opengarage/sensor.py +++ b/homeassistant/components/opengarage/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import cast from homeassistant.components.sensor import ( SensorDeviceClass, @@ -20,6 +21,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback +from . import OpenGarageDataUpdateCoordinator from .const import DOMAIN from .entity import OpenGarageEntity @@ -59,12 +61,14 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the OpenGarage sensors.""" - open_garage_data_coordinator = hass.data[DOMAIN][entry.entry_id] + open_garage_data_coordinator: OpenGarageDataUpdateCoordinator = hass.data[DOMAIN][ + entry.entry_id + ] async_add_entities( [ OpenGarageSensor( open_garage_data_coordinator, - entry.unique_id, + cast(str, entry.unique_id), description, ) for description in SENSOR_TYPES diff --git a/homeassistant/components/opengarage/translations/lv.json b/homeassistant/components/opengarage/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/opengarage/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/translations/uk.json b/homeassistant/components/opengarage/translations/uk.json new file mode 100644 index 00000000000..488df8b4045 --- /dev/null +++ b/homeassistant/components/opengarage/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "device_key": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index fba397c1326..707be76be3b 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -5,12 +5,11 @@ import asyncio from collections.abc import Awaitable, Callable, Coroutine import functools import logging -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar import aiohttp from async_upnp_client.client import UpnpError from openhomedevice.device import Device -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.components import media_source diff --git a/homeassistant/components/opentherm_gw/translations/id.json b/homeassistant/components/opentherm_gw/translations/id.json index c41035d1800..4cfb4f1c80e 100644 --- a/homeassistant/components/opentherm_gw/translations/id.json +++ b/homeassistant/components/opentherm_gw/translations/id.json @@ -4,7 +4,7 @@ "already_configured": "Perangkat sudah dikonfigurasi", "cannot_connect": "Gagal terhubung", "id_exists": "ID gateway sudah ada", - "timeout_connect": "Tenggang waktu membuat koneksi habis" + "timeout_connect": "Tenggang waktu pembuatan koneksi habis" }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/translations/lv.json b/homeassistant/components/opentherm_gw/translations/lv.json index 916fe4661a6..1c001791da6 100644 --- a/homeassistant/components/opentherm_gw/translations/lv.json +++ b/homeassistant/components/opentherm_gw/translations/lv.json @@ -1,4 +1,9 @@ { + "config": { + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/ca.json b/homeassistant/components/openuv/translations/ca.json index b7a7f3e7630..5a84892555d 100644 --- a/homeassistant/components/openuv/translations/ca.json +++ b/homeassistant/components/openuv/translations/ca.json @@ -28,7 +28,7 @@ }, "issues": { "deprecated_service_multiple_alternate_targets": { - "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei perqu\u00e8 passin a utilitzar el servei `{alternate_service}` amb alguna de les seg\u00fcents entitats com a objectiu o 'target': `{alternate_targets}`.", + "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei perqu\u00e8 passin a utilitzar el servei `{alternate_service}` amb algun dels seg\u00fcents ID's d'entitat objectiu o 'target': `{alternate_targets}`.", "title": "El servei {deprecated_service} est\u00e0 sent eliminat" }, "deprecated_service_single_alternate_target": { diff --git a/homeassistant/components/openuv/translations/hu.json b/homeassistant/components/openuv/translations/hu.json index 6e9a7cc4c1f..20540868c07 100644 --- a/homeassistant/components/openuv/translations/hu.json +++ b/homeassistant/components/openuv/translations/hu.json @@ -12,7 +12,7 @@ "data": { "api_key": "API kulcs" }, - "description": "K\u00e9rj\u00fck, adja meg \u00fajra az API-kulcsot a k\u00f6vetkez\u0151h\u00f6z: {latitude}, {longitude}.", + "description": "K\u00e9rem, adja meg \u00fajra az API-kulcsot a k\u00f6vetkez\u0151h\u00f6z: {latitude}, {longitude}.", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "user": { diff --git a/homeassistant/components/openuv/translations/nl.json b/homeassistant/components/openuv/translations/nl.json index 8a1a5e0489d..9ccbef259f9 100644 --- a/homeassistant/components/openuv/translations/nl.json +++ b/homeassistant/components/openuv/translations/nl.json @@ -25,6 +25,14 @@ } } }, + "issues": { + "deprecated_service_multiple_alternate_targets": { + "title": "De {deprecated_service} service wordt verwijderd" + }, + "deprecated_service_single_alternate_target": { + "title": "De {deprecated_service} service wordt verwijderd" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/openuv/translations/sv.json b/homeassistant/components/openuv/translations/sv.json index 073d160dece..4c294595b11 100644 --- a/homeassistant/components/openuv/translations/sv.json +++ b/homeassistant/components/openuv/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Platsen \u00e4r redan konfigurerad" + "already_configured": "Platsen \u00e4r redan konfigurerad", + "reauth_successful": "Reautentisering lyckades" }, "error": { "invalid_api_key": "Ogiltigt API-l\u00f6senord" diff --git a/homeassistant/components/openuv/translations/tr.json b/homeassistant/components/openuv/translations/tr.json index cf70500f213..1683b43c056 100644 --- a/homeassistant/components/openuv/translations/tr.json +++ b/homeassistant/components/openuv/translations/tr.json @@ -1,12 +1,20 @@ { "config": { "abort": { - "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" }, "error": { "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" }, "step": { + "reauth_confirm": { + "data": { + "api_key": "API Anahtar\u0131" + }, + "description": "L\u00fctfen {latitude} , {longitude} i\u00e7in API anahtar\u0131n\u0131 yeniden girin.", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "api_key": "API Anahtar\u0131", diff --git a/homeassistant/components/openweathermap/translations/el.json b/homeassistant/components/openweathermap/translations/el.json index dada058b430..99f2264383e 100644 --- a/homeassistant/components/openweathermap/translations/el.json +++ b/homeassistant/components/openweathermap/translations/el.json @@ -15,9 +15,9 @@ "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 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2" + "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, - "description": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 OpenWeatherMap. \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://openweathermap.org/appid" + "description": "\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://openweathermap.org/appid" } } }, diff --git a/homeassistant/components/oralb/__init__.py b/homeassistant/components/oralb/__init__.py index 61547b5e432..c981ad01bd8 100644 --- a/homeassistant/components/oralb/__init__.py +++ b/homeassistant/components/oralb/__init__.py @@ -5,13 +5,17 @@ import logging from oralb_ble import OralBBluetoothDeviceData -from homeassistant.components.bluetooth import BluetoothScanningMode -from homeassistant.components.bluetooth.passive_update_processor import ( - PassiveBluetoothProcessorCoordinator, +from homeassistant.components.bluetooth import ( + BluetoothScanningMode, + BluetoothServiceInfoBleak, + async_ble_device_from_address, +) +from homeassistant.components.bluetooth.active_update_processor import ( + ActiveBluetoothProcessorCoordinator, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import CoreState, HomeAssistant from .const import DOMAIN @@ -25,14 +29,55 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address = entry.unique_id assert address is not None data = OralBBluetoothDeviceData() + + def _needs_poll( + service_info: BluetoothServiceInfoBleak, last_poll: float | None + ) -> bool: + # Only poll if hass is running, we need to poll, + # and we actually have a way to connect to the device + return ( + hass.state == CoreState.running + and data.poll_needed(service_info, last_poll) + and bool( + async_ble_device_from_address( + hass, service_info.device.address, connectable=True + ) + ) + ) + + async def _async_poll(service_info: BluetoothServiceInfoBleak): + # BluetoothServiceInfoBleak is defined in HA, otherwise would just pass it + # directly to the oralb code + # Make sure the device we have is one that we can connect with + # in case its coming from a passive scanner + if service_info.connectable: + connectable_device = service_info.device + elif device := async_ble_device_from_address( + hass, service_info.device.address, True + ): + connectable_device = device + else: + # We have no bluetooth controller that is in range of + # the device to poll it + raise RuntimeError( + f"No connectable device found for {service_info.device.address}" + ) + return await data.async_poll(connectable_device) + coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id - ] = PassiveBluetoothProcessorCoordinator( + ] = ActiveBluetoothProcessorCoordinator( hass, _LOGGER, address=address, mode=BluetoothScanningMode.PASSIVE, update_method=data.update, + needs_poll_method=_needs_poll, + poll_method=_async_poll, + # We will take advertisements from non-connectable devices + # since we will trade the BLEDevice for a connectable one + # if we need to poll it + connectable=False, ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload( diff --git a/homeassistant/components/oralb/manifest.json b/homeassistant/components/oralb/manifest.json index 94abb85a7b8..602c674b329 100644 --- a/homeassistant/components/oralb/manifest.json +++ b/homeassistant/components/oralb/manifest.json @@ -8,8 +8,8 @@ "manufacturer_id": 220 } ], - "requirements": ["oralb-ble==0.14.3"], - "dependencies": ["bluetooth"], - "codeowners": ["@bdraco"], + "requirements": ["oralb-ble==0.17.1"], + "dependencies": ["bluetooth_adapters"], + "codeowners": ["@bdraco", "@Lash-L"], "iot_class": "local_push" } diff --git a/homeassistant/components/oralb/sensor.py b/homeassistant/components/oralb/sensor.py index 124138d7e36..7a198c21f80 100644 --- a/homeassistant/components/oralb/sensor.py +++ b/homeassistant/components/oralb/sensor.py @@ -1,8 +1,6 @@ """Support for OralB sensors.""" from __future__ import annotations -from typing import Optional, Union - from oralb_ble import OralBSensor, SensorUpdate from homeassistant import config_entries @@ -18,7 +16,11 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.const import SIGNAL_STRENGTH_DECIBELS_MILLIWATT, UnitOfTime +from homeassistant.const import ( + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + UnitOfTime, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -59,6 +61,12 @@ SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = { entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, ), + OralBSensor.BATTERY_PERCENT: SensorEntityDescription( + key=OralBSensor.BATTERY_PERCENT, + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ), } @@ -107,9 +115,7 @@ async def async_setup_entry( class OralBBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[str, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[str | int | None]], SensorEntity, ): """Representation of a OralB sensor.""" diff --git a/homeassistant/components/oralb/translations/lv.json b/homeassistant/components/oralb/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/oralb/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/oralb/translations/uk.json b/homeassistant/components/oralb/translations/uk.json new file mode 100644 index 00000000000..e58b49d4c9e --- /dev/null +++ b/homeassistant/components/oralb/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/__init__.py b/homeassistant/components/otbr/__init__.py new file mode 100644 index 00000000000..046643480ca --- /dev/null +++ b/homeassistant/components/otbr/__init__.py @@ -0,0 +1,81 @@ +"""The Open Thread Border Router integration.""" +from __future__ import annotations + +from collections.abc import Callable, Coroutine +import dataclasses +from functools import wraps +from typing import Any, Concatenate, ParamSpec, TypeVar + +import python_otbr_api + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType + +from . import websocket_api +from .const import DOMAIN + +_R = TypeVar("_R") +_P = ParamSpec("_P") + + +def _handle_otbr_error( + func: Callable[Concatenate[OTBRData, _P], Coroutine[Any, Any, _R]] +) -> Callable[Concatenate[OTBRData, _P], Coroutine[Any, Any, _R]]: + """Handle OTBR errors.""" + + @wraps(func) + async def _func(self: OTBRData, *args: _P.args, **kwargs: _P.kwargs) -> _R: + try: + return await func(self, *args, **kwargs) + except python_otbr_api.OTBRError as exc: + raise HomeAssistantError("Failed to call OTBR API") from exc + + return _func + + +@dataclasses.dataclass +class OTBRData: + """Container for OTBR data.""" + + url: str + api: python_otbr_api.OTBR + + @_handle_otbr_error + async def get_active_dataset_tlvs(self) -> bytes | None: + """Get current active operational dataset in TLVS format, or None.""" + return await self.api.get_active_dataset_tlvs() + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Open Thread Border Router component.""" + websocket_api.async_setup(hass) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up an Open Thread Border Router config entry.""" + api = python_otbr_api.OTBR(entry.data["url"], async_get_clientsession(hass), 10) + hass.data[DOMAIN] = OTBRData(entry.data["url"], api) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + hass.data.pop(DOMAIN) + return True + + +async def async_get_active_dataset_tlvs(hass: HomeAssistant) -> bytes | None: + """Get current active operational dataset in TLVS format, or None. + + Returns None if there is no active operational dataset. + Raises if the http status is 400 or higher or if the response is invalid. + """ + if DOMAIN not in hass.data: + raise HomeAssistantError("OTBR API not available") + + data: OTBRData = hass.data[DOMAIN] + return await data.get_active_dataset_tlvs() diff --git a/homeassistant/components/otbr/config_flow.py b/homeassistant/components/otbr/config_flow.py new file mode 100644 index 00000000000..306a7f7e700 --- /dev/null +++ b/homeassistant/components/otbr/config_flow.py @@ -0,0 +1,60 @@ +"""Config flow for the Open Thread Border Router integration.""" +from __future__ import annotations + +import python_otbr_api +import voluptuous as vol + +from homeassistant.components.hassio import HassioServiceInfo +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_URL +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + + +class OTBRConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Open Thread Border Router.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Set up by user.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + errors = {} + + if user_input is not None: + url = user_input[CONF_URL] + api = python_otbr_api.OTBR(url, async_get_clientsession(self.hass), 10) + try: + await api.get_active_dataset_tlvs() + except python_otbr_api.OTBRError: + errors["base"] = "cannot_connect" + else: + await self.async_set_unique_id(DOMAIN) + return self.async_create_entry( + title="Open Thread Border Router", + data=user_input, + ) + + data_schema = vol.Schema({CONF_URL: str}) + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors + ) + + async def async_step_hassio(self, discovery_info: HassioServiceInfo) -> FlowResult: + """Handle hassio discovery.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + config = discovery_info.config + url = f"http://{config['host']}:{config['port']}" + await self.async_set_unique_id(DOMAIN) + return self.async_create_entry( + title="Open Thread Border Router", + data={"url": url}, + ) diff --git a/homeassistant/components/otbr/const.py b/homeassistant/components/otbr/const.py new file mode 100644 index 00000000000..72884a198d8 --- /dev/null +++ b/homeassistant/components/otbr/const.py @@ -0,0 +1,3 @@ +"""Constants for the Open Thread Border Router integration.""" + +DOMAIN = "otbr" diff --git a/homeassistant/components/otbr/manifest.json b/homeassistant/components/otbr/manifest.json new file mode 100644 index 00000000000..796dcd00141 --- /dev/null +++ b/homeassistant/components/otbr/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "otbr", + "name": "Open Thread Border Router", + "config_flow": true, + "dependencies": ["thread"], + "documentation": "https://www.home-assistant.io/integrations/otbr", + "requirements": ["python-otbr-api==1.0.2"], + "after_dependencies": ["hassio"], + "codeowners": ["@home-assistant/core"], + "iot_class": "local_polling", + "integration_type": "service" +} diff --git a/homeassistant/components/otbr/strings.json b/homeassistant/components/otbr/strings.json new file mode 100644 index 00000000000..58b32276ba8 --- /dev/null +++ b/homeassistant/components/otbr/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "[%key:common::config_flow::data::url%]" + }, + "description": "Provide URL for the Open Thread Border Router's REST API" + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + } + } +} diff --git a/homeassistant/components/otbr/translations/bg.json b/homeassistant/components/otbr/translations/bg.json new file mode 100644 index 00000000000..8a9e2db6afc --- /dev/null +++ b/homeassistant/components/otbr/translations/bg.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/ca.json b/homeassistant/components/otbr/translations/ca.json new file mode 100644 index 00000000000..868ba2917cf --- /dev/null +++ b/homeassistant/components/otbr/translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Proporciona l'URL de l'API REST de l'encaminador frontera Open Thread" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/de.json b/homeassistant/components/otbr/translations/de.json new file mode 100644 index 00000000000..b4b2f590e15 --- /dev/null +++ b/homeassistant/components/otbr/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Gib die URL f\u00fcr die REST-API des Open Thread Border Routers an" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/el.json b/homeassistant/components/otbr/translations/el.json new file mode 100644 index 00000000000..546f4312ad6 --- /dev/null +++ b/homeassistant/components/otbr/translations/el.json @@ -0,0 +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" + }, + "step": { + "user": { + "data": { + "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" + }, + "description": "\u039a\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03af\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03b3\u03b9\u03b1 \u03c4\u03bf REST API \u03c4\u03bf\u03c5 \u03b4\u03c1\u03bf\u03bc\u03bf\u03bb\u03bf\u03b3\u03b7\u03c4\u03ae \u03c0\u03b5\u03c1\u03b9\u03b3\u03c1\u03ac\u03bc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03b1\u03bd\u03bf\u03b9\u03c7\u03c4\u03bf\u03cd \u03bd\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/en.json b/homeassistant/components/otbr/translations/en.json new file mode 100644 index 00000000000..36101b77bea --- /dev/null +++ b/homeassistant/components/otbr/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Service is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Provide URL for the Open Thread Border Router's REST API" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/es-419.json b/homeassistant/components/otbr/translations/es-419.json new file mode 100644 index 00000000000..afad5267a36 --- /dev/null +++ b/homeassistant/components/otbr/translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/es.json b/homeassistant/components/otbr/translations/es.json new file mode 100644 index 00000000000..b1e9824ef2a --- /dev/null +++ b/homeassistant/components/otbr/translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "El servicio ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Proporciona la URL para la API REST del Open Thread Border Router" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/et.json b/homeassistant/components/otbr/translations/et.json new file mode 100644 index 00000000000..759b92c55a3 --- /dev/null +++ b/homeassistant/components/otbr/translations/et.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Sisesta Open Thread Border Routeri REST API URL" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/hu.json b/homeassistant/components/otbr/translations/hu.json new file mode 100644 index 00000000000..ac8e626ee26 --- /dev/null +++ b/homeassistant/components/otbr/translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Adja meg az Open Thread Border Router REST API-j\u00e1nak URL-c\u00edm\u00e9t" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/id.json b/homeassistant/components/otbr/translations/id.json new file mode 100644 index 00000000000..1cb1e550af4 --- /dev/null +++ b/homeassistant/components/otbr/translations/id.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Layanan sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Tentukan URL untuk API REST Open Thread Border Router" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/it.json b/homeassistant/components/otbr/translations/it.json new file mode 100644 index 00000000000..a4ec6b1ec25 --- /dev/null +++ b/homeassistant/components/otbr/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Fornisci l'URL per l'API REST di Open Thread Border Router" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/ja.json b/homeassistant/components/otbr/translations/ja.json new file mode 100644 index 00000000000..6c003d1381d --- /dev/null +++ b/homeassistant/components/otbr/translations/ja.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u30b5\u30fc\u30d3\u30b9\u306f\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059" + }, + "error": { + "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/lv.json b/homeassistant/components/otbr/translations/lv.json new file mode 100644 index 00000000000..3bca700de3b --- /dev/null +++ b/homeassistant/components/otbr/translations/lv.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u0160is pakalpojums jau piesl\u0113gts Home Assistant" + }, + "error": { + "cannot_connect": "Piesl\u0113guma k\u013c\u016bda" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/nl.json b/homeassistant/components/otbr/translations/nl.json new file mode 100644 index 00000000000..f4c9efba15c --- /dev/null +++ b/homeassistant/components/otbr/translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Dienst is al geconfigureerd" + }, + "error": { + "cannot_connect": "Verbinden mislukt" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Geef URL voor de Open Thread Border Router REST API" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/no.json b/homeassistant/components/otbr/translations/no.json new file mode 100644 index 00000000000..141f225e140 --- /dev/null +++ b/homeassistant/components/otbr/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Oppgi URL for Open Thread Border Routers REST API" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/pl.json b/homeassistant/components/otbr/translations/pl.json new file mode 100644 index 00000000000..5fe84f6ca57 --- /dev/null +++ b/homeassistant/components/otbr/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Podaj adres URL interfejsu API REST dla Open Thread Border Router" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/pt-BR.json b/homeassistant/components/otbr/translations/pt-BR.json new file mode 100644 index 00000000000..b4210e0056a --- /dev/null +++ b/homeassistant/components/otbr/translations/pt-BR.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Forne\u00e7a URL para a API REST do Open Thread Border Router" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/ru.json b/homeassistant/components/otbr/translations/ru.json new file mode 100644 index 00000000000..ddef2203761 --- /dev/null +++ b/homeassistant/components/otbr/translations/ru.json @@ -0,0 +1,18 @@ +{ + "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." + }, + "step": { + "user": { + "data": { + "url": "URL-\u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 URL-\u0430\u0434\u0440\u0435\u0441 REST API Open Thread Border Router" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/sk.json b/homeassistant/components/otbr/translations/sk.json new file mode 100644 index 00000000000..4aec1981fc5 --- /dev/null +++ b/homeassistant/components/otbr/translations/sk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "user": { + "data": { + "url": "URL" + }, + "description": "Zadajte adresu URL pre rozhranie REST API smerova\u010da s otvoren\u00fdm vl\u00e1knom" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/sv.json b/homeassistant/components/otbr/translations/sv.json new file mode 100644 index 00000000000..5028dbe8803 --- /dev/null +++ b/homeassistant/components/otbr/translations/sv.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Tj\u00e4nsten \u00e4r redan konfigurerad" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/uk.json b/homeassistant/components/otbr/translations/uk.json new file mode 100644 index 00000000000..9a1c4710126 --- /dev/null +++ b/homeassistant/components/otbr/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u043e\u0441\u043b\u0443\u0433\u0430 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/translations/zh-Hant.json b/homeassistant/components/otbr/translations/zh-Hant.json new file mode 100644 index 00000000000..fab95df572c --- /dev/null +++ b/homeassistant/components/otbr/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "url": "\u7db2\u5740" + }, + "description": "\u70ba Open Thread Border Router \u7684 REST API \u63d0\u4f9b URL" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/otbr/websocket_api.py b/homeassistant/components/otbr/websocket_api.py new file mode 100644 index 00000000000..a07819793b1 --- /dev/null +++ b/homeassistant/components/otbr/websocket_api.py @@ -0,0 +1,53 @@ +"""Websocket API for OTBR.""" +from typing import TYPE_CHECKING + +from homeassistant.components.websocket_api import ( + ActiveConnection, + async_register_command, + async_response, + websocket_command, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN + +if TYPE_CHECKING: + from . import OTBRData + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Set up the OTBR Websocket API.""" + async_register_command(hass, websocket_info) + + +@websocket_command( + { + "type": "otbr/info", + } +) +@async_response +async def websocket_info( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: + """Get OTBR info.""" + if DOMAIN not in hass.data: + connection.send_error(msg["id"], "not_loaded", "No OTBR API loaded") + return + + data: OTBRData = hass.data[DOMAIN] + + try: + dataset = await data.get_active_dataset_tlvs() + except HomeAssistantError as exc: + connection.send_error(msg["id"], "get_dataset_failed", str(exc)) + return + + connection.send_result( + msg["id"], + { + "url": data.url, + "active_dataset_tlvs": dataset.hex() if dataset else None, + }, + ) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index e6a0e04deb7..7e7c8ad2625 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -386,7 +386,7 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ key=OverkizState.CORE_THREE_WAY_HANDLE_DIRECTION, name="Three way handle direction", device_class=SensorDeviceClass.ENUM, - options=["open", "tilt", "close"], + options=["open", "tilt", "closed"], translation_key="three_way_handle_direction", ), ] diff --git a/homeassistant/components/overkiz/translations/fr.json b/homeassistant/components/overkiz/translations/fr.json index 82997fbd1ae..2d2c39f171a 100644 --- a/homeassistant/components/overkiz/translations/fr.json +++ b/homeassistant/components/overkiz/translations/fr.json @@ -26,5 +26,15 @@ "description": "La plateforme Overkiz est utilis\u00e9e par diff\u00e9rents \u00e9diteurs comme Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) et Atlantic (Cozytouch). Saisissez les informations d'identification de votre application et s\u00e9lectionnez votre hub." } } + }, + "entity": { + "sensor": { + "three_way_handle_direction": { + "state": { + "closed": "Ferm\u00e9", + "open": "Ouvert" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/nl.json b/homeassistant/components/overkiz/translations/nl.json index f2cce55db16..c165c6232e1 100644 --- a/homeassistant/components/overkiz/translations/nl.json +++ b/homeassistant/components/overkiz/translations/nl.json @@ -28,6 +28,12 @@ }, "entity": { "select": { + "memorized_simple_volume": { + "state": { + "highest": "Hoogste", + "standard": "Standaard" + } + }, "open_closed_pedestrian": { "state": { "closed": "Gesloten", @@ -45,13 +51,28 @@ }, "discrete_rssi_level": { "state": { - "good": "Goed" + "good": "Goed", + "low": "Laag", + "verylow": "Heel laag" } }, "priority_lock_originator": { "state": { + "external_gateway": "Externe gateway", + "local_user": "Lokale gebruiker", + "rain": "Regen", + "security": "Beveiliging", "temperature": "Temperatuur", - "user": "Gebruiker" + "timer": "Timer", + "user": "Gebruiker", + "wind": "Wind" + } + }, + "sensor_defect": { + "state": { + "low_battery": "Batterij bijna leeg", + "maintenance_required": "Onderhoud vereist", + "no_defect": "Geen defect" } }, "sensor_room": { diff --git a/homeassistant/components/overkiz/translations/tr.json b/homeassistant/components/overkiz/translations/tr.json index f3cb5b70d45..8216c758636 100644 --- a/homeassistant/components/overkiz/translations/tr.json +++ b/homeassistant/components/overkiz/translations/tr.json @@ -26,5 +26,78 @@ "description": "Overkiz platformu, Somfy (Connexoon / TaHoma), Hitachi (Hi Kumo), Rexel (Energeasy Connect) ve Atlantic (Cozytouch) gibi \u00e7e\u015fitli sat\u0131c\u0131lar taraf\u0131ndan kullan\u0131lmaktad\u0131r. Uygulama kimlik bilgilerinizi girin ve hub'\u0131n\u0131z\u0131 se\u00e7in." } } + }, + "entity": { + "select": { + "memorized_simple_volume": { + "state": { + "highest": "En y\u00fcksek", + "standard": "Standart" + } + }, + "open_closed_pedestrian": { + "state": { + "closed": "Kapal\u0131", + "open": "A\u00e7\u0131k", + "pedestrian": "Yaya" + } + } + }, + "sensor": { + "battery": { + "state": { + "full": "Tam", + "low": "D\u00fc\u015f\u00fck", + "normal": "Normal", + "verylow": "\u00c7ok d\u00fc\u015f\u00fck" + } + }, + "discrete_rssi_level": { + "state": { + "good": "\u0130yi", + "low": "D\u00fc\u015f\u00fck", + "normal": "Normal", + "verylow": "\u00c7ok d\u00fc\u015f\u00fck" + } + }, + "priority_lock_originator": { + "state": { + "external_gateway": "Harici a\u011f ge\u00e7idi", + "local_user": "Yerel kullan\u0131c\u0131", + "lsc": "LSC", + "myself": "Kendim", + "rain": "Ya\u011fmur", + "saac": "SAAC", + "security": "G\u00fcvenlik", + "sfc": "SFC", + "temperature": "S\u0131cakl\u0131k", + "timer": "Zamanlay\u0131c\u0131", + "ups": "G\u00fc\u00e7 Kayna\u011f\u0131", + "user": "Kullan\u0131c\u0131", + "wind": "R\u00fczg\u00e2r" + } + }, + "sensor_defect": { + "state": { + "dead": "\u00d6l\u00fc", + "low_battery": "D\u00fc\u015f\u00fck pil", + "maintenance_required": "Bak\u0131m gerekli", + "no_defect": "Hasar yok" + } + }, + "sensor_room": { + "state": { + "clean": "Temiz", + "dirty": "Kirli" + } + }, + "three_way_handle_direction": { + "state": { + "closed": "Kapal\u0131", + "open": "A\u00e7\u0131k", + "tilt": "E\u011fim" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/uk.json b/homeassistant/components/overkiz/translations/uk.json new file mode 100644 index 00000000000..2aed6be91ba --- /dev/null +++ b/homeassistant/components/overkiz/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/config_flow.py b/homeassistant/components/ovo_energy/config_flow.py index 9e406ce6b96..227182e5f99 100644 --- a/homeassistant/components/ovo_energy/config_flow.py +++ b/homeassistant/components/ovo_energy/config_flow.py @@ -1,10 +1,16 @@ """Config flow to configure the OVO Energy integration.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + import aiohttp from ovoenergy.ovoenergy import OVOEnergy import voluptuous as vol from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult from .const import CONF_ACCOUNT, DOMAIN @@ -28,7 +34,10 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): self.username = None self.account = None - async def async_step_user(self, user_input=None): + async def async_step_user( + self, + user_input: Mapping[str, Any] | None = None, + ) -> FlowResult: """Handle a flow initiated by the user.""" errors = {} if user_input is not None: @@ -61,7 +70,10 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=USER_SCHEMA, errors=errors ) - async def async_step_reauth(self, user_input): + async def async_step_reauth( + self, + user_input: Mapping[str, Any], + ) -> FlowResult: """Handle configuration by re-auth.""" errors = {} @@ -84,15 +96,15 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): else: if authenticated: entry = await self.async_set_unique_id(self.username) - self.hass.config_entries.async_update_entry( - entry, - data={ - CONF_USERNAME: self.username, - CONF_PASSWORD: user_input[CONF_PASSWORD], - CONF_ACCOUNT: self.account, - }, - ) - return self.async_abort(reason="reauth_successful") + if entry: + self.hass.config_entries.async_update_entry( + entry, + data={ + CONF_USERNAME: self.username, + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + return self.async_abort(reason="reauth_successful") errors["base"] = "authorization_error" diff --git a/homeassistant/components/ovo_energy/translations/he.json b/homeassistant/components/ovo_energy/translations/he.json index 270d8744b96..1d33347e77c 100644 --- a/homeassistant/components/ovo_energy/translations/he.json +++ b/homeassistant/components/ovo_energy/translations/he.json @@ -14,6 +14,7 @@ }, "user": { "data": { + "account": "\u05de\u05d6\u05d4\u05d4 \u05d7\u05e9\u05d1\u05d5\u05df OVO (\u05d4\u05d5\u05e1\u05e4\u05d4 \u05e8\u05e7 \u05d0\u05dd \u05d9\u05e9 \u05dc\u05da \u05de\u05e1\u05e4\u05e8 \u05d7\u05e9\u05d1\u05d5\u05e0\u05d5\u05ea)", "password": "\u05e1\u05d9\u05e1\u05de\u05d4", "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" }, diff --git a/homeassistant/components/ovo_energy/translations/lt.json b/homeassistant/components/ovo_energy/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/tr.json b/homeassistant/components/ovo_energy/translations/tr.json index 6ff9f6a25ce..4acd88e6f8d 100644 --- a/homeassistant/components/ovo_energy/translations/tr.json +++ b/homeassistant/components/ovo_energy/translations/tr.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "account": "OVO hesap kimli\u011fi (yaln\u0131zca birden fazla hesab\u0131n\u0131z varsa ekleyin)", "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, diff --git a/homeassistant/components/p1_monitor/translations/nl.json b/homeassistant/components/p1_monitor/translations/nl.json index 614a0079589..640d6fbe8ff 100644 --- a/homeassistant/components/p1_monitor/translations/nl.json +++ b/homeassistant/components/p1_monitor/translations/nl.json @@ -8,6 +8,9 @@ "data": { "host": "Host" }, + "data_description": { + "host": "Het IP-adres of de hostnaam van uw P1 Monitor installatie." + }, "description": "Stel P1 Monitor in om te integreren met Home Assistant." } } diff --git a/homeassistant/components/panasonic_viera/translations/hu.json b/homeassistant/components/panasonic_viera/translations/hu.json index 7fd4d2524da..94f916e5384 100644 --- a/homeassistant/components/panasonic_viera/translations/hu.json +++ b/homeassistant/components/panasonic_viera/translations/hu.json @@ -23,7 +23,7 @@ "name": "Elnevez\u00e9s" }, "description": "Adja meg a Panasonic Viera TV-hez tartoz\u00f3 IP c\u00edmet", - "title": "A TV be\u00e1ll\u00edt\u00e1sa" + "title": "TV be\u00e1ll\u00edt\u00e1sa" } } } diff --git a/homeassistant/components/panasonic_viera/translations/lv.json b/homeassistant/components/panasonic_viera/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index cd613306130..51da8b0f9dc 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -2,7 +2,7 @@ "domain": "philips_js", "name": "Philips TV", "documentation": "https://www.home-assistant.io/integrations/philips_js", - "requirements": ["ha-philipsjs==2.9.0"], + "requirements": ["ha-philipsjs==3.0.0"], "codeowners": ["@elupus"], "config_flow": true, "iot_class": "local_polling", diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 684dafbc750..f88f02128e2 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -17,6 +17,7 @@ from homeassistant.components.media_player import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.trigger import PluggableAction @@ -77,8 +78,6 @@ class PhilipsTVMediaPlayer( """Initialize the Philips TV.""" self._tv = coordinator.api self._sources: dict[str, str] = {} - self._supports = SUPPORT_PHILIPS_JS - self._system = coordinator.system self._attr_unique_id = coordinator.unique_id self._attr_device_info = DeviceInfo( identifiers={ @@ -89,11 +88,7 @@ class PhilipsTVMediaPlayer( sw_version=coordinator.system.get("softwareversion"), name=coordinator.system["name"], ) - self._state = MediaPlayerState.OFF - self._media_content_type: str | None = None - self._media_content_id: str | None = None - self._media_title: str | None = None - self._media_channel: str | None = None + self._attr_state = MediaPlayerState.OFF self._turn_on = PluggableAction(self.async_write_ha_state) super().__init__(coordinator) @@ -118,58 +113,31 @@ class PhilipsTVMediaPlayer( @property def supported_features(self) -> MediaPlayerEntityFeature: """Flag media player features that are supported.""" - supports = self._supports + supports = SUPPORT_PHILIPS_JS if self._turn_on or (self._tv.on and self._tv.powerstate is not None): supports |= MediaPlayerEntityFeature.TURN_ON return supports - @property - def state(self) -> MediaPlayerState: - """Get the device state. An exception means OFF state.""" - if self._tv.on and (self._tv.powerstate == "On" or self._tv.powerstate is None): - return MediaPlayerState.ON - return MediaPlayerState.OFF - - @property - def source(self): - """Return the current input source.""" - return self._sources.get(self._tv.source_id) - - @property - def source_list(self): - """List of available input sources.""" - return list(self._sources.values()) - async def async_select_source(self, source: str) -> None: """Set the input source.""" if source_id := _inverted(self._sources).get(source): await self._tv.setSource(source_id) await self._async_update_soon() - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._tv.volume - - @property - def is_volume_muted(self): - """Boolean if volume is currently muted.""" - return self._tv.muted - async def async_turn_on(self) -> None: """Turn on the device.""" if self._tv.on and self._tv.powerstate: await self._tv.setPowerState("On") - self._state = MediaPlayerState.ON + self._attr_state = MediaPlayerState.ON else: await self._turn_on.async_run(self.hass, self._context) await self._async_update_soon() async def async_turn_off(self) -> None: """Turn off the device.""" - if self._state == MediaPlayerState.ON: + if self._attr_state == MediaPlayerState.ON: await self._tv.sendKey("Standby") - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF await self._async_update_soon() else: _LOGGER.debug("Ignoring turn off when already in expected state") @@ -199,12 +167,18 @@ class PhilipsTVMediaPlayer( async def async_media_previous_track(self) -> None: """Send rewind command.""" - await self._tv.sendKey("Previous") + if self._tv.channel_active: + await self._tv.sendKey("ChannelStepDown") + else: + await self._tv.sendKey("Previous") await self._async_update_soon() async def async_media_next_track(self) -> None: """Send fast forward command.""" - await self._tv.sendKey("Next") + if self._tv.channel_active: + await self._tv.sendKey("ChannelStepUp") + else: + await self._tv.sendKey("Next") await self._async_update_soon() async def async_media_play_pause(self) -> None: @@ -231,83 +205,65 @@ class PhilipsTVMediaPlayer( await self._async_update_soon() @property - def media_channel(self): - """Get current channel if it's a channel.""" - return self._media_channel - - @property - def media_title(self): - """Title of current playing media.""" - return self._media_title - - @property - def media_content_type(self): - """Return content type of playing media.""" - return self._media_content_type - - @property - def media_content_id(self): - """Content type of current playing media.""" - return self._media_content_id - - @property - def media_image_url(self): + def media_image_url(self) -> str | None: """Image url of current playing media.""" - if self._media_content_id and self._media_content_type in ( + if self._attr_media_content_id and self._attr_media_content_type in ( MediaType.APP, MediaType.CHANNEL, ): return self.get_browse_image_url( - self._media_content_type, self._media_content_id, media_image_id=None + self._attr_media_content_type, + self._attr_media_content_id, + media_image_id=None, ) return None - @property - def app_id(self): - """ID of the current running app.""" - return self._tv.application_id + async def async_play_media_channel(self, media_id: str): + """Play a channel.""" + list_id, _, channel_id = media_id.partition("/") + if channel_id: + await self._tv.setChannel(channel_id, list_id) + await self._async_update_soon() + return - @property - def app_name(self): - """Name of the current running app.""" - if app := self._tv.applications.get(self._tv.application_id): - return app.get("label") + for channel in self._tv.channels_current: + if channel.get("preset") == media_id: + await self._tv.setChannel(channel["ccid"], self._tv.channel_list_id) + await self._async_update_soon() + return + + raise HomeAssistantError(f"Unable to find channel {media_id}") async def async_play_media( - self, media_type: str, media_id: str, **kwargs: Any + self, media_type: MediaType | str, media_id: str, **kwargs: Any ) -> None: """Play a piece of media.""" _LOGGER.debug("Call play media type <%s>, Id <%s>", media_type, media_id) if media_type == MediaType.CHANNEL: - list_id, _, channel_id = media_id.partition("/") - if channel_id: - await self._tv.setChannel(channel_id, list_id) - await self._async_update_soon() - else: - _LOGGER.error("Unable to find channel <%s>", media_id) + await self.async_play_media_channel(media_id) elif media_type == MediaType.APP: if app := self._tv.applications.get(media_id): await self._tv.setApplication(app["intent"]) await self._async_update_soon() else: - _LOGGER.error("Unable to find application <%s>", media_id) + raise HomeAssistantError(f"Unable to find application {media_id}") else: - _LOGGER.error("Unsupported media type <%s>", media_type) + raise HomeAssistantError(f"Unsupported media type {media_type}") - async def async_browse_media_channels(self, expanded): + async def async_browse_media_channels(self, expanded: bool) -> BrowseMedia: """Return channel media objects.""" if expanded: children = [ BrowseMedia( - title=channel.get("name", f"Channel: {channel_id}"), + title=channel.get("name", f"Channel: {channel['ccid']}"), media_class=MediaClass.CHANNEL, - media_content_id=f"alltv/{channel_id}", + media_content_id=f"{self._tv.channel_list_id}/{channel['ccid']}", media_content_type=MediaType.CHANNEL, can_play=True, can_expand=False, ) - for channel_id, channel in self._tv.channels.items() + for channel in self._tv.channels_current ] else: children = None @@ -323,10 +279,12 @@ class PhilipsTVMediaPlayer( children=children, ) - async def async_browse_media_favorites(self, list_id, expanded): + async def async_browse_media_favorites( + self, list_id: str, expanded: bool + ) -> BrowseMedia: """Return channel media objects.""" if expanded: - favorites = await self._tv.getFavoriteList(list_id) + favorites = self._tv.favorite_lists.get(list_id) if favorites: def get_name(channel): @@ -344,7 +302,7 @@ class PhilipsTVMediaPlayer( can_play=True, can_expand=False, ) - for channel in favorites + for channel in favorites.get("channels", []) ] else: children = None @@ -432,12 +390,14 @@ class PhilipsTVMediaPlayer( ], ) - 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._tv.on: raise BrowseError("Can't browse when tv is turned off") - if media_content_id in (None, ""): + if media_content_id is None or media_content_id == "": return await self.async_browse_media_root() path = media_content_id.partition("/") if path[0] == "channels": @@ -469,6 +429,8 @@ class PhilipsTVMediaPlayer( async def async_get_media_image(self) -> tuple[bytes | None, str | None]: """Serve album art. Returns (content, content_type).""" + if self.media_content_type is None or self.media_content_id is None: + return None, None return await self.async_get_browse_image( self.media_content_type, self.media_content_id, None ) @@ -478,36 +440,48 @@ class PhilipsTVMediaPlayer( if self._tv.on: if self._tv.powerstate in ("Standby", "StandbyKeep"): - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF else: - self._state = MediaPlayerState.ON + self._attr_state = MediaPlayerState.ON else: - self._state = MediaPlayerState.OFF + self._attr_state = MediaPlayerState.OFF self._sources = { srcid: source.get("name") or f"Source {srcid}" for srcid, source in (self._tv.sources or {}).items() } + self._attr_source = self._sources.get(self._tv.source_id) + self._attr_source_list = list(self._sources.values()) + + self._attr_app_id = self._tv.application_id + if app := self._tv.applications.get(self._tv.application_id): + self._attr_app_name = app.get("label") + else: + self._attr_app_name = None + + self._attr_volume_level = self._tv.volume + self._attr_is_volume_muted = self._tv.muted + if self._tv.channel_active: - self._media_content_type = MediaType.CHANNEL - self._media_content_id = f"all/{self._tv.channel_id}" - self._media_title = self._tv.channels.get(self._tv.channel_id, {}).get( + self._attr_media_content_type = MediaType.CHANNEL + self._attr_media_content_id = f"all/{self._tv.channel_id}" + self._attr_media_title = self._tv.channels.get(self._tv.channel_id, {}).get( "name" ) - self._media_channel = self._media_title + self._attr_media_channel = self._attr_media_title elif self._tv.application_id: - self._media_content_type = MediaType.APP - self._media_content_id = self._tv.application_id - self._media_title = self._tv.applications.get( + self._attr_media_content_type = MediaType.APP + self._attr_media_content_id = self._tv.application_id + self._attr_media_title = self._tv.applications.get( self._tv.application_id, {} ).get("label") - self._media_channel = None + self._attr_media_channel = None else: - self._media_content_type = None - self._media_content_id = None - self._media_title = self._sources.get(self._tv.source_id) - self._media_channel = None + self._attr_media_content_type = None + self._attr_media_content_id = None + self._attr_media_title = self._sources.get(self._tv.source_id) + self._attr_media_channel = None self._attr_assumed_state = True diff --git a/homeassistant/components/philips_js/remote.py b/homeassistant/components/philips_js/remote.py index 2c2f8752e0d..0aa61979eb2 100644 --- a/homeassistant/components/philips_js/remote.py +++ b/homeassistant/components/philips_js/remote.py @@ -1,4 +1,6 @@ """Remote control support for Apple TV.""" +from __future__ import annotations + import asyncio from collections.abc import Iterable from typing import Any @@ -68,7 +70,7 @@ class PhilipsTVRemote(CoordinatorEntity[PhilipsTVDataUpdateCoordinator], RemoteE ) @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if device is on.""" return bool( self._tv.on and (self._tv.powerstate == "On" or self._tv.powerstate is None) diff --git a/homeassistant/components/philips_js/translations/lv.json b/homeassistant/components/philips_js/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/philips_js/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index b33b9438354..49f1697adc6 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -5,9 +5,8 @@ import logging from hole import Hole from hole.exceptions import HoleError -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -22,8 +21,6 @@ from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -34,36 +31,13 @@ from .const import ( CONF_STATISTICS_ONLY, DATA_KEY_API, DATA_KEY_COORDINATOR, - DEFAULT_LOCATION, - DEFAULT_NAME, - DEFAULT_SSL, - DEFAULT_VERIFY_SSL, DOMAIN, MIN_TIME_BETWEEN_UPDATES, ) _LOGGER = logging.getLogger(__name__) -PI_HOLE_SCHEMA = vol.Schema( - vol.All( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_API_KEY): cv.string, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - }, - ) -) - -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [PI_HOLE_SCHEMA]))}, - ), - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) PLATFORMS = [ Platform.BINARY_SENSOR, @@ -73,35 +47,6 @@ PLATFORMS = [ ] -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Pi-hole integration.""" - - hass.data[DOMAIN] = {} - - if DOMAIN not in config: - return True - - async_create_issue( - hass, - DOMAIN, - "deprecated_yaml", - breaks_in_ha_version="2023.2.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - ) - - # import - for conf in config[DOMAIN]: - 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 Pi-hole entry.""" name = entry.data[CONF_NAME] @@ -133,7 +78,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await api.get_data() await api.get_versions() - _LOGGER.debug("async_update_data() api.data: %s", api.data) except HoleError as err: raise UpdateFailed(f"Failed to communicate with API: {err}") from err if not isinstance(api.data, dict): @@ -146,6 +90,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: update_method=async_update_data, update_interval=MIN_TIME_BETWEEN_UPDATES, ) + + hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = { DATA_KEY_API: api, DATA_KEY_COORDINATOR: coordinator, diff --git a/homeassistant/components/pi_hole/binary_sensor.py b/homeassistant/components/pi_hole/binary_sensor.py index 7d0d9034fad..4aa391b567f 100644 --- a/homeassistant/components/pi_hole/binary_sensor.py +++ b/homeassistant/components/pi_hole/binary_sensor.py @@ -1,11 +1,17 @@ """Support for getting status from a Pi-hole system.""" from __future__ import annotations +from collections.abc import Callable +from dataclasses import dataclass from typing import Any from hole import Hole -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant @@ -13,12 +19,68 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import PiHoleEntity -from .const import ( - BINARY_SENSOR_TYPES, - DATA_KEY_API, - DATA_KEY_COORDINATOR, - DOMAIN as PIHOLE_DOMAIN, - PiHoleBinarySensorEntityDescription, +from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN as PIHOLE_DOMAIN + + +@dataclass +class RequiredPiHoleBinaryDescription: + """Represent the required attributes of the PiHole binary description.""" + + state_value: Callable[[Hole], bool] + + +@dataclass +class PiHoleBinarySensorEntityDescription( + BinarySensorEntityDescription, RequiredPiHoleBinaryDescription +): + """Describes PiHole binary sensor entity.""" + + extra_value: Callable[[Hole], dict[str, Any] | None] = lambda api: None + + +BINARY_SENSOR_TYPES: tuple[PiHoleBinarySensorEntityDescription, ...] = ( + PiHoleBinarySensorEntityDescription( + # Deprecated, scheduled to be removed in 2022.6 + key="core_update_available", + name="Core Update Available", + entity_registry_enabled_default=False, + device_class=BinarySensorDeviceClass.UPDATE, + extra_value=lambda api: { + "current_version": api.versions["core_current"], + "latest_version": api.versions["core_latest"], + }, + state_value=lambda api: bool(api.versions["core_update"]), + ), + PiHoleBinarySensorEntityDescription( + # Deprecated, scheduled to be removed in 2022.6 + key="web_update_available", + name="Web Update Available", + entity_registry_enabled_default=False, + device_class=BinarySensorDeviceClass.UPDATE, + extra_value=lambda api: { + "current_version": api.versions["web_current"], + "latest_version": api.versions["web_latest"], + }, + state_value=lambda api: bool(api.versions["web_update"]), + ), + PiHoleBinarySensorEntityDescription( + # Deprecated, scheduled to be removed in 2022.6 + key="ftl_update_available", + name="FTL Update Available", + entity_registry_enabled_default=False, + device_class=BinarySensorDeviceClass.UPDATE, + extra_value=lambda api: { + "current_version": api.versions["FTL_current"], + "latest_version": api.versions["FTL_latest"], + }, + state_value=lambda api: bool(api.versions["FTL_update"]), + ), + PiHoleBinarySensorEntityDescription( + key="status", + name="Status", + icon="mdi:pi-hole", + state_value=lambda api: bool(api.data.get("status") == "enabled"), + ), ) diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py index 519ef5a7628..136e851429d 100644 --- a/homeassistant/components/pi_hole/config_flow.py +++ b/homeassistant/components/pi_hole/config_flow.py @@ -23,7 +23,6 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( - CONF_STATISTICS_ONLY, DEFAULT_LOCATION, DEFAULT_NAME, DEFAULT_SSL, @@ -121,42 +120,6 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Handle a flow initiated by import.""" - - host = user_input[CONF_HOST] - name = user_input[CONF_NAME] - location = user_input[CONF_LOCATION] - tls = user_input[CONF_SSL] - verify_tls = user_input[CONF_VERIFY_SSL] - endpoint = f"{host}/{location}" - - if await self._async_endpoint_existed(endpoint): - return self.async_abort(reason="already_configured") - - try: - await self._async_try_connect_legacy(host, location, tls, verify_tls) - except HoleError as ex: - _LOGGER.debug("Connection failed: %s", ex) - _LOGGER.error("Failed to import: %s", ex) - return self.async_abort(reason="cannot_connect") - self._config = { - CONF_HOST: host, - CONF_NAME: name, - CONF_LOCATION: location, - CONF_SSL: tls, - CONF_VERIFY_SSL: verify_tls, - } - api_key = user_input.get(CONF_API_KEY) - return self.async_create_entry( - title=name, - data={ - **self._config, - CONF_STATISTICS_ONLY: api_key is None, - CONF_API_KEY: api_key, - }, - ) - async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: """Perform reauth upon an API authentication error.""" self._config = dict(entry_data) @@ -208,17 +171,3 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not isinstance(pi_hole.data, dict): return {CONF_API_KEY: "invalid_auth"} return {} - - async def _async_endpoint_existed(self, endpoint: str) -> bool: - existing_endpoints = [ - f"{entry.data.get(CONF_HOST)}/{entry.data.get(CONF_LOCATION)}" - for entry in self._async_current_entries() - ] - return endpoint in existing_endpoints - - async def _async_try_connect_legacy( - self, host: str, location: str, tls: bool, verify_tls: bool - ) -> None: - session = async_get_clientsession(self.hass, verify_tls) - pi_hole = Hole(host, session, location=location, tls=tls) - await pi_hole.get_data() diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index a9bc5824ad9..0114a6621b5 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -1,19 +1,5 @@ """Constants for the pi_hole integration.""" -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass from datetime import timedelta -from typing import Any - -from hole import Hole - -from homeassistant.components.binary_sensor import ( - BinarySensorDeviceClass, - BinarySensorEntityDescription, -) -from homeassistant.components.sensor import SensorEntityDescription -from homeassistant.const import PERCENTAGE DOMAIN = "pi_hole" @@ -29,135 +15,7 @@ DEFAULT_STATISTICS_ONLY = True SERVICE_DISABLE = "disable" SERVICE_DISABLE_ATTR_DURATION = "duration" -ATTR_BLOCKED_DOMAINS = "domains_blocked" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) DATA_KEY_API = "api" DATA_KEY_COORDINATOR = "coordinator" - - -@dataclass -class PiHoleSensorEntityDescription(SensorEntityDescription): - """Describes PiHole sensor entity.""" - - icon: str = "mdi:pi-hole" - - -SENSOR_TYPES: tuple[PiHoleSensorEntityDescription, ...] = ( - PiHoleSensorEntityDescription( - key="ads_blocked_today", - name="Ads Blocked Today", - native_unit_of_measurement="ads", - icon="mdi:close-octagon-outline", - ), - PiHoleSensorEntityDescription( - key="ads_percentage_today", - name="Ads Percentage Blocked Today", - native_unit_of_measurement=PERCENTAGE, - icon="mdi:close-octagon-outline", - ), - PiHoleSensorEntityDescription( - key="clients_ever_seen", - name="Seen Clients", - native_unit_of_measurement="clients", - icon="mdi:account-outline", - ), - PiHoleSensorEntityDescription( - key="dns_queries_today", - name="DNS Queries Today", - native_unit_of_measurement="queries", - icon="mdi:comment-question-outline", - ), - PiHoleSensorEntityDescription( - key="domains_being_blocked", - name="Domains Blocked", - native_unit_of_measurement="domains", - icon="mdi:block-helper", - ), - PiHoleSensorEntityDescription( - key="queries_cached", - name="DNS Queries Cached", - native_unit_of_measurement="queries", - icon="mdi:comment-question-outline", - ), - PiHoleSensorEntityDescription( - key="queries_forwarded", - name="DNS Queries Forwarded", - native_unit_of_measurement="queries", - icon="mdi:comment-question-outline", - ), - PiHoleSensorEntityDescription( - key="unique_clients", - name="DNS Unique Clients", - native_unit_of_measurement="clients", - icon="mdi:account-outline", - ), - PiHoleSensorEntityDescription( - key="unique_domains", - name="DNS Unique Domains", - native_unit_of_measurement="domains", - icon="mdi:domain", - ), -) - - -@dataclass -class RequiredPiHoleBinaryDescription: - """Represent the required attributes of the PiHole binary description.""" - - state_value: Callable[[Hole], bool] - - -@dataclass -class PiHoleBinarySensorEntityDescription( - BinarySensorEntityDescription, RequiredPiHoleBinaryDescription -): - """Describes PiHole binary sensor entity.""" - - extra_value: Callable[[Hole], dict[str, Any] | None] = lambda api: None - - -BINARY_SENSOR_TYPES: tuple[PiHoleBinarySensorEntityDescription, ...] = ( - PiHoleBinarySensorEntityDescription( - # Deprecated, scheduled to be removed in 2022.6 - key="core_update_available", - name="Core Update Available", - entity_registry_enabled_default=False, - device_class=BinarySensorDeviceClass.UPDATE, - extra_value=lambda api: { - "current_version": api.versions["core_current"], - "latest_version": api.versions["core_latest"], - }, - state_value=lambda api: bool(api.versions["core_update"]), - ), - PiHoleBinarySensorEntityDescription( - # Deprecated, scheduled to be removed in 2022.6 - key="web_update_available", - name="Web Update Available", - entity_registry_enabled_default=False, - device_class=BinarySensorDeviceClass.UPDATE, - extra_value=lambda api: { - "current_version": api.versions["web_current"], - "latest_version": api.versions["web_latest"], - }, - state_value=lambda api: bool(api.versions["web_update"]), - ), - PiHoleBinarySensorEntityDescription( - # Deprecated, scheduled to be removed in 2022.6 - key="ftl_update_available", - name="FTL Update Available", - entity_registry_enabled_default=False, - device_class=BinarySensorDeviceClass.UPDATE, - extra_value=lambda api: { - "current_version": api.versions["FTL_current"], - "latest_version": api.versions["FTL_latest"], - }, - state_value=lambda api: bool(api.versions["FTL_update"]), - ), - PiHoleBinarySensorEntityDescription( - key="status", - name="Status", - icon="mdi:pi-hole", - state_value=lambda api: bool(api.data.get("status") == "enabled"), - ), -) diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index 0e231868647..dbca8661377 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -5,21 +5,71 @@ from typing import Any from hole import Hole -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import PiHoleEntity -from .const import ( - ATTR_BLOCKED_DOMAINS, - DATA_KEY_API, - DATA_KEY_COORDINATOR, - DOMAIN as PIHOLE_DOMAIN, - SENSOR_TYPES, - PiHoleSensorEntityDescription, +from .const import DATA_KEY_API, DATA_KEY_COORDINATOR, DOMAIN as PIHOLE_DOMAIN + +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="ads_blocked_today", + name="Ads Blocked Today", + native_unit_of_measurement="ads", + icon="mdi:close-octagon-outline", + ), + SensorEntityDescription( + key="ads_percentage_today", + name="Ads Percentage Blocked Today", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:close-octagon-outline", + ), + SensorEntityDescription( + key="clients_ever_seen", + name="Seen Clients", + native_unit_of_measurement="clients", + icon="mdi:account-outline", + ), + SensorEntityDescription( + key="dns_queries_today", + name="DNS Queries Today", + native_unit_of_measurement="queries", + icon="mdi:comment-question-outline", + ), + SensorEntityDescription( + key="domains_being_blocked", + name="Domains Blocked", + native_unit_of_measurement="domains", + icon="mdi:block-helper", + ), + SensorEntityDescription( + key="queries_cached", + name="DNS Queries Cached", + native_unit_of_measurement="queries", + icon="mdi:comment-question-outline", + ), + SensorEntityDescription( + key="queries_forwarded", + name="DNS Queries Forwarded", + native_unit_of_measurement="queries", + icon="mdi:comment-question-outline", + ), + SensorEntityDescription( + key="unique_clients", + name="DNS Unique Clients", + native_unit_of_measurement="clients", + icon="mdi:account-outline", + ), + SensorEntityDescription( + key="unique_domains", + name="DNS Unique Domains", + native_unit_of_measurement="domains", + icon="mdi:domain", + ), ) @@ -45,7 +95,7 @@ async def async_setup_entry( class PiHoleSensor(PiHoleEntity, SensorEntity): """Representation of a Pi-hole sensor.""" - entity_description: PiHoleSensorEntityDescription + entity_description: SensorEntityDescription def __init__( self, @@ -53,7 +103,7 @@ class PiHoleSensor(PiHoleEntity, SensorEntity): coordinator: DataUpdateCoordinator, name: str, server_unique_id: str, - description: PiHoleSensorEntityDescription, + description: SensorEntityDescription, ) -> None: """Initialize a Pi-hole sensor.""" super().__init__(api, coordinator, name, server_unique_id) @@ -69,8 +119,3 @@ class PiHoleSensor(PiHoleEntity, SensorEntity): return round(self.api.data[self.entity_description.key], 2) except TypeError: return self.api.data[self.entity_description.key] - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return the state attributes of the Pi-hole.""" - return {ATTR_BLOCKED_DOMAINS: self.api.data["domains_being_blocked"]} diff --git a/homeassistant/components/pi_hole/translations/bg.json b/homeassistant/components/pi_hole/translations/bg.json index 0a8f88b6b0d..48d51db3a80 100644 --- a/homeassistant/components/pi_hole/translations/bg.json +++ b/homeassistant/components/pi_hole/translations/bg.json @@ -1,10 +1,12 @@ { "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" + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { - "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" }, "step": { "api_key": { @@ -12,6 +14,12 @@ "api_key": "API \u043a\u043b\u044e\u0447" } }, + "reauth_confirm": { + "data": { + "api_key": "API \u043a\u043b\u044e\u0447" + }, + "description": "\u041c\u043e\u043b\u044f, \u0432\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043d\u043e\u0432 API \u043a\u043b\u044e\u0447 \u0437\u0430 PI-Hole \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 {host}/{location}" + }, "user": { "data": { "api_key": "API \u043a\u043b\u044e\u0447", diff --git a/homeassistant/components/pi_hole/translations/ca.json b/homeassistant/components/pi_hole/translations/ca.json index eb15fa7bf97..498a1c62b48 100644 --- a/homeassistant/components/pi_hole/translations/ca.json +++ b/homeassistant/components/pi_hole/translations/ca.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "El servei ja est\u00e0 configurat" + "already_configured": "El servei ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { - "cannot_connect": "Ha fallat la connexi\u00f3" + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "Clau API" } }, + "reauth_confirm": { + "data": { + "api_key": "Clau API" + }, + "description": "Introdueix una nova clau API per a PI-Hole a {host}/{location}", + "title": "Reautenticaci\u00f3 de la integraci\u00f3 PI-Hole" + }, "user": { "data": { "api_key": "Clau API", @@ -20,7 +29,6 @@ "name": "Nom", "port": "Port", "ssl": "Utilitza un certificat SSL", - "statistics_only": "Nom\u00e9s les estad\u00edstiques", "verify_ssl": "Verifica el certificat SSL" } } diff --git a/homeassistant/components/pi_hole/translations/cs.json b/homeassistant/components/pi_hole/translations/cs.json index fa90fbdb2a0..a9057ceabab 100644 --- a/homeassistant/components/pi_hole/translations/cs.json +++ b/homeassistant/components/pi_hole/translations/cs.json @@ -7,11 +7,6 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { - "api_key": { - "data": { - "api_key": "Kl\u00ed\u010d API" - } - }, "user": { "data": { "api_key": "Kl\u00ed\u010d API", diff --git a/homeassistant/components/pi_hole/translations/de.json b/homeassistant/components/pi_hole/translations/de.json index 831c1daf03e..958deb5a0c6 100644 --- a/homeassistant/components/pi_hole/translations/de.json +++ b/homeassistant/components/pi_hole/translations/de.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Der Dienst ist bereits konfiguriert" + "already_configured": "Der Dienst ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "API-Schl\u00fcssel" } }, + "reauth_confirm": { + "data": { + "api_key": "API-Schl\u00fcssel" + }, + "description": "Bitte gib einen neuen API-Schl\u00fcssel f\u00fcr PI-Hole unter {host}/{location} ein", + "title": "PI-Hole Integration erneut authentifizieren" + }, "user": { "data": { "api_key": "API-Schl\u00fcssel", @@ -20,16 +29,9 @@ "name": "Name", "port": "Port", "ssl": "Verwendet ein SSL-Zertifikat", - "statistics_only": "Nur Statistiken", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" } } } - }, - "issues": { - "deprecated_yaml": { - "description": "Die Konfiguration von PI-Hole mit YAML wird entfernt. \n\nDeine vorhandene YAML-Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert. \n\nEntferne die PI-Hole-YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", - "title": "Die PI-Hole YAML-Konfiguration wird entfernt" - } } } \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/el.json b/homeassistant/components/pi_hole/translations/el.json index b6aa0fe5365..ff570e3e8d2 100644 --- a/homeassistant/components/pi_hole/translations/el.json +++ b/homeassistant/components/pi_hole/translations/el.json @@ -1,10 +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" + "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" + "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": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" } }, + "reauth_confirm": { + "data": { + "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" + }, + "description": "\u03a0\u03b1\u03c1\u03b1\u03ba\u03b1\u03bb\u03ce \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03bd\u03ad\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af api \u03b3\u03b9\u03b1 \u03c4\u03bf PI-Hole \u03c3\u03c4\u03bf {host}/{location}", + "title": "PI-Hole \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", @@ -20,7 +29,6 @@ "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/en.json b/homeassistant/components/pi_hole/translations/en.json index 940e3950281..a7ed9af1d8a 100644 --- a/homeassistant/components/pi_hole/translations/en.json +++ b/homeassistant/components/pi_hole/translations/en.json @@ -23,6 +23,7 @@ }, "user": { "data": { + "api_key": "API Key", "host": "Host", "location": "Location", "name": "Name", diff --git a/homeassistant/components/pi_hole/translations/es.json b/homeassistant/components/pi_hole/translations/es.json index dca8e8c7f98..884eb3c3a26 100644 --- a/homeassistant/components/pi_hole/translations/es.json +++ b/homeassistant/components/pi_hole/translations/es.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "El servicio ya est\u00e1 configurado" + "already_configured": "El servicio ya est\u00e1 configurado", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { - "cannot_connect": "No se pudo conectar" + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "Clave API" } }, + "reauth_confirm": { + "data": { + "api_key": "Clave API" + }, + "description": "Por favor, introduce una nueva clave API para PI-Hole en {host}/{location}", + "title": "Volver a autenticar la integraci\u00f3n Pi-Hole" + }, "user": { "data": { "api_key": "Clave API", @@ -20,7 +29,6 @@ "name": "Nombre", "port": "Puerto", "ssl": "Utiliza un certificado SSL", - "statistics_only": "S\u00f3lo las estad\u00edsticas", "verify_ssl": "Verificar el certificado SSL" } } diff --git a/homeassistant/components/pi_hole/translations/et.json b/homeassistant/components/pi_hole/translations/et.json index 4ff0fdd0ba8..c8b30439125 100644 --- a/homeassistant/components/pi_hole/translations/et.json +++ b/homeassistant/components/pi_hole/translations/et.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Teenus on juba seadistatud" + "already_configured": "Teenus on juba seadistatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { - "cannot_connect": "\u00dchendamine nurjus" + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "API v\u00f5ti" } }, + "reauth_confirm": { + "data": { + "api_key": "API v\u00f5ti" + }, + "description": "Sisesta PI-Hole'i uus API-v\u00f5ti aadressil {host}/{location}", + "title": "PI-Hole Taastuvasta sidumine" + }, "user": { "data": { "api_key": "API v\u00f5ti", @@ -20,7 +29,6 @@ "name": "Nimi", "port": "", "ssl": "Kasuatb SSL serti", - "statistics_only": "Ainult statistika", "verify_ssl": "Kontrolli SSL sertifikaati" } } diff --git a/homeassistant/components/pi_hole/translations/fr.json b/homeassistant/components/pi_hole/translations/fr.json index 4eb97a04756..730b4b4d0fe 100644 --- a/homeassistant/components/pi_hole/translations/fr.json +++ b/homeassistant/components/pi_hole/translations/fr.json @@ -1,13 +1,15 @@ { "config": { "abort": { - "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Le service est d\u00e9j\u00e0 configur\u00e9", + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi" }, "error": { - "cannot_connect": "\u00c9chec de connexion" + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide" }, "step": { - "api_key": { + "reauth_confirm": { "data": { "api_key": "Cl\u00e9 d'API" } @@ -20,7 +22,6 @@ "name": "Nom", "port": "Port", "ssl": "Utilise un certificat SSL", - "statistics_only": "Statistiques uniquement", "verify_ssl": "V\u00e9rifier le certificat SSL" } } diff --git a/homeassistant/components/pi_hole/translations/he.json b/homeassistant/components/pi_hole/translations/he.json index 9b4392617f9..4e178b02552 100644 --- a/homeassistant/components/pi_hole/translations/he.json +++ b/homeassistant/components/pi_hole/translations/he.json @@ -7,11 +7,6 @@ "cannot_connect": "\u05d4\u05d4\u05ea\u05d7\u05d1\u05e8\u05d5\u05ea \u05e0\u05db\u05e9\u05dc\u05d4" }, "step": { - "api_key": { - "data": { - "api_key": "\u05de\u05e4\u05ea\u05d7 API" - } - }, "user": { "data": { "api_key": "\u05de\u05e4\u05ea\u05d7 API", diff --git a/homeassistant/components/pi_hole/translations/hu.json b/homeassistant/components/pi_hole/translations/hu.json index e55c7d543e3..edbf77c3209 100644 --- a/homeassistant/components/pi_hole/translations/hu.json +++ b/homeassistant/components/pi_hole/translations/hu.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van" + "already_configured": "A szolg\u00e1ltat\u00e1s m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "API kulcs" } }, + "reauth_confirm": { + "data": { + "api_key": "API kulcs" + }, + "description": "K\u00e9rem, adjon meg egy \u00faj api-kulcsot a PI-Hole sz\u00e1m\u00e1ra a {host}/{location} c\u00edmen.", + "title": "PI-Hole Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, "user": { "data": { "api_key": "API kulcs", @@ -20,7 +29,6 @@ "name": "Elnevez\u00e9s", "port": "Port", "ssl": "SSL tan\u00fas\u00edtv\u00e1ny haszn\u00e1lata", - "statistics_only": "Csak statisztik\u00e1k", "verify_ssl": "SSL-tan\u00fas\u00edtv\u00e1ny ellen\u0151rz\u00e9se" } } diff --git a/homeassistant/components/pi_hole/translations/id.json b/homeassistant/components/pi_hole/translations/id.json index c38c0f5c9bb..303a7a4448b 100644 --- a/homeassistant/components/pi_hole/translations/id.json +++ b/homeassistant/components/pi_hole/translations/id.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Layanan sudah dikonfigurasi" + "already_configured": "Layanan sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" }, "error": { - "cannot_connect": "Gagal terhubung" + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "Kunci API" } }, + "reauth_confirm": { + "data": { + "api_key": "Kunci API" + }, + "description": "Masukkan kunci api baru untuk PI-Hole di {host}/{location}", + "title": "Autentikasi Ulang Integrasi PI-Hole" + }, "user": { "data": { "api_key": "Kunci API", @@ -20,7 +29,6 @@ "name": "Nama", "port": "Port", "ssl": "Menggunakan sertifikat SSL", - "statistics_only": "Hanya Statistik", "verify_ssl": "Verifikasi sertifikat SSL" } } diff --git a/homeassistant/components/pi_hole/translations/it.json b/homeassistant/components/pi_hole/translations/it.json index c25f7546c62..30ae009124f 100644 --- a/homeassistant/components/pi_hole/translations/it.json +++ b/homeassistant/components/pi_hole/translations/it.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Il servizio \u00e8 gi\u00e0 configurato" + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" }, "error": { - "cannot_connect": "Impossibile connettersi" + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "Chiave API" } }, + "reauth_confirm": { + "data": { + "api_key": "Chiave API" + }, + "description": "Inserisci una nuova chiave API per PI-Hole in {host}/{location}", + "title": "Autentica nuovamente l'integrazione PI-Hole" + }, "user": { "data": { "api_key": "Chiave API", @@ -20,7 +29,6 @@ "name": "Nome", "port": "Porta", "ssl": "Utilizza un certificato SSL", - "statistics_only": "Solo Statistiche", "verify_ssl": "Verifica il certificato SSL" } } diff --git a/homeassistant/components/pi_hole/translations/ja.json b/homeassistant/components/pi_hole/translations/ja.json index 313790dfcfc..1a301aa03b3 100644 --- a/homeassistant/components/pi_hole/translations/ja.json +++ b/homeassistant/components/pi_hole/translations/ja.json @@ -7,11 +7,6 @@ "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f" }, "step": { - "api_key": { - "data": { - "api_key": "API\u30ad\u30fc" - } - }, "user": { "data": { "api_key": "API\u30ad\u30fc", @@ -20,7 +15,6 @@ "name": "\u540d\u524d", "port": "\u30dd\u30fc\u30c8", "ssl": "SSL\u8a3c\u660e\u66f8\u3092\u4f7f\u7528\u3059\u308b", - "statistics_only": "\u7d71\u8a08\u306e\u307f", "verify_ssl": "SSL\u8a3c\u660e\u66f8\u3092\u78ba\u8a8d\u3059\u308b" } } diff --git a/homeassistant/components/pi_hole/translations/ko.json b/homeassistant/components/pi_hole/translations/ko.json index d79878d8a42..d374956e18b 100644 --- a/homeassistant/components/pi_hole/translations/ko.json +++ b/homeassistant/components/pi_hole/translations/ko.json @@ -7,11 +7,6 @@ "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" }, "step": { - "api_key": { - "data": { - "api_key": "API \ud0a4" - } - }, "user": { "data": { "api_key": "API \ud0a4", @@ -20,7 +15,6 @@ "name": "\uc774\ub984", "port": "\ud3ec\ud2b8", "ssl": "SSL \uc778\uc99d\uc11c \uc0ac\uc6a9", - "statistics_only": "\ud1b5\uacc4 \uc804\uc6a9", "verify_ssl": "SSL \uc778\uc99d\uc11c \ud655\uc778" } } diff --git a/homeassistant/components/pi_hole/translations/nl.json b/homeassistant/components/pi_hole/translations/nl.json index 8199969aae8..b3659838e04 100644 --- a/homeassistant/components/pi_hole/translations/nl.json +++ b/homeassistant/components/pi_hole/translations/nl.json @@ -1,16 +1,20 @@ { "config": { "abort": { - "already_configured": "Dienst is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" }, "error": { - "cannot_connect": "Kan geen verbinding maken" + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie" }, "step": { - "api_key": { + "reauth_confirm": { "data": { "api_key": "API-sleutel" - } + }, + "description": "Voer een nieuwe API-sleutel in van Pi-Hole op {host}/{location}", + "title": "Pi-Hole-Integratie herauthenticeren" }, "user": { "data": { @@ -20,7 +24,6 @@ "name": "Naam", "port": "Poort", "ssl": "Maakt gebruik van een SSL-certificaat", - "statistics_only": "Alleen statistieken", "verify_ssl": "SSL-certificaat verifi\u00ebren" } } diff --git a/homeassistant/components/pi_hole/translations/no.json b/homeassistant/components/pi_hole/translations/no.json index 7d005fa6516..531411777ff 100644 --- a/homeassistant/components/pi_hole/translations/no.json +++ b/homeassistant/components/pi_hole/translations/no.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Tjenesten er allerede konfigurert" + "already_configured": "Tjenesten er allerede konfigurert", + "reauth_successful": "Re-autentisering var vellykket" }, "error": { - "cannot_connect": "Tilkobling mislyktes" + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "API-n\u00f8kkel" } }, + "reauth_confirm": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "description": "Vennligst skriv inn en ny API-n\u00f8kkel for PI-hull p\u00e5 {host} / {location}", + "title": "PI-Hole Godkjenne integrering p\u00e5 nytt" + }, "user": { "data": { "api_key": "API-n\u00f8kkel", @@ -20,7 +29,6 @@ "name": "Navn", "port": "Port", "ssl": "Bruker et SSL-sertifikat", - "statistics_only": "Bare statistikk", "verify_ssl": "Verifisere SSL-sertifikat" } } diff --git a/homeassistant/components/pi_hole/translations/pl.json b/homeassistant/components/pi_hole/translations/pl.json index ee4b6eadd87..c58382ef2e7 100644 --- a/homeassistant/components/pi_hole/translations/pl.json +++ b/homeassistant/components/pi_hole/translations/pl.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "Klucz API" } }, + "reauth_confirm": { + "data": { + "api_key": "Klucz API" + }, + "description": "Wprowad\u017a nowy klucz API PI-Hole dla {host}/{location}", + "title": "Ponownie uwierzytelnij integracj\u0119 PI-Hole" + }, "user": { "data": { "api_key": "Klucz API", @@ -20,7 +29,6 @@ "name": "Nazwa", "port": "Port", "ssl": "Certyfikat SSL", - "statistics_only": "Tylko statystyki", "verify_ssl": "Weryfikacja certyfikatu SSL" } } diff --git a/homeassistant/components/pi_hole/translations/pt-BR.json b/homeassistant/components/pi_hole/translations/pt-BR.json index 3de821afe8d..ef95e799ab2 100644 --- a/homeassistant/components/pi_hole/translations/pt-BR.json +++ b/homeassistant/components/pi_hole/translations/pt-BR.json @@ -1,17 +1,26 @@ { "config": { "abort": { - "already_configured": "O 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" + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { "api_key": { "data": { - "api_key": "Chave da API" + "api_key": "Chave API" } }, + "reauth_confirm": { + "data": { + "api_key": "Chave API" + }, + "description": "Insira uma nova chave de API para o PI-Hole em {host}/{location}", + "title": "Reautentica\u00e7\u00e3o da integra\u00e7\u00e3o do PI-Hole" + }, "user": { "data": { "api_key": "Chave da API", @@ -20,7 +29,6 @@ "name": "Nome", "port": "Porta", "ssl": "Usar um certificado SSL", - "statistics_only": "Somente estat\u00edsticas", "verify_ssl": "Verifique o certificado SSL" } } diff --git a/homeassistant/components/pi_hole/translations/pt.json b/homeassistant/components/pi_hole/translations/pt.json index ff1c0fcd1be..adc699eeb1a 100644 --- a/homeassistant/components/pi_hole/translations/pt.json +++ b/homeassistant/components/pi_hole/translations/pt.json @@ -7,11 +7,6 @@ "cannot_connect": "A liga\u00e7\u00e3o falhou" }, "step": { - "api_key": { - "data": { - "api_key": "Chave da API" - } - }, "user": { "data": { "api_key": "Chave da API", diff --git a/homeassistant/components/pi_hole/translations/ru.json b/homeassistant/components/pi_hole/translations/ru.json index eed9596c907..753c353b04f 100644 --- a/homeassistant/components/pi_hole/translations/ru.json +++ b/homeassistant/components/pi_hole/translations/ru.json @@ -1,10 +1,12 @@ { "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." + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "\u041a\u043b\u044e\u0447 API" } }, + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043d\u043e\u0432\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0434\u043b\u044f PI-Hole \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 {host}/{location}", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f PI-Hole" + }, "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", @@ -20,7 +29,6 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "port": "\u041f\u043e\u0440\u0442", "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", - "statistics_only": "\u0422\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0430", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" } } diff --git a/homeassistant/components/pi_hole/translations/sk.json b/homeassistant/components/pi_hole/translations/sk.json index ee03e7a4993..f49dadb240f 100644 --- a/homeassistant/components/pi_hole/translations/sk.json +++ b/homeassistant/components/pi_hole/translations/sk.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1" + "already_configured": "Slu\u017eba u\u017e je nakonfigurovan\u00e1", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" }, "error": { - "cannot_connect": "Nepodarilo sa pripoji\u0165" + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "API k\u013e\u00fa\u010d" } }, + "reauth_confirm": { + "data": { + "api_key": "API k\u013e\u00fa\u010d" + }, + "description": "Zadajte nov\u00fd k\u013e\u00fa\u010d api pre PI-Hole na adrese {host}/{location}", + "title": "PI-Hole Znova overi\u0165 integr\u00e1ciu" + }, "user": { "data": { "api_key": "API k\u013e\u00fa\u010d", @@ -20,7 +29,6 @@ "name": "N\u00e1zov", "port": "Port", "ssl": "Pou\u017e\u00edva SSL certifik\u00e1t", - "statistics_only": "Iba \u0161tatistika", "verify_ssl": "Overi\u0165 SSL certifik\u00e1t" } } diff --git a/homeassistant/components/pi_hole/translations/sv.json b/homeassistant/components/pi_hole/translations/sv.json index 589fe66fa9b..cbee4f2d6c9 100644 --- a/homeassistant/components/pi_hole/translations/sv.json +++ b/homeassistant/components/pi_hole/translations/sv.json @@ -7,11 +7,6 @@ "cannot_connect": "Det gick inte att ansluta." }, "step": { - "api_key": { - "data": { - "api_key": "API-nyckel" - } - }, "user": { "data": { "api_key": "API-nyckel", @@ -20,7 +15,6 @@ "name": "Namn", "port": "Port", "ssl": "Anv\u00e4nd ett SSL certifikat", - "statistics_only": "Endast statistik", "verify_ssl": "Verifiera SSL-certifikat" } } diff --git a/homeassistant/components/pi_hole/translations/tr.json b/homeassistant/components/pi_hole/translations/tr.json index 3d9b617f197..8c0c24fc8c5 100644 --- a/homeassistant/components/pi_hole/translations/tr.json +++ b/homeassistant/components/pi_hole/translations/tr.json @@ -1,16 +1,20 @@ { "config": { "abort": { - "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "already_configured": "Hizmet 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" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" }, "step": { - "api_key": { + "reauth_confirm": { "data": { "api_key": "API Anahtar\u0131" - } + }, + "description": "L\u00fctfen {host} / {location} adresinde PI-Hole i\u00e7in yeni bir api anahtar\u0131 girin", + "title": "PI-Hole Entegrasyonu Yeniden Do\u011frula" }, "user": { "data": { @@ -20,7 +24,6 @@ "name": "Ad", "port": "Port", "ssl": "SSL sertifikas\u0131 kullan\u0131r", - "statistics_only": "Yaln\u0131zca \u0130statistikler", "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" } } diff --git a/homeassistant/components/pi_hole/translations/uk.json b/homeassistant/components/pi_hole/translations/uk.json index 93413f9abff..72813dee73c 100644 --- a/homeassistant/components/pi_hole/translations/uk.json +++ b/homeassistant/components/pi_hole/translations/uk.json @@ -1,12 +1,24 @@ { "config": { "abort": { - "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" }, "error": { - "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + "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\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" }, "step": { + "api_key": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + }, + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + }, "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", diff --git a/homeassistant/components/pi_hole/translations/zh-Hant.json b/homeassistant/components/pi_hole/translations/zh-Hant.json index e8948ae5735..d1cffe52fec 100644 --- a/homeassistant/components/pi_hole/translations/zh-Hant.json +++ b/homeassistant/components/pi_hole/translations/zh-Hant.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557" + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, "step": { "api_key": { @@ -12,6 +14,13 @@ "api_key": "API \u91d1\u9470" } }, + "reauth_confirm": { + "data": { + "api_key": "API \u91d1\u9470" + }, + "description": "\u8acb\u8f38\u5165\u4f4d\u65bc {host}/{location} \u7684 PI-Hole \u65b0\u5bc6\u9470", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408 PI-Hole" + }, "user": { "data": { "api_key": "API \u91d1\u9470", @@ -20,7 +29,6 @@ "name": "\u540d\u7a31", "port": "\u901a\u8a0a\u57e0", "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49", - "statistics_only": "\u50c5\u7d71\u8a08\u8cc7\u8a0a", "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" } } diff --git a/homeassistant/components/picnic/translations/lv.json b/homeassistant/components/picnic/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/picnic/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/picnic/translations/uk.json b/homeassistant/components/picnic/translations/uk.json new file mode 100644 index 00000000000..28676798223 --- /dev/null +++ b/homeassistant/components/picnic/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "country_code": "\u041a\u043e\u0434 \u043a\u0440\u0430\u0457\u043d\u0438", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py index 0386d267f04..bcf43a0b735 100644 --- a/homeassistant/components/pilight/__init__.py +++ b/homeassistant/components/pilight/__init__.py @@ -7,10 +7,9 @@ import functools import logging import socket import threading -from typing import Any +from typing import Any, ParamSpec from pilight import pilight -from typing_extensions import ParamSpec import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/components/ping/__init__.py b/homeassistant/components/ping/__init__.py index c3699e0fe2d..2236b8dc337 100644 --- a/homeassistant/components/ping/__init__.py +++ b/homeassistant/components/ping/__init__.py @@ -36,9 +36,9 @@ def _can_use_icmp_lib_with_privilege() -> None | bool: " socket" ) return None - else: - _LOGGER.debug("Using icmplib in privileged=False mode") - return False - else: - _LOGGER.debug("Using icmplib in privileged=True mode") - return True + + _LOGGER.debug("Using icmplib in privileged=False mode") + return False + + _LOGGER.debug("Using icmplib in privileged=True mode") + return True diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index b4266c8e9a7..c3729f04c14 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -9,7 +9,7 @@ import subprocess from icmplib import async_multiping import voluptuous as vol -from homeassistant import const, util +from homeassistant import util from homeassistant.components.device_tracker import ( CONF_SCAN_INTERVAL, PLATFORM_SCHEMA as BASE_PLATFORM_SCHEMA, @@ -17,6 +17,7 @@ from homeassistant.components.device_tracker import ( AsyncSeeCallback, SourceType, ) +from homeassistant.const import CONF_HOSTS from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_point_in_utc_time @@ -34,7 +35,7 @@ CONCURRENT_PING_LIMIT = 6 PLATFORM_SCHEMA = BASE_PLATFORM_SCHEMA.extend( { - vol.Required(const.CONF_HOSTS): {cv.slug: cv.string}, + vol.Required(CONF_HOSTS): {cv.slug: cv.string}, vol.Optional(CONF_PING_COUNT, default=1): cv.positive_int, } ) @@ -87,7 +88,7 @@ async def async_setup_scanner( """Set up the Host objects and return the update function.""" privileged = hass.data[DOMAIN][PING_PRIVS] - ip_to_dev_id = {ip: dev_id for (dev_id, ip) in config[const.CONF_HOSTS].items()} + ip_to_dev_id = {ip: dev_id for (dev_id, ip) in config[CONF_HOSTS].items()} interval = config.get( CONF_SCAN_INTERVAL, timedelta(seconds=len(ip_to_dev_id) * config[CONF_PING_COUNT]) + SCAN_INTERVAL, @@ -101,7 +102,7 @@ async def async_setup_scanner( if privileged is None: hosts = [ HostSubProcess(ip, dev_id, hass, config, privileged) - for (dev_id, ip) in config[const.CONF_HOSTS].items() + for (dev_id, ip) in config[CONF_HOSTS].items() ] async def async_update(now): diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py index 620e314fdd8..a124362251a 100644 --- a/homeassistant/components/pioneer/media_player.py +++ b/homeassistant/components/pioneer/media_player.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging import telnetlib +from typing import Final import voluptuous as vol @@ -24,7 +25,7 @@ CONF_SOURCES = "sources" DEFAULT_NAME = "Pioneer AVR" DEFAULT_PORT = 23 # telnet default. Some Pioneer AVRs use 8102 -DEFAULT_TIMEOUT = None +DEFAULT_TIMEOUT: Final = None DEFAULT_SOURCES: dict[str, str] = {} diff --git a/homeassistant/components/plaato/translations/el.json b/homeassistant/components/plaato/translations/el.json index 8813a4d71bf..ab96156d468 100644 --- a/homeassistant/components/plaato/translations/el.json +++ b/homeassistant/components/plaato/translations/el.json @@ -7,7 +7,7 @@ "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!" + "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", @@ -32,7 +32,7 @@ "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.", + "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 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \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 \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\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.", "title": "Webhook \u03b3\u03b9\u03b1 \u03c7\u03c1\u03ae\u03c3\u03b7" } } diff --git a/homeassistant/components/plaato/translations/hu.json b/homeassistant/components/plaato/translations/hu.json index 990ecc7a561..6694267cd6f 100644 --- a/homeassistant/components/plaato/translations/hu.json +++ b/homeassistant/components/plaato/translations/hu.json @@ -7,7 +7,7 @@ "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." }, "create_entry": { - "default": "A Plaato {device_type} **{device_name}** n\u00e9vvel sikeresen telep\u00edtve lett!" + "default": "A Plaato {device_type} eszk\u00f6z **{device_name}** n\u00e9vvel sikeresen telep\u00edtve lett!" }, "error": { "invalid_webhook_device": "Olyan eszk\u00f6zt v\u00e1lasztott, amely nem t\u00e1mogatja az adatok webhookra t\u00f6rt\u00e9n\u0151 k\u00fcld\u00e9s\u00e9t. Csak az Airlock sz\u00e1m\u00e1ra \u00e9rhet\u0151 el", @@ -32,7 +32,7 @@ "title": "A Plaato eszk\u00f6z\u00f6k be\u00e1ll\u00edt\u00e1sa" }, "webhook": { - "description": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Plaato Airlock-ban. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST \n\n Tov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1ssa a [dokument\u00e1ci\u00f3t]({docs_url}).", + "description": "Ha esem\u00e9nyeket szeretne k\u00fcldeni Home Assistantba, be kell \u00e1ll\u00edtania a Plaato Airlock webhook funkci\u00f3j\u00e1t. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 adatokat: \n\n - URL: `{webhook_url}`\n - Met\u00f3dus: POST\n\nB\u0151vebb inform\u00e1ci\u00f3 [a dokument\u00e1ci\u00f3ban]({docs_url}) olvashat\u00f3.", "title": "Haszn\u00e1land\u00f3 webhook" } } @@ -47,7 +47,7 @@ "title": "Opci\u00f3k a Plaato sz\u00e1m\u00e1ra" }, "webhook": { - "description": "Webhook inform\u00e1ci\u00f3k: \n\n - URL: \" {webhook_url} \"\n - M\u00f3dszer: POST \n\n", + "description": "Webhook inform\u00e1ci\u00f3k: \n\n - URL: `{webhook_url}`\n - Met\u00f3dus: POST\n\n", "title": "Opci\u00f3k a Plaato Airlock-hoz" } } diff --git a/homeassistant/components/plaato/translations/sk.json b/homeassistant/components/plaato/translations/sk.json index af05e58e84e..4df7cedfa19 100644 --- a/homeassistant/components/plaato/translations/sk.json +++ b/homeassistant/components/plaato/translations/sk.json @@ -20,7 +20,7 @@ "token": "Sem vlo\u017ete autoriza\u010dn\u00fd token", "use_webhook": "Pou\u017eite webhook" }, - "description": "Aby ste mohli vyh\u013ead\u00e1va\u0165 API, je potrebn\u00fd \u201eauth_token\u201c, ktor\u00fd mo\u017eno z\u00edska\u0165 pod\u013ea [t\u00fdchto](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) pokynov \n\nVybran\u00e9 zariadenie: **{device_type}** \n\nAk rad\u0161ej pou\u017e\u00edvate vstavan\u00fa met\u00f3du webhooku (iba Airlock), za\u010diarknite pol\u00ed\u010dko ni\u017e\u0161ie a ponechajte Auth Token pr\u00e1zdne", + "description": "Aby ste mohli vyh\u013ead\u00e1va\u0165 API, je potrebn\u00fd `auth_token`, ktor\u00fd mo\u017eno z\u00edska\u0165 pod\u013ea [t\u00fdchto](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) pokynov \n\nVybran\u00e9 zariadenie: **{device_type}** \n\nAk rad\u0161ej pou\u017e\u00edvate vstavan\u00fa met\u00f3du webhooku (iba Airlock), za\u010diarknite pol\u00ed\u010dko ni\u017e\u0161ie a ponechajte Auth Token pr\u00e1zdne", "title": "Vyberte met\u00f3du API" }, "user": { diff --git a/homeassistant/components/plaato/translations/tr.json b/homeassistant/components/plaato/translations/tr.json index 21f2bddcc35..11e2480b909 100644 --- a/homeassistant/components/plaato/translations/tr.json +++ b/homeassistant/components/plaato/translations/tr.json @@ -7,7 +7,7 @@ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, "create_entry": { - "default": "Plaato {device_type} ad\u0131 ** ile {device_name} ** ba\u015far\u0131yla kurulum oldu!" + "default": "** {device_name} {device_type} ba\u015far\u0131yla kuruldu!" }, "error": { "invalid_webhook_device": "Webhook veri g\u00f6ndermeyi desteklemeyen bir cihaz se\u00e7tiniz. Yaln\u0131zca Airlock i\u00e7in kullan\u0131labilir", @@ -28,11 +28,11 @@ "device_name": "Cihaz\u0131n\u0131z\u0131 adland\u0131r\u0131n", "device_type": "Plaato cihaz\u0131n\u0131n t\u00fcr\u00fc" }, - "description": "Kuruluma ba\u015flamak ister misiniz?", + "description": "Kurulumu ba\u015flatmak istiyor musunuz?", "title": "Plaato cihazlar\u0131n\u0131 kurun" }, "webhook": { - "description": "Olaylar\u0131 Home Assistant'a g\u00f6ndermek i\u00e7in Plaato Airlock'ta webhook \u00f6zelli\u011fini kurman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url}", + "description": "Etkinlikleri Home Assistant'a g\u00f6ndermek i\u00e7in Plaato Airlock'ta webhook \u00f6zelli\u011fini ayarlaman\u0131z gerekir. \n\n A\u015fa\u011f\u0131daki bilgileri doldurun: \n\n - URL: ` {webhook_url} `\n - Y\u00f6ntem: POST \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url} ) bak\u0131n.", "title": "Webhook kullanmak i\u00e7in" } } diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index b43c4dc0e21..13422beec4f 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -4,11 +4,10 @@ from __future__ import annotations from collections.abc import Callable from functools import wraps import logging -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar import plexapi.exceptions import requests.exceptions -from typing_extensions import Concatenate, ParamSpec from homeassistant.components.media_player import ( DOMAIN as MP_DOMAIN, diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index cb88e98257d..b5080dd2c5c 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -118,7 +118,7 @@ def process_plex_payload( if content_id.startswith(PLEX_URI_SCHEME + "{"): # Handle the special payload of 'plex://{}' - content_id = content_id[len(PLEX_URI_SCHEME) :] + content_id = content_id.removeprefix(PLEX_URI_SCHEME) content = json.loads(content_id) elif content_id.startswith(PLEX_URI_SCHEME): # Handle standard media_browser payloads diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index c1f759622fa..dd13e0e5092 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -20,7 +20,6 @@ PW_TYPE: Final = "plugwise_type" SMILE: Final = "smile" STRETCH: Final = "stretch" STRETCH_USERNAME: Final = "stretch" -UNIT_LUMEN: Final = "lm" PLATFORMS_GATEWAY: Final[list[str]] = [ Platform.BINARY_SENSOR, diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index e10d86f2779..a69aaae8eeb 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.25.14"], + "requirements": ["plugwise==0.27.5"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true, diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 91fd32c92c2..6039cadf392 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -9,7 +9,9 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + LIGHT_LUX, PERCENTAGE, + UnitOfElectricPotential, UnitOfEnergy, UnitOfPower, UnitOfPressure, @@ -20,7 +22,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import DOMAIN, UNIT_LUMEN +from .const import DOMAIN from .coordinator import PlugwiseDataUpdateCoordinator from .entity import PlugwiseEntity @@ -162,6 +164,13 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL, ), + SensorEntityDescription( + key="electricity_consumed_point", + name="Electricity consumed point", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), SensorEntityDescription( key="electricity_consumed_off_peak_point", name="Electricity consumed off peak point", @@ -190,6 +199,13 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), + SensorEntityDescription( + key="electricity_produced_point", + name="Electricity produced point", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), SensorEntityDescription( key="electricity_produced_off_peak_point", name="Electricity produced off peak point", @@ -218,6 +234,72 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), + SensorEntityDescription( + key="electricity_phase_one_consumed", + name="Electricity phase one consumed", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="electricity_phase_two_consumed", + name="Electricity phase two consumed", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="electricity_phase_three_consumed", + name="Electricity phase three consumed", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="electricity_phase_one_produced", + name="Electricity phase one produced", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="electricity_phase_two_produced", + name="Electricity phase two produced", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="electricity_phase_three_produced", + name="Electricity phase three produced", + device_class=SensorDeviceClass.POWER, + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="voltage_phase_one", + name="Voltage phase one", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="voltage_phase_two", + name="Voltage phase two", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="voltage_phase_three", + name="Voltage phase three", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), SensorEntityDescription( key="gas_consumed_interval", name="Gas consumed interval", @@ -257,7 +339,8 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="illuminance", name="Illuminance", - native_unit_of_measurement=UNIT_LUMEN, + native_unit_of_measurement=LIGHT_LUX, + device_class=SensorDeviceClass.ILLUMINANCE, state_class=SensorStateClass.MEASUREMENT, ), SensorEntityDescription( diff --git a/homeassistant/components/plugwise/translations/bg.json b/homeassistant/components/plugwise/translations/bg.json index d8b8c7d10a0..695ead020c3 100644 --- a/homeassistant/components/plugwise/translations/bg.json +++ b/homeassistant/components/plugwise/translations/bg.json @@ -20,6 +20,17 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "vacation": "\u0412\u0430\u043a\u0430\u043d\u0446\u0438\u044f" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/ca.json b/homeassistant/components/plugwise/translations/ca.json index 1c5b3fb67be..373a8781f80 100644 --- a/homeassistant/components/plugwise/translations/ca.json +++ b/homeassistant/components/plugwise/translations/ca.json @@ -47,7 +47,7 @@ "auto": "Autom\u00e0tic", "boost": "Incrementat", "comfort": "Confort", - "off": "OFF" + "off": "Inactiu" } }, "regulation_mode": { @@ -56,7 +56,7 @@ "bleeding_hot": "Molt calent", "cooling": "Refredant", "heating": "Escalfant", - "off": "OFF" + "off": "Inactiu" } } } diff --git a/homeassistant/components/plugwise/translations/cs.json b/homeassistant/components/plugwise/translations/cs.json index 02a806f4548..c9aa41e18d9 100644 --- a/homeassistant/components/plugwise/translations/cs.json +++ b/homeassistant/components/plugwise/translations/cs.json @@ -20,5 +20,22 @@ "title": "Typ Plugwise" } } + }, + "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "Noc", + "away": "Pry\u010d", + "home": "Doma", + "no_frost": "Proti mrazu", + "vacation": "Pr\u00e1zdniny" + } + } + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index b554c8a689d..59d4ed957e6 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -26,6 +26,21 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "Nacht", + "away": "Abwesend", + "home": "Anwesend", + "no_frost": "Frostschutz", + "vacation": "Urlaub" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/el.json b/homeassistant/components/plugwise/translations/el.json index 385a98fc255..1ad0d671cb3 100644 --- a/homeassistant/components/plugwise/translations/el.json +++ b/homeassistant/components/plugwise/translations/el.json @@ -7,7 +7,7 @@ "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_setup": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd Adam \u03c3\u03b1\u03c2 \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd Anna \u03c3\u03b1\u03c2, \u03b1\u03bd\u03b1\u03c4\u03c1\u03ad\u03be\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Home Assistant Plugwise \u03b3\u03b9\u03b1 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2", + "invalid_setup": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c3\u03c4\u03b5 \u03c4\u03bf\u03bd Adam \u03b1\u03bd\u03c4\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd Anna \u03c3\u03b1\u03c2, \u03b4\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7", "response_error": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 XML \u03ae \u03bb\u03b7\u03c6\u03b8\u03b5\u03af\u03c3\u03b1 \u03ad\u03bd\u03b4\u03b5\u03b9\u03be\u03b7 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1\u03c4\u03bf\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", "unsupported": "\u03a3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03bc\u03b5 \u03bc\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03bf \u03c5\u03bb\u03b9\u03ba\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03bc\u03b9\u03ba\u03cc" @@ -20,12 +20,27 @@ "port": "\u0398\u03cd\u03c1\u03b1", "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 Smile" }, - "description": "\u03a0\u03c1\u03bf\u03ca\u03cc\u03bd:", - "title": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03b2\u03cd\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2" + "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" } } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "\u039d\u03cd\u03c7\u03c4\u03b1", + "away": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03a3\u03c0\u03b9\u03c4\u03b9\u03bf\u03cd", + "home": "\u03a3\u03c0\u03af\u03c4\u03b9", + "no_frost": "\u0391\u03bd\u03c4\u03b9\u03c0\u03b1\u03b3\u03b5\u03c4\u03b9\u03ba\u03cc", + "vacation": "\u0394\u03b9\u03b1\u03ba\u03bf\u03c0\u03ad\u03c2" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/et.json b/homeassistant/components/plugwise/translations/et.json index 88740292a6f..87b61096588 100644 --- a/homeassistant/components/plugwise/translations/et.json +++ b/homeassistant/components/plugwise/translations/et.json @@ -26,6 +26,21 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "\u00d6\u00f6", + "away": "Eemal", + "home": "Kodus", + "no_frost": "K\u00fclmumiskaitse", + "vacation": "Puhkus" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/fr.json b/homeassistant/components/plugwise/translations/fr.json index ee2b91b189e..99a2f40a8b1 100644 --- a/homeassistant/components/plugwise/translations/fr.json +++ b/homeassistant/components/plugwise/translations/fr.json @@ -22,5 +22,19 @@ "title": "Se connecter \u00e0 Smile" } } + }, + "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "no_frost": "Hors-gel", + "vacation": "Vacances" + } + } + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/hu.json b/homeassistant/components/plugwise/translations/hu.json index 35de97a32ef..b1d3b3a33d2 100644 --- a/homeassistant/components/plugwise/translations/hu.json +++ b/homeassistant/components/plugwise/translations/hu.json @@ -26,6 +26,21 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "\u00c9jszaka", + "away": "T\u00e1vol", + "home": "Otthon", + "no_frost": "Fagyv\u00e9delem", + "vacation": "Vak\u00e1ci\u00f3" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/id.json b/homeassistant/components/plugwise/translations/id.json index cda92a53e7f..6b2e5ab8da5 100644 --- a/homeassistant/components/plugwise/translations/id.json +++ b/homeassistant/components/plugwise/translations/id.json @@ -26,6 +26,21 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "Malam", + "away": "Keluar", + "home": "Di Rumah", + "no_frost": "Anti beku", + "vacation": "Liburan" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/it.json b/homeassistant/components/plugwise/translations/it.json index 8e74f8bfb7c..978ceaf661a 100644 --- a/homeassistant/components/plugwise/translations/it.json +++ b/homeassistant/components/plugwise/translations/it.json @@ -26,6 +26,21 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "Notte", + "away": "Fuori casa", + "home": "In casa", + "no_frost": "Antigelo", + "vacation": "Vacanza" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/lt.json b/homeassistant/components/plugwise/translations/lt.json new file mode 100644 index 00000000000..1fda31f7c80 --- /dev/null +++ b/homeassistant/components/plugwise/translations/lt.json @@ -0,0 +1,19 @@ +{ + "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "Naktis", + "away": "I\u0161vyk\u0119s", + "home": "Namuose", + "no_frost": "Apsauga nuo \u0161al\u010dio", + "vacation": "U\u017erakinti - atostog\u0173 re\u017eime" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/nl.json b/homeassistant/components/plugwise/translations/nl.json index dc8044ee29f..5a11aec17af 100644 --- a/homeassistant/components/plugwise/translations/nl.json +++ b/homeassistant/components/plugwise/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Dienst is al geconfigureerd" + "already_configured": "Dienst is al geconfigureerd", + "anna_with_adam": "Zowel Anna als Adam gedetecteerd. Voeg je Adam toe in plaats van je Anna" }, "error": { "cannot_connect": "Kan geen verbinding maken", @@ -25,6 +26,20 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "Nacht", + "away": "Afwezig", + "home": "Thuis", + "vacation": "Vakantie" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/no.json b/homeassistant/components/plugwise/translations/no.json index 338b08b6624..a7b93f9a4e4 100644 --- a/homeassistant/components/plugwise/translations/no.json +++ b/homeassistant/components/plugwise/translations/no.json @@ -26,6 +26,21 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "Natt", + "away": "Borte", + "home": "Hjemme", + "no_frost": "Anti-frost", + "vacation": "Ferie" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/pl.json b/homeassistant/components/plugwise/translations/pl.json index 5d0041b0ef2..9ca6387ec65 100644 --- a/homeassistant/components/plugwise/translations/pl.json +++ b/homeassistant/components/plugwise/translations/pl.json @@ -26,6 +26,21 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "noc", + "away": "poza domem", + "home": "w domu", + "no_frost": "przeciwzamro\u017ceniowy", + "vacation": "tryb wakacyjny" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/pt-BR.json b/homeassistant/components/plugwise/translations/pt-BR.json index fe3378b77ba..3e0ed8fd9e9 100644 --- a/homeassistant/components/plugwise/translations/pt-BR.json +++ b/homeassistant/components/plugwise/translations/pt-BR.json @@ -26,6 +26,21 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "Noite", + "away": "Fora", + "home": "Casa", + "no_frost": "Anti-geada", + "vacation": "F\u00e9rias" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json index 08bd7f13228..c2cba8a73b0 100644 --- a/homeassistant/components/plugwise/translations/ru.json +++ b/homeassistant/components/plugwise/translations/ru.json @@ -26,6 +26,21 @@ } }, "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "\u041d\u043e\u0447\u044c", + "away": "\u041d\u0435 \u0434\u043e\u043c\u0430", + "home": "\u0414\u043e\u043c\u0430", + "no_frost": "\u0410\u043d\u0442\u0438-\u0437\u0430\u043c\u0435\u0440\u0437\u0430\u043d\u0438\u0435", + "vacation": "\u041e\u0442\u043f\u0443\u0441\u043a" + } + } + } + } + }, "select": { "dhw_mode": { "state": { diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json index fb14d8da2d8..1261a691782 100644 --- a/homeassistant/components/plugwise/translations/tr.json +++ b/homeassistant/components/plugwise/translations/tr.json @@ -7,8 +7,10 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", - "invalid_setup": "Anna'n\u0131z yerine Adam'\u0131n\u0131z\u0131 ekleyin, daha fazla bilgi i\u00e7in Home Assistant Plugwise entegrasyon belgelerine bak\u0131n", - "unknown": "Beklenmeyen hata" + "invalid_setup": "Anna'n\u0131z yerine Adam'\u0131n\u0131z\u0131 ekleyin, belgelere bak\u0131n", + "response_error": "Ge\u00e7ersiz XML verileri veya al\u0131nan hata g\u00f6stergesi", + "unknown": "Beklenmeyen hata", + "unsupported": "Desteklenmeyen \u00fcr\u00fcn yaz\u0131l\u0131m\u0131na sahip cihaz" }, "step": { "user": { @@ -22,5 +24,41 @@ "title": "Smile'a Ba\u011flan\u0131n" } } + }, + "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "Gece", + "away": "D\u0131\u015far\u0131da", + "home": "Evde", + "no_frost": "Anti-don", + "vacation": "Tatil" + } + } + } + } + }, + "select": { + "dhw_mode": { + "state": { + "auto": "Otomatik", + "boost": "G\u00fc\u00e7l\u00fc", + "comfort": "Konfor", + "off": "Kapal\u0131" + } + }, + "regulation_mode": { + "state": { + "bleeding_cold": "So\u011futma", + "bleeding_hot": "Is\u0131tma", + "cooling": "So\u011futuluyor", + "heating": "Is\u0131t\u0131l\u0131yor", + "off": "Kapal\u0131" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/uk.json b/homeassistant/components/plugwise/translations/uk.json index ac62753459b..155a0bc27c0 100644 --- a/homeassistant/components/plugwise/translations/uk.json +++ b/homeassistant/components/plugwise/translations/uk.json @@ -10,9 +10,25 @@ }, "step": { "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 Smile" + }, "description": "\u041f\u0440\u043e\u0434\u0443\u043a\u0442:", "title": "\u0422\u0438\u043f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Plugwise" } } + }, + "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "asleep": "\u041d\u0456\u0447" + } + } + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/zh-Hans.json b/homeassistant/components/plugwise/translations/zh-Hans.json index b8ed72ea7af..48f8bdb131d 100644 --- a/homeassistant/components/plugwise/translations/zh-Hans.json +++ b/homeassistant/components/plugwise/translations/zh-Hans.json @@ -4,5 +4,18 @@ "response_error": "\u65e0\u6548\u7684 XML \u6570\u636e\uff0c\u6216\u6536\u5230\u7684\u9519\u8bef\u6307\u793a", "unsupported": "\u8bbe\u5907\u5b89\u88c5\u4e86\u4e0d\u88ab\u652f\u6301\u7684\u56fa\u4ef6" } + }, + "entity": { + "climate": { + "plugwise": { + "state_attributes": { + "preset_mode": { + "state": { + "vacation": "\u5ea6\u5047\u6a21\u5f0f" + } + } + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plugwise/util.py b/homeassistant/components/plugwise/util.py index 55d9c204dd3..2abb1051d74 100644 --- a/homeassistant/components/plugwise/util.py +++ b/homeassistant/components/plugwise/util.py @@ -1,9 +1,8 @@ """Utilities for Plugwise.""" from collections.abc import Awaitable, Callable, Coroutine -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from plugwise.exceptions import PlugwiseException -from typing_extensions import Concatenate, ParamSpec from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/plum_lightpad/translations/lt.json b/homeassistant/components/plum_lightpad/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/translations/tr.json b/homeassistant/components/point/translations/tr.json index 9bdf954f0c8..413008bde05 100644 --- a/homeassistant/components/point/translations/tr.json +++ b/homeassistant/components/point/translations/tr.json @@ -23,7 +23,7 @@ "data": { "flow_impl": "Sa\u011flay\u0131c\u0131" }, - "description": "Kuruluma ba\u015flamak ister misiniz?", + "description": "Kurulumu ba\u015flatmak istiyor musunuz?", "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" } } diff --git a/homeassistant/components/poolsense/translations/lt.json b/homeassistant/components/poolsense/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/poolsense/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index d8550e6f46b..3d4268a6178 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -35,7 +35,7 @@ from .models import PowerwallBaseInfo, PowerwallData, PowerwallRuntimeData CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) -PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH] _LOGGER = logging.getLogger(__name__) @@ -156,6 +156,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: base_info=base_info, http_session=http_session, coordinator=None, + api_instance=power_wall, ) manager = PowerwallDataManager(hass, power_wall, ip_address, password, runtime_data) diff --git a/homeassistant/components/powerwall/binary_sensor.py b/homeassistant/components/powerwall/binary_sensor.py index fed47823c7f..0bb089898d1 100644 --- a/homeassistant/components/powerwall/binary_sensor.py +++ b/homeassistant/components/powerwall/binary_sensor.py @@ -14,6 +14,11 @@ from .const import DOMAIN from .entity import PowerWallEntity from .models import PowerwallRuntimeData +CONNECTED_GRID_STATUSES = { + GridStatus.TRANSITION_TO_GRID, + GridStatus.CONNECTED, +} + async def async_setup_entry( hass: HomeAssistant, @@ -101,7 +106,7 @@ class PowerWallGridStatusSensor(PowerWallEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Grid is online.""" - return self.data.grid_status == GridStatus.CONNECTED + return self.data.grid_status in CONNECTED_GRID_STATUSES class PowerWallChargingStatusSensor(PowerWallEntity, BinarySensorEntity): diff --git a/homeassistant/components/powerwall/const.py b/homeassistant/components/powerwall/const.py index 9df710e2df4..b22e6466cf6 100644 --- a/homeassistant/components/powerwall/const.py +++ b/homeassistant/components/powerwall/const.py @@ -5,6 +5,7 @@ DOMAIN = "powerwall" POWERWALL_BASE_INFO: Final = "base_info" POWERWALL_COORDINATOR: Final = "coordinator" +POWERWALL_API: Final = "api_instance" POWERWALL_API_CHANGED: Final = "api_changed" POWERWALL_HTTP_SESSION: Final = "http_session" diff --git a/homeassistant/components/powerwall/entity.py b/homeassistant/components/powerwall/entity.py index 5d55b8b8bf1..1b42215483d 100644 --- a/homeassistant/components/powerwall/entity.py +++ b/homeassistant/components/powerwall/entity.py @@ -10,6 +10,7 @@ from .const import ( DOMAIN, MANUFACTURER, MODEL, + POWERWALL_API, POWERWALL_BASE_INFO, POWERWALL_COORDINATOR, ) @@ -25,6 +26,7 @@ class PowerWallEntity(CoordinatorEntity[DataUpdateCoordinator[PowerwallData]]): coordinator = powerwall_data[POWERWALL_COORDINATOR] assert coordinator is not None super().__init__(coordinator) + self.power_wall = powerwall_data[POWERWALL_API] # The serial numbers of the powerwalls are unique to every site self.base_unique_id = "_".join(base_info.serial_numbers) self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index d5baae17df6..f83982aa770 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -3,8 +3,8 @@ "name": "Tesla Powerwall", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/powerwall", - "requirements": ["tesla-powerwall==0.3.18"], - "codeowners": ["@bdraco", "@jrester"], + "requirements": ["tesla-powerwall==0.3.19"], + "codeowners": ["@bdraco", "@jrester", "@daniel-simpson"], "dhcp": [ { "hostname": "1118431-*" diff --git a/homeassistant/components/powerwall/models.py b/homeassistant/components/powerwall/models.py index e048cd559ba..3ee95b815f5 100644 --- a/homeassistant/components/powerwall/models.py +++ b/homeassistant/components/powerwall/models.py @@ -9,6 +9,7 @@ from tesla_powerwall import ( DeviceType, GridStatus, MetersAggregates, + Powerwall, PowerwallStatus, SiteInfo, SiteMaster, @@ -45,6 +46,7 @@ class PowerwallRuntimeData(TypedDict): """Run time data for the powerwall.""" coordinator: DataUpdateCoordinator[PowerwallData] | None + api_instance: Powerwall base_info: PowerwallBaseInfo api_changed: bool http_session: Session diff --git a/homeassistant/components/powerwall/switch.py b/homeassistant/components/powerwall/switch.py new file mode 100644 index 00000000000..41ae6a3cf1e --- /dev/null +++ b/homeassistant/components/powerwall/switch.py @@ -0,0 +1,74 @@ +"""Support for Powerwall Switches (V2 API only).""" + +from typing import Any + +from tesla_powerwall import GridStatus, IslandMode, PowerwallError + +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .entity import PowerWallEntity +from .models import PowerwallRuntimeData + +OFF_GRID_STATUSES = { + GridStatus.TRANSITION_TO_ISLAND, + GridStatus.ISLANDED, +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Powerwall switch platform from Powerwall resources.""" + powerwall_data: PowerwallRuntimeData = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities([PowerwallOffGridEnabledEntity(powerwall_data)]) + + +class PowerwallOffGridEnabledEntity(PowerWallEntity, SwitchEntity): + """Representation of a Switch entity for Powerwall Off-grid operation.""" + + _attr_name = "Off-Grid operation" + _attr_has_entity_name = True + _attr_entity_category = EntityCategory.CONFIG + _attr_device_class = SwitchDeviceClass.SWITCH + + def __init__(self, powerwall_data: PowerwallRuntimeData) -> None: + """Initialize powerwall entity and unique id.""" + super().__init__(powerwall_data) + self._attr_unique_id = f"{self.base_unique_id}_off_grid_operation" + + @property + def is_on(self) -> bool: + """Return true if the powerwall is off-grid.""" + return self.coordinator.data.grid_status in OFF_GRID_STATUSES + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn off-grid mode on.""" + await self._async_set_island_mode(IslandMode.OFFGRID) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off-grid mode off (return to on-grid usage).""" + await self._async_set_island_mode(IslandMode.ONGRID) + + async def _async_set_island_mode(self, island_mode: IslandMode) -> None: + """Toggles off-grid mode using the island_mode argument.""" + try: + await self.hass.async_add_executor_job( + self.power_wall.set_island_mode, island_mode + ) + except PowerwallError as ex: + raise HomeAssistantError( + f"Setting off-grid operation to {island_mode} failed: {ex}" + ) from ex + + self._attr_is_on = island_mode == IslandMode.OFFGRID + self.async_write_ha_state() + + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/powerwall/translations/lv.json b/homeassistant/components/powerwall/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/powerwall/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/tr.json b/homeassistant/components/powerwall/translations/tr.json index 48c076d8eed..e7f07885156 100644 --- a/homeassistant/components/powerwall/translations/tr.json +++ b/homeassistant/components/powerwall/translations/tr.json @@ -14,7 +14,7 @@ "flow_title": "{name} ({ip_address})", "step": { "confirm_discovery": { - "description": "{name} ( {ip_address} ) kurulumu yapmak istiyor musunuz?", + "description": "{name} ( {ip_address} ) kurmak istiyor musunuz?", "title": "Powerwall'a ba\u011flan\u0131n" }, "reauth_confim": { diff --git a/homeassistant/components/powerwall/translations/uk.json b/homeassistant/components/powerwall/translations/uk.json index cee740b4d50..0aeebb583b6 100644 --- a/homeassistant/components/powerwall/translations/uk.json +++ b/homeassistant/components/powerwall/translations/uk.json @@ -17,7 +17,8 @@ }, "user": { "data": { - "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, "title": "Tesla Powerwall" } diff --git a/homeassistant/components/profiler/translations/tr.json b/homeassistant/components/profiler/translations/tr.json index 48ce4808c04..0ee3ae71e9a 100644 --- a/homeassistant/components/profiler/translations/tr.json +++ b/homeassistant/components/profiler/translations/tr.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } } diff --git a/homeassistant/components/prosegur/translations/lv.json b/homeassistant/components/prosegur/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/prosegur/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prosegur/translations/uk.json b/homeassistant/components/prosegur/translations/uk.json new file mode 100644 index 00000000000..389cb28ca0c --- /dev/null +++ b/homeassistant/components/prosegur/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index e863122b872..d354a188a6e 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,6 +2,6 @@ "domain": "proxy", "name": "Camera Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["pillow==9.3.0"], + "requirements": ["pillow==9.4.0"], "codeowners": [] } diff --git a/homeassistant/components/prusalink/__init__.py b/homeassistant/components/prusalink/__init__.py index 0172a237da8..70853623f0e 100644 --- a/homeassistant/components/prusalink/__init__.py +++ b/homeassistant/components/prusalink/__init__.py @@ -77,7 +77,7 @@ class PrusaLinkUpdateCoordinator(DataUpdateCoordinator, Generic[T], ABC): async def _async_update_data(self) -> T: """Update the data.""" try: - with async_timeout.timeout(5): + async with async_timeout.timeout(5): data = await self._fetch_data() except InvalidAuth: raise UpdateFailed("Invalid authentication") from None diff --git a/homeassistant/components/prusalink/translations/lt.json b/homeassistant/components/prusalink/translations/lt.json new file mode 100644 index 00000000000..9c1a10c6595 --- /dev/null +++ b/homeassistant/components/prusalink/translations/lt.json @@ -0,0 +1,11 @@ +{ + "entity": { + "sensor": { + "printer_state": { + "state": { + "idle": "Laukiama" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/nl.json b/homeassistant/components/prusalink/translations/nl.json index 2a250129c18..2afc2808076 100644 --- a/homeassistant/components/prusalink/translations/nl.json +++ b/homeassistant/components/prusalink/translations/nl.json @@ -14,5 +14,15 @@ } } } + }, + "entity": { + "sensor": { + "printer_state": { + "state": { + "idle": "Inactief", + "paused": "Gepauzeerd" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.lt.json b/homeassistant/components/prusalink/translations/sensor.lt.json new file mode 100644 index 00000000000..777441b2898 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.lt.json @@ -0,0 +1,7 @@ +{ + "state": { + "prusalink__printer_state": { + "idle": "Laukiama" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/sensor.uk.json b/homeassistant/components/prusalink/translations/sensor.uk.json new file mode 100644 index 00000000000..1576fea6d68 --- /dev/null +++ b/homeassistant/components/prusalink/translations/sensor.uk.json @@ -0,0 +1,7 @@ +{ + "state": { + "prusalink__printer_state": { + "idle": "\u0411\u0435\u0437\u0434\u0456\u044f\u043b\u044c\u043d\u0456\u0441\u0442\u044c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/tr.json b/homeassistant/components/prusalink/translations/tr.json index 4658856d2fa..9384d12b0f9 100644 --- a/homeassistant/components/prusalink/translations/tr.json +++ b/homeassistant/components/prusalink/translations/tr.json @@ -14,5 +14,18 @@ } } } + }, + "entity": { + "sensor": { + "printer_state": { + "state": { + "cancelling": "\u0130ptal ediliyor", + "idle": "Bo\u015fta", + "paused": "Durduruldu", + "pausing": "Duraklat\u0131l\u0131yor", + "printing": "Yazd\u0131r\u0131l\u0131yor" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/prusalink/translations/uk.json b/homeassistant/components/prusalink/translations/uk.json new file mode 100644 index 00000000000..4a87308faba --- /dev/null +++ b/homeassistant/components/prusalink/translations/uk.json @@ -0,0 +1,11 @@ +{ + "entity": { + "sensor": { + "printer_state": { + "state": { + "idle": "\u0411\u0435\u0437\u0434\u0456\u044f\u043b\u044c\u043d\u0456\u0441\u0442\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index b5421af279b..50c11781fa1 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -41,7 +41,6 @@ _LOGGER = logging.getLogger(__name__) ICON = "mdi:sony-playstation" -MEDIA_IMAGE_DEFAULT = None DEFAULT_RETRIES = 2 @@ -374,13 +373,13 @@ class PS4Device(MediaPlayerEntity): f"/api/media_player_proxy/{self.entity_id}?" f"token={self.access_token}&cache={image_hash}" ) - return MEDIA_IMAGE_DEFAULT + return None @property def media_image_url(self): """Image url of current playing media.""" if self.media_content_id is None: - return MEDIA_IMAGE_DEFAULT + return None return self._media_image async def async_turn_off(self) -> None: diff --git a/homeassistant/components/ps4/translations/lv.json b/homeassistant/components/ps4/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/ps4/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/translations/sk.json b/homeassistant/components/ps4/translations/sk.json index 0fab44cdf25..5a59fe5779b 100644 --- a/homeassistant/components/ps4/translations/sk.json +++ b/homeassistant/components/ps4/translations/sk.json @@ -25,7 +25,7 @@ "region": "Regi\u00f3n" }, "data_description": { - "code": "Na konzole PlayStation 4 prejdite do \u010dasti Nastavenia. Potom prejdite na \u201eNastavenia pripojenia k mobilnej aplik\u00e1cii\u201c a vyberte \u201ePrida\u0165 zariadenie\u201c, aby ste z\u00edskali PIN." + "code": "Na konzole PlayStation 4 prejdite do \u010dasti Nastavenia. Potom prejdite na `Nastavenia pripojenia k mobilnej aplik\u00e1cii` a vyberte `Prida\u0165 zariadenie`, aby ste z\u00edskali PIN." } }, "mode": { diff --git a/homeassistant/components/pure_energie/translations/lv.json b/homeassistant/components/pure_energie/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/pure_energie/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pure_energie/translations/nl.json b/homeassistant/components/pure_energie/translations/nl.json index 14e705836b0..929dcbf2d91 100644 --- a/homeassistant/components/pure_energie/translations/nl.json +++ b/homeassistant/components/pure_energie/translations/nl.json @@ -12,6 +12,9 @@ "user": { "data": { "host": "Host" + }, + "data_description": { + "host": "Het IP-adres of de hostnaam van uw Pure Energie Meter." } }, "zeroconf_confirm": { diff --git a/homeassistant/components/purpleair/config_flow.py b/homeassistant/components/purpleair/config_flow.py index 0b1be019350..604bcb28c0e 100644 --- a/homeassistant/components/purpleair/config_flow.py +++ b/homeassistant/components/purpleair/config_flow.py @@ -367,7 +367,7 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): options = deepcopy({**self.config_entry.options}) options[CONF_SENSOR_INDICES].append(sensor_index) - return self.async_create_entry(title="", data=options) + return self.async_create_entry(data=options) async def async_step_init( self, user_input: dict[str, Any] | None = None @@ -436,4 +436,4 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow): options = deepcopy({**self.config_entry.options}) options[CONF_SENSOR_INDICES].remove(removed_sensor_index) - return self.async_create_entry(title="", data=options) + return self.async_create_entry(data=options) diff --git a/homeassistant/components/purpleair/sensor.py b/homeassistant/components/purpleair/sensor.py index 44fa63b2fbc..06c0d36610c 100644 --- a/homeassistant/components/purpleair/sensor.py +++ b/homeassistant/components/purpleair/sensor.py @@ -30,7 +30,6 @@ from . import PurpleAirEntity from .const import CONF_SENSOR_INDICES, DOMAIN from .coordinator import PurpleAirDataUpdateCoordinator -CONCENTRATION_IAQ = "iaq" CONCENTRATION_PARTICLES_PER_100_MILLILITERS = f"particles/100{UnitOfVolume.MILLILITERS}" diff --git a/homeassistant/components/purpleair/translations/ca.json b/homeassistant/components/purpleair/translations/ca.json index 95979489379..e9c3fde09c2 100644 --- a/homeassistant/components/purpleair/translations/ca.json +++ b/homeassistant/components/purpleair/translations/ca.json @@ -30,14 +30,14 @@ "data_description": { "sensor_index": "Sensor al qual fer-li seguiment" }, - "description": "Quins dels sensors propers t'agradaria seguir?" + "description": "Quins dels sensors propers us agradaria seguir?" }, "reauth_confirm": { "data": { "api_key": "Clau API" }, "data_description": { - "api_key": "Clau API de PurpleAir (si tens claus de lectura i escriptura, utilitza la de lectura)" + "api_key": "Clau API de PurpleAir (si teniu claus de lectura i escriptura, utilitzeu la de lectura)" } }, "user": { @@ -45,7 +45,7 @@ "api_key": "Clau API" }, "data_description": { - "api_key": "Clau API de PurpleAir (si tens claus de lectura i escriptura, utilitza la de lectura)" + "api_key": "Clau API de PurpleAir (si teniu claus de lectura i escriptura, utilitzeu la de lectura)" } } } @@ -81,13 +81,13 @@ "data_description": { "sensor_index": "Sensor al qual fer-li seguiment" }, - "description": "Quins dels sensors propers t'agradaria seguir?", - "title": "Tria sensor a afegir" + "description": "Quins dels sensors propers us agradaria seguir?", + "title": "Trieu el sensor a afegir" }, "init": { "menu_options": { - "add_sensor": "Afegeix sensor", - "remove_sensor": "Elimina sensor" + "add_sensor": "Afegeix un sensor", + "remove_sensor": "Elimina un sensor" } }, "remove_sensor": { @@ -97,7 +97,7 @@ "data_description": { "sensor_device_id": "Sensor a eliminar" }, - "title": "Elimina sensor" + "title": "Elimina un sensor" } } } diff --git a/homeassistant/components/purpleair/translations/cs.json b/homeassistant/components/purpleair/translations/cs.json new file mode 100644 index 00000000000..83e4716df12 --- /dev/null +++ b/homeassistant/components/purpleair/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + }, + "options": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/purpleair/translations/lv.json b/homeassistant/components/purpleair/translations/lv.json new file mode 100644 index 00000000000..8f5ce54bf27 --- /dev/null +++ b/homeassistant/components/purpleair/translations/lv.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "step": { + "by_coordinates": { + "data": { + "latitude": "Platums", + "longitude": "Garums" + } + } + } + }, + "options": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "step": { + "add_sensor": { + "data": { + "latitude": "Platums", + "longitude": "Garums" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/purpleair/translations/nl.json b/homeassistant/components/purpleair/translations/nl.json new file mode 100644 index 00000000000..4e4a35f3aa3 --- /dev/null +++ b/homeassistant/components/purpleair/translations/nl.json @@ -0,0 +1,70 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" + }, + "error": { + "invalid_api_key": "Ongeldige API-sleutel", + "unknown": "Onverwachte fout" + }, + "step": { + "by_coordinates": { + "data": { + "latitude": "Breedtegraad", + "longitude": "Lengtegraad" + } + }, + "choose_sensor": { + "data": { + "sensor_index": "Sensor" + } + }, + "reauth_confirm": { + "data": { + "api_key": "API-sleutel" + } + }, + "user": { + "data": { + "api_key": "API-sleutel" + } + } + } + }, + "options": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "invalid_api_key": "Ongeldige API-sleutel", + "unknown": "Onverwachte fout" + }, + "step": { + "add_sensor": { + "data": { + "latitude": "Breedtegraad", + "longitude": "Lengtegraad" + }, + "title": "Sensor toevoegen" + }, + "choose_sensor": { + "data": { + "sensor_index": "Sensor" + } + }, + "init": { + "menu_options": { + "add_sensor": "Sensor toevoegen", + "remove_sensor": "Sensor verwijderen" + } + }, + "remove_sensor": { + "data": { + "sensor_device_id": "Sensornaam" + }, + "title": "Sensor verwijderen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/purpleair/translations/pt-BR.json b/homeassistant/components/purpleair/translations/pt-BR.json index ab636a6192b..954cf5fd624 100644 --- a/homeassistant/components/purpleair/translations/pt-BR.json +++ b/homeassistant/components/purpleair/translations/pt-BR.json @@ -1,7 +1,7 @@ { "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": { @@ -34,25 +34,25 @@ }, "reauth_confirm": { "data": { - "api_key": "Chave de API" + "api_key": "Chave da API" }, "data_description": { - "api_key": "Sua chave de API PurpleAir (se voc\u00ea tiver chaves de leitura e grava\u00e7\u00e3o, use a chave de leitura)" + "api_key": "Sua chave de API PurpleAir (se voc\u00ea tiver chaves de leitura e grava\u00e7\u00e3o, use a chave de leitura" } }, "user": { "data": { - "api_key": "Chave de API" + "api_key": "Chave da API" }, "data_description": { - "api_key": "Sua chave de API PurpleAir (se voc\u00ea tiver chaves de leitura e grava\u00e7\u00e3o, use a chave de leitura)" + "api_key": "Sua chave de API PurpleAir (se voc\u00ea tiver chaves de leitura e grava\u00e7\u00e3o, use a chave de leitura" } } } }, "options": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" }, "error": { "invalid_api_key": "Chave de API inv\u00e1lida", diff --git a/homeassistant/components/purpleair/translations/sk.json b/homeassistant/components/purpleair/translations/sk.json index 71ea14fb3ad..ab26def7dce 100644 --- a/homeassistant/components/purpleair/translations/sk.json +++ b/homeassistant/components/purpleair/translations/sk.json @@ -62,7 +62,9 @@ "step": { "add_sensor": { "data": { - "distance": "Polomer vyh\u013ead\u00e1vania" + "distance": "Polomer vyh\u013ead\u00e1vania", + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka" }, "data_description": { "distance": "Polomer (v kilometroch) kruhu, v ktorom sa m\u00e1 vyh\u013ead\u00e1va\u0165", diff --git a/homeassistant/components/purpleair/translations/sv.json b/homeassistant/components/purpleair/translations/sv.json index 61d53cf5e6d..365394a2e35 100644 --- a/homeassistant/components/purpleair/translations/sv.json +++ b/homeassistant/components/purpleair/translations/sv.json @@ -1,6 +1,12 @@ { "config": { "step": { + "by_coordinates": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + } + }, "choose_sensor": { "data": { "sensor_index": "Sensor" @@ -11,6 +17,9 @@ "options": { "step": { "add_sensor": { + "data": { + "longitude": "Longitud" + }, "title": "L\u00e4gg till sensor" }, "choose_sensor": { diff --git a/homeassistant/components/purpleair/translations/tr.json b/homeassistant/components/purpleair/translations/tr.json new file mode 100644 index 00000000000..8b238311862 --- /dev/null +++ b/homeassistant/components/purpleair/translations/tr.json @@ -0,0 +1,104 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "no_sensors_near_coordinates": "Koordinatlar\u0131n yak\u0131n\u0131nda sens\u00f6r bulunamad\u0131 (mesafe i\u00e7inde)", + "unknown": "Beklenmeyen hata" + }, + "step": { + "by_coordinates": { + "data": { + "distance": "Yar\u0131\u00e7ap\u0131 ara\u015ft\u0131r", + "latitude": "Enlem", + "longitude": "Boylam" + }, + "data_description": { + "distance": "Aranacak dairenin yar\u0131\u00e7ap\u0131 (kilometre cinsinden)", + "latitude": "Sens\u00f6rlerin aranaca\u011f\u0131 enlem", + "longitude": "Sens\u00f6rlerin aranaca\u011f\u0131 boylam" + }, + "description": "Belirli bir enlem/boylam mesafesi i\u00e7inde bir PurpleAir sens\u00f6r\u00fc aray\u0131n." + }, + "choose_sensor": { + "data": { + "sensor_index": "Sens\u00f6r" + }, + "data_description": { + "sensor_index": "\u0130zlenecek sens\u00f6r" + }, + "description": "Yak\u0131ndaki sens\u00f6rlerden hangisini izlemek istersiniz?" + }, + "reauth_confirm": { + "data": { + "api_key": "API Anahtar\u0131" + }, + "data_description": { + "api_key": "PurpleAir API anahtar\u0131n\u0131z (hem okuma hem de yazma anahtar\u0131n\u0131z varsa, okuma anahtar\u0131n\u0131 kullan\u0131n)" + } + }, + "user": { + "data": { + "api_key": "API Anahtar\u0131" + }, + "data_description": { + "api_key": "PurpleAir API anahtar\u0131n\u0131z (hem okuma hem de yazma anahtar\u0131n\u0131z varsa, okuma anahtar\u0131n\u0131 kullan\u0131n)" + } + } + } + }, + "options": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "no_sensors_near_coordinates": "Koordinatlar\u0131n yak\u0131n\u0131nda sens\u00f6r bulunamad\u0131 (mesafe i\u00e7inde)", + "unknown": "Beklenmeyen hata" + }, + "step": { + "add_sensor": { + "data": { + "distance": "Yar\u0131\u00e7ap\u0131 ara\u015ft\u0131r", + "latitude": "Enlem", + "longitude": "Boylam" + }, + "data_description": { + "distance": "Aranacak dairenin yar\u0131\u00e7ap\u0131 (kilometre cinsinden)", + "latitude": "Sens\u00f6rlerin aranaca\u011f\u0131 enlem", + "longitude": "Sens\u00f6rlerin aranaca\u011f\u0131 boylam" + }, + "description": "Belirli bir enlem/boylam mesafesi i\u00e7inde bir PurpleAir sens\u00f6r\u00fc aray\u0131n.", + "title": "Sens\u00f6r Ekle" + }, + "choose_sensor": { + "data": { + "sensor_index": "Sens\u00f6r" + }, + "data_description": { + "sensor_index": "\u0130zlenecek sens\u00f6r" + }, + "description": "Yak\u0131ndaki sens\u00f6rlerden hangisini izlemek istersiniz?", + "title": "Eklenecek Sens\u00f6r\u00fc Se\u00e7in" + }, + "init": { + "menu_options": { + "add_sensor": "Sens\u00f6r ekle", + "remove_sensor": "Sens\u00f6r\u00fc kald\u0131r" + } + }, + "remove_sensor": { + "data": { + "sensor_device_id": "Sens\u00f6r Ad\u0131" + }, + "data_description": { + "sensor_device_id": "Kald\u0131r\u0131lacak sens\u00f6r" + }, + "title": "Sens\u00f6r\u00fc Kald\u0131r" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/purpleair/translations/uk.json b/homeassistant/components/purpleair/translations/uk.json new file mode 100644 index 00000000000..e60436636f1 --- /dev/null +++ b/homeassistant/components/purpleair/translations/uk.json @@ -0,0 +1,47 @@ +{ + "config": { + "step": { + "by_coordinates": { + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430" + } + }, + "choose_sensor": { + "data": { + "sensor_index": "\u0414\u0430\u0442\u0447\u0438\u043a" + } + } + } + }, + "options": { + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "add_sensor": { + "data_description": { + "distance": "\u0420\u0430\u0434\u0456\u0443\u0441 (\u0443 \u043a\u0456\u043b\u043e\u043c\u0435\u0442\u0440\u0430\u0445) \u043a\u043e\u043b\u0430 \u0434\u043b\u044f \u043f\u043e\u0448\u0443\u043a\u0443", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430, \u043d\u0430\u0432\u043a\u043e\u043b\u043e \u044f\u043a\u043e\u0457 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0448\u0443\u043a\u0430\u0442\u0438 \u0434\u0430\u0442\u0447\u0438\u043a\u0438", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430, \u043d\u0430\u0432\u043a\u043e\u043b\u043e \u044f\u043a\u043e\u0457 \u0448\u0443\u043a\u0430\u0442\u0438 \u0434\u0430\u0442\u0447\u0438\u043a\u0438" + }, + "title": "\u0414\u043e\u0434\u0430\u0442\u0438 \u0434\u0430\u0442\u0447\u0438\u043a" + }, + "choose_sensor": { + "data": { + "sensor_index": "\u0414\u0430\u0442\u0447\u0438\u043a" + } + }, + "init": { + "menu_options": { + "add_sensor": "\u0414\u043e\u0434\u0430\u0439\u0442\u0435 \u0434\u0430\u0442\u0447\u0438\u043a" + } + }, + "remove_sensor": { + "data": { + "sensor_device_id": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0430\u0442\u0447\u0438\u043a\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushbullet/translations/tr.json b/homeassistant/components/pushbullet/translations/tr.json new file mode 100644 index 00000000000..e949ff58cff --- /dev/null +++ b/homeassistant/components/pushbullet/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "name": "Ad" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "YAML kullanarak Pushbullet yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z, kullan\u0131c\u0131 aray\u00fcz\u00fcne otomatik olarak aktar\u0131ld\u0131. \n\n Configuration.yaml dosyan\u0131zdan Pushbullet YAML yap\u0131land\u0131rmas\u0131n\u0131 kald\u0131r\u0131n ve bu sorunu \u00e7\u00f6zmek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Pushbullet YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index fa4b35da2fa..9d6ae4b080d 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -103,7 +103,7 @@ class PushoverNotificationService(BaseNotificationService): if self._hass.config.is_allowed_path(data[ATTR_ATTACHMENT]): # try to open it as a normal file. try: - # pylint: disable=consider-using-with + # pylint: disable-next=consider-using-with file_handle = open(data[ATTR_ATTACHMENT], "rb") # Replace the attachment identifier with file object. image = file_handle diff --git a/homeassistant/components/pushover/translations/nl.json b/homeassistant/components/pushover/translations/nl.json index fb6ebf9862d..a58662b7aa6 100644 --- a/homeassistant/components/pushover/translations/nl.json +++ b/homeassistant/components/pushover/translations/nl.json @@ -6,7 +6,8 @@ }, "error": { "cannot_connect": "Kan geen verbinding maken", - "invalid_api_key": "Ongeldige API-sleutel" + "invalid_api_key": "Ongeldige API-sleutel", + "invalid_user_key": "Ongeldige gebruikerssleutel" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/pvoutput/strings.json b/homeassistant/components/pvoutput/strings.json index 644a756924c..12f30b773d5 100644 --- a/homeassistant/components/pvoutput/strings.json +++ b/homeassistant/components/pvoutput/strings.json @@ -20,6 +20,7 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } diff --git a/homeassistant/components/pvoutput/translations/bg.json b/homeassistant/components/pvoutput/translations/bg.json index 8ec410d2f18..f6d38b13daa 100644 --- a/homeassistant/components/pvoutput/translations/bg.json +++ b/homeassistant/components/pvoutput/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { diff --git a/homeassistant/components/pvoutput/translations/ca.json b/homeassistant/components/pvoutput/translations/ca.json index 952792be2a3..6d3f6cf2fb5 100644 --- a/homeassistant/components/pvoutput/translations/ca.json +++ b/homeassistant/components/pvoutput/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { diff --git a/homeassistant/components/pvoutput/translations/de.json b/homeassistant/components/pvoutput/translations/de.json index e7368b174b4..b6b10038370 100644 --- a/homeassistant/components/pvoutput/translations/de.json +++ b/homeassistant/components/pvoutput/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { diff --git a/homeassistant/components/pvoutput/translations/en.json b/homeassistant/components/pvoutput/translations/en.json index ad2194232e7..15ca2a91725 100644 --- a/homeassistant/components/pvoutput/translations/en.json +++ b/homeassistant/components/pvoutput/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Device is already configured", "reauth_successful": "Re-authentication was successful" }, "error": { diff --git a/homeassistant/components/pvoutput/translations/et.json b/homeassistant/components/pvoutput/translations/et.json index c5778063bac..f0870733500 100644 --- a/homeassistant/components/pvoutput/translations/et.json +++ b/homeassistant/components/pvoutput/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { diff --git a/homeassistant/components/pvoutput/translations/no.json b/homeassistant/components/pvoutput/translations/no.json index 6f64f05bcd8..7885be09779 100644 --- a/homeassistant/components/pvoutput/translations/no.json +++ b/homeassistant/components/pvoutput/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", "reauth_successful": "Re-autentisering var vellykket" }, "error": { diff --git a/homeassistant/components/pvoutput/translations/ru.json b/homeassistant/components/pvoutput/translations/ru.json index 88cfbdc44c1..f999f11fd61 100644 --- a/homeassistant/components/pvoutput/translations/ru.json +++ b/homeassistant/components/pvoutput/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "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": { diff --git a/homeassistant/components/pvoutput/translations/zh-Hant.json b/homeassistant/components/pvoutput/translations/zh-Hant.json index af2c6508245..6ac2b985b47 100644 --- a/homeassistant/components/pvoutput/translations/zh-Hant.json +++ b/homeassistant/components/pvoutput/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py index 27a006833ea..808ff1b4cc4 100644 --- a/homeassistant/components/pvpc_hourly_pricing/__init__.py +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -1,22 +1,15 @@ """The pvpc_hourly_pricing integration to collect Spain official electric prices.""" -from collections.abc import Mapping -from datetime import datetime, timedelta +from datetime import timedelta import logging -from aiopvpc import DEFAULT_POWER_KW, TARIFFS, PVPCData +from aiopvpc import DEFAULT_POWER_KW, TARIFFS, EsiosApiData, PVPCData import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_registry import ( - EntityRegistry, - async_get, - async_migrate_entries, -) -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util import dt as dt_util @@ -41,73 +34,11 @@ UI_CONFIG_SCHEMA = vol.Schema( vol.Required(ATTR_POWER_P3, default=DEFAULT_POWER_KW): VALID_POWER, } ) -CONFIG_SCHEMA = vol.Schema( - vol.All(cv.deprecated(DOMAIN), {DOMAIN: cv.ensure_list(UI_CONFIG_SCHEMA)}), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the electricity price sensor from configuration.yaml.""" - for conf in config.get(DOMAIN, []): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, data=conf, context={"source": SOURCE_IMPORT} - ) - ) - - return True +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up pvpc hourly pricing from a config entry.""" - if len(entry.data) == 2: - defaults = { - ATTR_TARIFF: _DEFAULT_TARIFF, - ATTR_POWER: DEFAULT_POWER_KW, - ATTR_POWER_P3: DEFAULT_POWER_KW, - } - data = {**entry.data, **defaults} - hass.config_entries.async_update_entry( - entry, unique_id=_DEFAULT_TARIFF, data=data, options=defaults - ) - - @callback - def update_unique_id(reg_entry): - """Change unique id for sensor entity, pointing to new tariff.""" - return {"new_unique_id": _DEFAULT_TARIFF} - - try: - await async_migrate_entries(hass, entry.entry_id, update_unique_id) - _LOGGER.warning( - ( - "Migrating PVPC sensor from old tariff '%s' to new '%s'. " - "Configure the integration to set your contracted power, " - "and select prices for Ceuta/Melilla, " - "if that is your case" - ), - entry.data[ATTR_TARIFF], - _DEFAULT_TARIFF, - ) - except ValueError: - # there were multiple sensors (with different old tariffs, up to 3), - # so we leave just one and remove the others - ent_reg: EntityRegistry = async_get(hass) - for entity_id, reg_entry in ent_reg.entities.items(): - if reg_entry.config_entry_id == entry.entry_id: - ent_reg.async_remove(entity_id) - _LOGGER.warning( - ( - "Old PVPC Sensor %s is removed " - "(another one already exists, using the same tariff)" - ), - entity_id, - ) - break - - await hass.config_entries.async_remove(entry.entry_id) - return False - coordinator = ElecPricesDataUpdateCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() @@ -121,7 +52,7 @@ async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: """Handle options update.""" if any( entry.data.get(attrib) != entry.options.get(attrib) - for attrib in (ATTR_TARIFF, ATTR_POWER, ATTR_POWER_P3) + for attrib in (ATTR_POWER, ATTR_POWER_P3) ): # update entry replacing data with new options hass.config_entries.async_update_entry( @@ -138,7 +69,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -class ElecPricesDataUpdateCoordinator(DataUpdateCoordinator[Mapping[datetime, float]]): +class ElecPricesDataUpdateCoordinator(DataUpdateCoordinator[EsiosApiData]): """Class to manage fetching Electricity prices data from API.""" def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: @@ -160,11 +91,13 @@ class ElecPricesDataUpdateCoordinator(DataUpdateCoordinator[Mapping[datetime, fl """Return entry ID.""" return self._entry.entry_id - async def _async_update_data(self) -> Mapping[datetime, float]: + async def _async_update_data(self) -> EsiosApiData: """Update electricity prices from the ESIOS API.""" - prices = await self.api.async_update_prices(dt_util.utcnow()) - self.api.process_state_and_attributes(dt_util.utcnow()) - if not prices: + api_data = await self.api.async_update_all(self.data, dt_util.utcnow()) + if ( + not api_data + or not api_data.sensors + or not all(api_data.availability.values()) + ): raise UpdateFailed - - return prices + return api_data diff --git a/homeassistant/components/pvpc_hourly_pricing/config_flow.py b/homeassistant/components/pvpc_hourly_pricing/config_flow.py index f5aeb951d33..9412aa2e97d 100644 --- a/homeassistant/components/pvpc_hourly_pricing/config_flow.py +++ b/homeassistant/components/pvpc_hourly_pricing/config_flow.py @@ -1,12 +1,15 @@ """Config flow for pvpc_hourly_pricing.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult -from . import CONF_NAME, UI_CONFIG_SCHEMA, VALID_POWER, VALID_TARIFF +from . import CONF_NAME, UI_CONFIG_SCHEMA, VALID_POWER from .const import ATTR_POWER, ATTR_POWER_P3, ATTR_TARIFF, DOMAIN @@ -23,7 +26,9 @@ class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return PVPCOptionsFlowHandler(config_entry) - 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: await self.async_set_unique_id(user_input[ATTR_TARIFF]) @@ -32,10 +37,6 @@ class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="user", data_schema=UI_CONFIG_SCHEMA) - async def async_step_import(self, import_info): - """Handle import from config file.""" - return await self.async_step_user(import_info) - class PVPCOptionsFlowHandler(config_entries.OptionsFlow): """Handle PVPC options.""" @@ -44,15 +45,14 @@ class PVPCOptionsFlowHandler(config_entries.OptionsFlow): """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 + ) -> FlowResult: """Manage the options.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) # Fill options with entry data - tariff = self.config_entry.options.get( - ATTR_TARIFF, self.config_entry.data[ATTR_TARIFF] - ) power = self.config_entry.options.get( ATTR_POWER, self.config_entry.data[ATTR_POWER] ) @@ -61,7 +61,6 @@ class PVPCOptionsFlowHandler(config_entries.OptionsFlow): ) schema = vol.Schema( { - vol.Required(ATTR_TARIFF, default=tariff): VALID_TARIFF, vol.Required(ATTR_POWER, default=power): VALID_POWER, vol.Required(ATTR_POWER_P3, default=power_valley): VALID_POWER, } diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json index 7b44d2cfa95..02bcee70d01 100644 --- a/homeassistant/components/pvpc_hourly_pricing/manifest.json +++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json @@ -3,9 +3,9 @@ "name": "Spain electricity hourly pricing (PVPC)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing", - "requirements": ["aiopvpc==3.0.0"], + "requirements": ["aiopvpc==4.0.1"], "codeowners": ["@azogue"], "quality_scale": "platinum", "iot_class": "cloud_polling", - "loggers": ["aiopvpc", "holidays"] + "loggers": ["aiopvpc"] } diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index 7419b8eef46..56b77dec401 100644 --- a/homeassistant/components/pvpc_hourly_pricing/sensor.py +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -142,14 +142,12 @@ class ElecPriceSensor(CoordinatorEntity[ElecPricesDataUpdateCoordinator], Sensor self._attr_unique_id = unique_id self._attr_name = name self._attr_device_info = DeviceInfo( - configuration_url="https://www.ree.es/es/apidatos", + configuration_url="https://api.esios.ree.es", entry_type=DeviceEntryType.SERVICE, identifiers={(DOMAIN, coordinator.entry_id)}, manufacturer="REE", - name="PVPC (REData API)", + name="ESIOS API", ) - self._state: StateType = None - self._attrs: Mapping[str, Any] = {} async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" @@ -171,21 +169,24 @@ class ElecPriceSensor(CoordinatorEntity[ElecPricesDataUpdateCoordinator], Sensor @callback def update_current_price(self, now: datetime) -> None: """Update the sensor state, by selecting the current price for this hour.""" - self.coordinator.api.process_state_and_attributes(now) + self.coordinator.api.process_state_and_attributes( + self.coordinator.data, self.entity_description.key, now + ) self.async_write_ha_state() @property def native_value(self) -> StateType: """Return the state of the sensor.""" - self._state = self.coordinator.api.state - return self._state + return self.coordinator.api.states.get(self.entity_description.key) @property - def extra_state_attributes(self) -> Mapping[str, Any] | None: + def extra_state_attributes(self) -> Mapping[str, Any]: """Return the state attributes.""" - self._attrs = { + sensor_attributes = self.coordinator.api.sensor_attributes.get( + self.entity_description.key, {} + ) + return { _PRICE_SENSOR_ATTRIBUTES_MAP[key]: value - for key, value in self.coordinator.api.attributes.items() + for key, value in sensor_attributes.items() if key in _PRICE_SENSOR_ATTRIBUTES_MAP } - return self._attrs diff --git a/homeassistant/components/pvpc_hourly_pricing/strings.json b/homeassistant/components/pvpc_hourly_pricing/strings.json index a008ef9f4da..1a0055ddbac 100644 --- a/homeassistant/components/pvpc_hourly_pricing/strings.json +++ b/homeassistant/components/pvpc_hourly_pricing/strings.json @@ -18,9 +18,8 @@ "step": { "init": { "data": { - "tariff": "Applicable tariff by geographic zone", - "power": "Contracted power (kW)", - "power_p3": "Contracted power for valley period P3 (kW)" + "power": "[%key:component::pvpc_hourly_pricing::config::step::user::data::power%]", + "power_p3": "[%key:component::pvpc_hourly_pricing::config::step::user::data::power_p3%]" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ca.json b/homeassistant/components/pvpc_hourly_pricing/translations/ca.json index b435e12a90a..fefba1b15b0 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ca.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ca.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Pot\u00e8ncia contractada (kW)", - "power_p3": "Pot\u00e8ncia contractada del per\u00edode vall P3 (kW)", - "tariff": "Tarifa aplicable per zona geogr\u00e0fica" + "power_p3": "Pot\u00e8ncia contractada del per\u00edode vall P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/de.json b/homeassistant/components/pvpc_hourly_pricing/translations/de.json index 49f54ee41d8..bb431a11a4c 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/de.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/de.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Vertraglich vereinbarte Leistung (kW)", - "power_p3": "Vertraglich vereinbarte Leistung f\u00fcr Talperiode P3 (kW)", - "tariff": "Geltender Tarif nach geografischer Zone" + "power_p3": "Vertraglich vereinbarte Leistung f\u00fcr Talperiode P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/el.json b/homeassistant/components/pvpc_hourly_pricing/translations/el.json index 4c9d056daf7..bbd26f63568 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/el.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/el.json @@ -19,8 +19,7 @@ "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" + "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)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/en.json b/homeassistant/components/pvpc_hourly_pricing/translations/en.json index 9667d14fd05..73a5a2ca306 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/en.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/en.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Contracted power (kW)", - "power_p3": "Contracted power for valley period P3 (kW)", - "tariff": "Applicable tariff by geographic zone" + "power_p3": "Contracted power for valley period P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/es.json b/homeassistant/components/pvpc_hourly_pricing/translations/es.json index eb96606831c..156b35a978f 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/es.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/es.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Potencia contratada (kW)", - "power_p3": "Potencia contratada para el per\u00edodo valle P3 (kW)", - "tariff": "Tarifa aplicable por zona geogr\u00e1fica" + "power_p3": "Potencia contratada para el per\u00edodo valle P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/et.json b/homeassistant/components/pvpc_hourly_pricing/translations/et.json index 8554ca18196..331d2cffcb3 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/et.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/et.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Lepinguj\u00e4rgne v\u00f5imsus (kW)", - "power_p3": "Lepinguj\u00e4rgne v\u00f5imsus soodusperioodil P3 (kW)", - "tariff": "Kohaldatav tariif geograafilise tsooni j\u00e4rgi" + "power_p3": "Lepinguj\u00e4rgne v\u00f5imsus soodusperioodil P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/fr.json b/homeassistant/components/pvpc_hourly_pricing/translations/fr.json index 1f0c447ff0f..e7d14a697f5 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/fr.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/fr.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Puissance souscrite (kW)", - "power_p3": "Puissance souscrite pour la p\u00e9riode de vall\u00e9e P3 (kW)", - "tariff": "Tarif applicable par zone g\u00e9ographique" + "power_p3": "Puissance souscrite pour la p\u00e9riode de vall\u00e9e P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/hu.json b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json index 121a87af124..3ed5f91710e 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/hu.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/hu.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Szerz\u0151d\u00e9s szerinti teljes\u00edtm\u00e9ny (kW)", - "power_p3": "Szerz\u0151d\u00f6tt teljes\u00edtm\u00e9ny P3 v\u00f6lgyid\u0151szakra (kW)", - "tariff": "Alkalmazand\u00f3 tarifa f\u00f6ldrajzi z\u00f3n\u00e1k szerint" + "power_p3": "Szerz\u0151d\u00f6tt teljes\u00edtm\u00e9ny P3 v\u00f6lgyid\u0151szakra (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/id.json b/homeassistant/components/pvpc_hourly_pricing/translations/id.json index 2bc5c67e533..c0a80ffa54b 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/id.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/id.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Daya terkontrak (kW)", - "power_p3": "Daya terkontrak untuk periode lembah P3 (kW)", - "tariff": "Tarif yang berlaku menurut zona geografis" + "power_p3": "Daya terkontrak untuk periode lembah P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/it.json b/homeassistant/components/pvpc_hourly_pricing/translations/it.json index bd6f8494d49..2ff6634e44f 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/it.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/it.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Potenza contrattuale (kW)", - "power_p3": "Potenza contrattuale per il periodo di valle P3 (kW)", - "tariff": "Tariffa applicabile per zona geografica" + "power_p3": "Potenza contrattuale per il periodo di valle P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ja.json b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json index 6b85de978d3..f592c8a7ccd 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ja.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ja.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "\u5951\u7d04\u96fb\u529b (kW)", - "power_p3": "\u8c37\u9593(valley period) P3 (kW)\u306e\u5951\u7d04\u96fb\u529b", - "tariff": "\u5730\u57df\u5225\u9069\u7528\u95a2\u7a0e(Applicable tariff)" + "power_p3": "\u8c37\u9593(valley period) P3 (kW)\u306e\u5951\u7d04\u96fb\u529b" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json index a7d1ac0e743..f86b1a336ed 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/nl.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/nl.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Gecontracteerd vermogen (kW)", - "power_p3": "Gecontracteerd vermogen voor dalperiode P3 (kW)", - "tariff": "Toepasselijk tarief per geografische zone" + "power_p3": "Gecontracteerd vermogen voor dalperiode P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/no.json b/homeassistant/components/pvpc_hourly_pricing/translations/no.json index 77dadcbcffd..a5909439476 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/no.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/no.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Kontrahert effekt (kW)", - "power_p3": "Kontrahert kraft for dalperioden P3 (kW)", - "tariff": "Gjeldende tariff etter geografisk sone" + "power_p3": "Kontraktstr\u00f8m for dalperiode P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/pl.json b/homeassistant/components/pvpc_hourly_pricing/translations/pl.json index 52fb03fa985..0e53f3db5e0 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/pl.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/pl.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Moc zakontraktowana (kW)", - "power_p3": "Moc zakontraktowana dla okresu zni\u017ckowego P3 (kW)", - "tariff": "Obowi\u0105zuj\u0105ca taryfa wed\u0142ug strefy geograficznej" + "power_p3": "Moc zakontraktowana dla okresu zni\u017ckowego P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/pt-BR.json b/homeassistant/components/pvpc_hourly_pricing/translations/pt-BR.json index 7d66f5af13a..bb4cf53abde 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/pt-BR.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/pt-BR.json @@ -19,8 +19,7 @@ "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" + "power_p3": "Pot\u00eancia contratada para o per\u00edodo de vale P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/ru.json b/homeassistant/components/pvpc_hourly_pricing/translations/ru.json index 7994c217216..34ec2fe209c 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/ru.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/ru.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "\u0414\u043e\u0433\u043e\u0432\u043e\u0440\u043d\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c (\u043a\u0412\u0442)", - "power_p3": "\u0414\u043e\u0433\u043e\u0432\u043e\u0440\u043d\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c \u043d\u0430 \u043f\u0435\u0440\u0438\u043e\u0434 P3 (\u043a\u0412\u0442)", - "tariff": "\u041f\u0440\u0438\u043c\u0435\u043d\u044f\u0435\u043c\u044b\u0439 \u0442\u0430\u0440\u0438\u0444 \u043f\u043e \u0433\u0435\u043e\u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u0437\u043e\u043d\u0435" + "power_p3": "\u0414\u043e\u0433\u043e\u0432\u043e\u0440\u043d\u0430\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u044c \u043d\u0430 \u043f\u0435\u0440\u0438\u043e\u0434 P3 (\u043a\u0412\u0442)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/sk.json b/homeassistant/components/pvpc_hourly_pricing/translations/sk.json index 009602df740..d677242625d 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/sk.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/sk.json @@ -8,6 +8,7 @@ "data": { "name": "N\u00e1zov sn\u00edma\u010da", "power": "Zmluvn\u00fd v\u00fdkon (kW)", + "power_p3": "Zmluvn\u00fd v\u00fdkon NT (kW)", "tariff": "Platn\u00e1 tarifa pod\u013ea geografickej z\u00f3ny" } } @@ -18,7 +19,7 @@ "init": { "data": { "power": "Zmluvn\u00fd v\u00fdkon (kW)", - "tariff": "Platn\u00e1 tarifa pod\u013ea geografickej z\u00f3ny" + "power_p3": "Zmluvn\u00fd v\u00fdkon NT (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/sv.json b/homeassistant/components/pvpc_hourly_pricing/translations/sv.json index fe05a9cbfd5..b9830fa2cb7 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/sv.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/sv.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "Kontrakterad effekt (kW)", - "power_p3": "Kontrakterad effekt f\u00f6r dalperiod P3 (kW)", - "tariff": "Till\u00e4mplig taxa per geografisk zon" + "power_p3": "Kontrakterad effekt f\u00f6r dalperiod P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/tr.json b/homeassistant/components/pvpc_hourly_pricing/translations/tr.json index 908f04f6622..680738e3fc4 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/tr.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/tr.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "S\u00f6zle\u015fmeli g\u00fc\u00e7 (kW)", - "power_p3": "Vadi d\u00f6nemi i\u00e7in taahh\u00fct edilen g\u00fc\u00e7 P3 (kW)", - "tariff": "Co\u011frafi b\u00f6lgeye g\u00f6re ge\u00e7erli tarife" + "power_p3": "Vadi d\u00f6nemi i\u00e7in taahh\u00fct edilen g\u00fc\u00e7 P3 (kW)" } } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/uk.json b/homeassistant/components/pvpc_hourly_pricing/translations/uk.json index bc6d06b1b04..1c1af55705d 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/uk.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/uk.json @@ -11,5 +11,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "power": "\u0414\u043e\u0433\u043e\u0432\u0456\u0440\u043d\u0430 \u043f\u043e\u0442\u0443\u0436\u043d\u0456\u0441\u0442\u044c (\u043a\u0412\u0442)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json b/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json index 35ace573ead..917d48ebe04 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/zh-Hant.json @@ -19,8 +19,7 @@ "init": { "data": { "power": "\u5408\u7d04\u529f\u7387\uff08kW\uff09", - "power_p3": "\u4f4e\u5cf0\u671f P3 \u5408\u7d04\u529f\u7387\uff08kW\uff09", - "tariff": "\u5206\u5340\u9069\u7528\u8cbb\u7387" + "power_p3": "\u4f4e\u5cf0\u671f P3 \u5408\u7d04\u529f\u7387\uff08kW\uff09" } } } diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index 1dd6e3d3799..bbb262ac7db 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -220,7 +220,7 @@ def execute(hass, filename, source, data=None): try: _LOGGER.info("Executing %s: %s", filename, data) - # pylint: disable=exec-used + # pylint: disable-next=exec-used exec(compiled.code, restricted_globals) except ScriptError as err: logger.error("Error executing script: %s", err) diff --git a/homeassistant/components/python_script/manifest.json b/homeassistant/components/python_script/manifest.json index 2bc2763e777..586873c2c9c 100644 --- a/homeassistant/components/python_script/manifest.json +++ b/homeassistant/components/python_script/manifest.json @@ -2,7 +2,7 @@ "domain": "python_script", "name": "Python Scripts", "documentation": "https://www.home-assistant.io/integrations/python_script", - "requirements": ["restrictedpython==5.2"], + "requirements": ["restrictedpython==6.0"], "codeowners": [], "quality_scale": "internal", "loggers": ["RestrictedPython"] diff --git a/homeassistant/components/qingping/binary_sensor.py b/homeassistant/components/qingping/binary_sensor.py index b3cb80ad0f2..3a40e1baa09 100644 --- a/homeassistant/components/qingping/binary_sensor.py +++ b/homeassistant/components/qingping/binary_sensor.py @@ -1,8 +1,6 @@ """Support for Qingping binary sensors.""" from __future__ import annotations -from typing import Optional - from qingping_ble import ( BinarySensorDeviceClass as QingpingBinarySensorDeviceClass, SensorUpdate, @@ -93,7 +91,7 @@ async def async_setup_entry( class QingpingBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[Optional[bool]]], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[bool | None]], BinarySensorEntity, ): """Representation of a Qingping binary sensor.""" diff --git a/homeassistant/components/qingping/manifest.json b/homeassistant/components/qingping/manifest.json index 31657280b19..db3db64500f 100644 --- a/homeassistant/components/qingping/manifest.json +++ b/homeassistant/components/qingping/manifest.json @@ -12,7 +12,7 @@ } ], "requirements": ["qingping-ble==0.8.2"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco", "@skgsergio"], "iot_class": "local_push" } diff --git a/homeassistant/components/qingping/sensor.py b/homeassistant/components/qingping/sensor.py index 84276c11292..a128bdede0b 100644 --- a/homeassistant/components/qingping/sensor.py +++ b/homeassistant/components/qingping/sensor.py @@ -1,8 +1,6 @@ """Support for Qingping sensors.""" from __future__ import annotations -from typing import Optional, Union - from qingping_ble import ( SensorDeviceClass as QingpingSensorDeviceClass, SensorUpdate, @@ -161,9 +159,7 @@ async def async_setup_entry( class QingpingBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a Qingping sensor.""" diff --git a/homeassistant/components/qingping/translations/lv.json b/homeassistant/components/qingping/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/qingping/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qingping/translations/tr.json b/homeassistant/components/qingping/translations/tr.json index f0ddbc274c9..36347c44f7f 100644 --- a/homeassistant/components/qingping/translations/tr.json +++ b/homeassistant/components/qingping/translations/tr.json @@ -9,13 +9,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/qingping/translations/uk.json b/homeassistant/components/qingping/translations/uk.json new file mode 100644 index 00000000000..e58b49d4c9e --- /dev/null +++ b/homeassistant/components/qingping/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/sensor.py b/homeassistant/components/qnap_qsw/sensor.py index a00103a0f32..94534ced850 100644 --- a/homeassistant/components/qnap_qsw/sensor.py +++ b/homeassistant/components/qnap_qsw/sensor.py @@ -1,19 +1,22 @@ """Support for the QNAP QSW sensors.""" from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, replace from typing import Final from aioqsw.const import ( QSD_FAN1_SPEED, QSD_FAN2_SPEED, + QSD_LACP_PORTS, QSD_LINK, QSD_PORT_NUM, + QSD_PORTS, QSD_PORTS_STATISTICS, QSD_PORTS_STATUS, QSD_RX_ERRORS, QSD_RX_OCTETS, QSD_RX_SPEED, + QSD_SPEED, QSD_SYSTEM_BOARD, QSD_SYSTEM_SENSOR, QSD_SYSTEM_TIME, @@ -43,7 +46,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ATTR_MAX, DOMAIN, QSW_COORD_DATA, RPM from .coordinator import QswDataCoordinator -from .entity import QswEntityDescription, QswSensorEntity +from .entity import QswEntityDescription, QswEntityType, QswSensorEntity @dataclass @@ -51,6 +54,8 @@ class QswSensorEntityDescription(SensorEntityDescription, QswEntityDescription): """A class that describes QNAP QSW sensor entities.""" attributes: dict[str, list[str]] | None = None + qsw_type: QswEntityType | None = None + sep_key: str = "_" SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( @@ -152,20 +157,195 @@ SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( ), ) +LACP_PORT_SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( + QswSensorEntityDescription( + device_class=SensorDeviceClass.DATA_RATE, + entity_registry_enabled_default=False, + icon="mdi:speedometer", + key=QSD_PORTS_STATUS, + name="Link Speed", + native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, + qsw_type=QswEntityType.LACP_PORT, + state_class=SensorStateClass.MEASUREMENT, + subkey=QSD_SPEED, + ), + QswSensorEntityDescription( + entity_registry_enabled_default=False, + icon="mdi:download-network", + key=QSD_PORTS_STATISTICS, + name="RX", + native_unit_of_measurement=UnitOfInformation.BYTES, + qsw_type=QswEntityType.LACP_PORT, + state_class=SensorStateClass.TOTAL_INCREASING, + subkey=QSD_RX_OCTETS, + ), + QswSensorEntityDescription( + entity_registry_enabled_default=False, + icon="mdi:close-network", + key=QSD_PORTS_STATISTICS, + entity_category=EntityCategory.DIAGNOSTIC, + name="RX Errors", + qsw_type=QswEntityType.LACP_PORT, + state_class=SensorStateClass.TOTAL_INCREASING, + subkey=QSD_RX_ERRORS, + ), + QswSensorEntityDescription( + device_class=SensorDeviceClass.DATA_RATE, + entity_registry_enabled_default=False, + icon="mdi:download-network", + key=QSD_PORTS_STATISTICS, + name="RX Speed", + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + qsw_type=QswEntityType.LACP_PORT, + state_class=SensorStateClass.MEASUREMENT, + subkey=QSD_RX_SPEED, + ), + QswSensorEntityDescription( + entity_registry_enabled_default=False, + icon="mdi:upload-network", + key=QSD_PORTS_STATISTICS, + name="TX", + native_unit_of_measurement=UnitOfInformation.BYTES, + qsw_type=QswEntityType.LACP_PORT, + state_class=SensorStateClass.TOTAL_INCREASING, + subkey=QSD_TX_OCTETS, + ), + QswSensorEntityDescription( + device_class=SensorDeviceClass.DATA_RATE, + entity_registry_enabled_default=False, + icon="mdi:upload-network", + key=QSD_PORTS_STATISTICS, + name="TX Speed", + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + qsw_type=QswEntityType.LACP_PORT, + state_class=SensorStateClass.MEASUREMENT, + subkey=QSD_TX_SPEED, + ), +) + +PORT_SENSOR_TYPES: Final[tuple[QswSensorEntityDescription, ...]] = ( + QswSensorEntityDescription( + device_class=SensorDeviceClass.DATA_RATE, + entity_registry_enabled_default=False, + icon="mdi:speedometer", + key=QSD_PORTS_STATUS, + name="Link Speed", + native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND, + qsw_type=QswEntityType.PORT, + state_class=SensorStateClass.MEASUREMENT, + subkey=QSD_SPEED, + ), + QswSensorEntityDescription( + entity_registry_enabled_default=False, + icon="mdi:download-network", + key=QSD_PORTS_STATISTICS, + name="RX", + native_unit_of_measurement=UnitOfInformation.BYTES, + qsw_type=QswEntityType.PORT, + state_class=SensorStateClass.TOTAL_INCREASING, + subkey=QSD_RX_OCTETS, + ), + QswSensorEntityDescription( + entity_registry_enabled_default=False, + icon="mdi:close-network", + key=QSD_PORTS_STATISTICS, + entity_category=EntityCategory.DIAGNOSTIC, + name="RX Errors", + qsw_type=QswEntityType.PORT, + state_class=SensorStateClass.TOTAL_INCREASING, + subkey=QSD_RX_ERRORS, + ), + QswSensorEntityDescription( + device_class=SensorDeviceClass.DATA_RATE, + entity_registry_enabled_default=False, + icon="mdi:download-network", + key=QSD_PORTS_STATISTICS, + name="RX Speed", + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + qsw_type=QswEntityType.PORT, + state_class=SensorStateClass.MEASUREMENT, + subkey=QSD_RX_SPEED, + ), + QswSensorEntityDescription( + entity_registry_enabled_default=False, + icon="mdi:upload-network", + key=QSD_PORTS_STATISTICS, + name="TX", + native_unit_of_measurement=UnitOfInformation.BYTES, + qsw_type=QswEntityType.PORT, + state_class=SensorStateClass.TOTAL_INCREASING, + subkey=QSD_TX_OCTETS, + ), + QswSensorEntityDescription( + device_class=SensorDeviceClass.DATA_RATE, + entity_registry_enabled_default=False, + icon="mdi:upload-network", + key=QSD_PORTS_STATISTICS, + name="TX Speed", + native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND, + qsw_type=QswEntityType.PORT, + state_class=SensorStateClass.MEASUREMENT, + subkey=QSD_TX_SPEED, + ), +) + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Add QNAP QSW sensors from a config_entry.""" coordinator: QswDataCoordinator = hass.data[DOMAIN][entry.entry_id][QSW_COORD_DATA] - async_add_entities( - QswSensor(coordinator, description, entry) - for description in SENSOR_TYPES + + entities: list[QswSensor] = [] + + for description in SENSOR_TYPES: if ( description.key in coordinator.data and description.subkey in coordinator.data[description.key] - ) - ) + ): + entities.append(QswSensor(coordinator, description, entry)) + + for description in LACP_PORT_SENSOR_TYPES: + if ( + description.key not in coordinator.data + or QSD_LACP_PORTS not in coordinator.data[description.key] + ): + continue + + for port_id, port_values in coordinator.data[description.key][ + QSD_LACP_PORTS + ].items(): + if description.subkey not in port_values: + continue + + _desc = replace( + description, + sep_key=f"_lacp_port_{port_id}_", + name=f"LACP Port {port_id} {description.name}", + ) + entities.append(QswSensor(coordinator, _desc, entry, port_id)) + + for description in PORT_SENSOR_TYPES: + if ( + description.key not in coordinator.data + or QSD_PORTS not in coordinator.data[description.key] + ): + continue + + for port_id, port_values in coordinator.data[description.key][ + QSD_PORTS + ].items(): + if description.subkey not in port_values: + continue + + _desc = replace( + description, + sep_key=f"_port_{port_id}_", + name=f"Port {port_id} {description.name}", + ) + entities.append(QswSensor(coordinator, _desc, entry, port_id)) + + async_add_entities(entities) class QswSensor(QswSensorEntity, SensorEntity): @@ -178,13 +358,13 @@ class QswSensor(QswSensorEntity, SensorEntity): coordinator: QswDataCoordinator, description: QswSensorEntityDescription, entry: ConfigEntry, + type_id: int | None = None, ) -> None: """Initialize.""" - super().__init__(coordinator, entry) + super().__init__(coordinator, entry, type_id) + self._attr_name = f"{self.product} {description.name}" - self._attr_unique_id = ( - f"{entry.unique_id}_{description.key}_{description.subkey}" - ) + self._attr_unique_id = f"{entry.unique_id}_{description.key}{description.sep_key}{description.subkey}" self.entity_description = description self._async_update_attrs() @@ -192,7 +372,9 @@ class QswSensor(QswSensorEntity, SensorEntity): def _async_update_attrs(self) -> None: """Update sensor attributes.""" value = self.get_device_value( - self.entity_description.key, self.entity_description.subkey + self.entity_description.key, + self.entity_description.subkey, + self.entity_description.qsw_type, ) self._attr_native_value = value super()._async_update_attrs() diff --git a/homeassistant/components/qnap_qsw/translations/lv.json b/homeassistant/components/qnap_qsw/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qnap_qsw/translations/uk.json b/homeassistant/components/qnap_qsw/translations/uk.json new file mode 100644 index 00000000000..b1fd850385b --- /dev/null +++ b/homeassistant/components/qnap_qsw/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "discovered_connection": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/qrcode/image_processing.py b/homeassistant/components/qrcode/image_processing.py index bc9bae421ed..06fe92e5b1d 100644 --- a/homeassistant/components/qrcode/image_processing.py +++ b/homeassistant/components/qrcode/image_processing.py @@ -20,7 +20,6 @@ def setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the QR code image processing platform.""" - # pylint: disable=unused-argument entities = [] for camera in config[CONF_SOURCE]: entities.append(QrEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME))) diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 1a394e17f29..5330f24419e 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -2,7 +2,7 @@ "domain": "qrcode", "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", - "requirements": ["pillow==9.3.0", "pyzbar==0.1.7"], + "requirements": ["pillow==9.4.0", "pyzbar==0.1.7"], "codeowners": [], "iot_class": "calculated", "loggers": ["pyzbar"] diff --git a/homeassistant/components/qwikswitch/sensor.py b/homeassistant/components/qwikswitch/sensor.py index 08ce2214b43..8a72ca7c811 100644 --- a/homeassistant/components/qwikswitch/sensor.py +++ b/homeassistant/components/qwikswitch/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from typing import Any from pyqwikswitch.qwikswitch import SENSORS @@ -34,7 +35,7 @@ async def async_setup_platform( class QSSensor(QSEntity, SensorEntity): """Sensor based on a Qwikswitch relay/dimmer module.""" - _val = None + _val: Any | None = None def __init__(self, sensor): """Initialize the sensor.""" @@ -68,7 +69,7 @@ class QSSensor(QSEntity, SensorEntity): @property def native_value(self): """Return the value of the sensor.""" - return str(self._val) + return None if self._val is None else str(self._val) @property def unique_id(self): diff --git a/homeassistant/components/rachio/translations/lv.json b/homeassistant/components/rachio/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/rachio/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radarr/coordinator.py b/homeassistant/components/radarr/coordinator.py index c2f8d7ce6ba..5537a18725c 100644 --- a/homeassistant/components/radarr/coordinator.py +++ b/homeassistant/components/radarr/coordinator.py @@ -3,7 +3,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from datetime import timedelta -from typing import Generic, TypeVar, Union, cast +from typing import Generic, TypeVar, cast from aiopyarr import Health, RadarrMovie, RootFolder, SystemStatus, exceptions from aiopyarr.models.host_configuration import PyArrHostConfiguration @@ -16,7 +16,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DOMAIN, LOGGER -T = TypeVar("T", bound=Union[SystemStatus, list[RootFolder], list[Health], int]) +T = TypeVar("T", bound=SystemStatus | list[RootFolder] | list[Health] | int) class RadarrDataUpdateCoordinator(DataUpdateCoordinator[T], Generic[T], ABC): diff --git a/homeassistant/components/radarr/translations/hu.json b/homeassistant/components/radarr/translations/hu.json index 56fa961d607..aed7dfcbffe 100644 --- a/homeassistant/components/radarr/translations/hu.json +++ b/homeassistant/components/radarr/translations/hu.json @@ -8,7 +8,7 @@ "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt", - "wrong_app": "Helytelen alkalmaz\u00e1s el\u00e9rve. K\u00e9rj\u00fck, pr\u00f3b\u00e1lja \u00fajra", + "wrong_app": "Helytelen alkalmaz\u00e1s el\u00e9rve. K\u00e9rem, pr\u00f3b\u00e1lja \u00fajra", "zeroconf_failed": "Az API-kulcs nem tal\u00e1lhat\u00f3. K\u00e9rem, adja meg" }, "step": { diff --git a/homeassistant/components/radarr/translations/tr.json b/homeassistant/components/radarr/translations/tr.json index 10452a355ce..1c0cf7db626 100644 --- a/homeassistant/components/radarr/translations/tr.json +++ b/homeassistant/components/radarr/translations/tr.json @@ -26,6 +26,12 @@ } } }, + "issues": { + "removed_yaml": { + "description": "Radarr'\u0131 YAML kullanarak yap\u0131land\u0131rma kald\u0131r\u0131ld\u0131. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lm\u0131yor. \n\n YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu \u00e7\u00f6zmek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Radarr YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131ld\u0131" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/radiotherm/__init__.py b/homeassistant/components/radiotherm/__init__.py index 787570eaeb4..808ee56b092 100644 --- a/homeassistant/components/radiotherm/__init__.py +++ b/homeassistant/components/radiotherm/__init__.py @@ -32,12 +32,12 @@ async def _async_call_or_raise_not_ready( except RadiothermTstatError as ex: msg = f"{host} was busy (invalid value returned): {ex}" raise ConfigEntryNotReady(msg) from ex - except (OSError, URLError) as ex: - msg = f"{host} connection error: {ex}" - raise ConfigEntryNotReady(msg) from ex except timeout as ex: msg = f"{host} timed out waiting for a response: {ex}" raise ConfigEntryNotReady(msg) from ex + except (OSError, URLError) as ex: + msg = f"{host} connection error: {ex}" + raise ConfigEntryNotReady(msg) from ex async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/radiotherm/coordinator.py b/homeassistant/components/radiotherm/coordinator.py index 91acdee8710..ffc6bfcc8ba 100644 --- a/homeassistant/components/radiotherm/coordinator.py +++ b/homeassistant/components/radiotherm/coordinator.py @@ -39,9 +39,9 @@ class RadioThermUpdateCoordinator(DataUpdateCoordinator[RadioThermUpdate]): except RadiothermTstatError as ex: msg = f"{self._description} was busy (invalid value returned): {ex}" raise UpdateFailed(msg) from ex - except (OSError, URLError) as ex: - msg = f"{self._description} connection error: {ex}" - raise UpdateFailed(msg) from ex except timeout as ex: msg = f"{self._description}) timed out waiting for a response: {ex}" raise UpdateFailed(msg) from ex + except (OSError, URLError) as ex: + msg = f"{self._description} connection error: {ex}" + raise UpdateFailed(msg) from ex diff --git a/homeassistant/components/radiotherm/translations/lv.json b/homeassistant/components/radiotherm/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/radiotherm/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/translations/tr.json b/homeassistant/components/radiotherm/translations/tr.json index 99e57c1bf6b..bfdc4d4b898 100644 --- a/homeassistant/components/radiotherm/translations/tr.json +++ b/homeassistant/components/radiotherm/translations/tr.json @@ -10,7 +10,7 @@ "flow_title": "{name} {model} ({host})", "step": { "confirm": { - "description": "{name} {model} ( {host} ) kurulumu yapmak istiyor musunuz?" + "description": "{name} {model} ( {host} ) kurmak istiyor musunuz?" }, "user": { "data": { diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index 1e80cfb1cbc..7b41a3f2f5e 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -1,16 +1,12 @@ """Support for Rain Bird Irrigation system LNK WiFi Module.""" from __future__ import annotations -import asyncio import logging -from pyrainbird.async_client import ( - AsyncRainbirdClient, - AsyncRainbirdController, - RainbirdApiException, -) +from pyrainbird.async_client import AsyncRainbirdClient, AsyncRainbirdController import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryState from homeassistant.const import ( CONF_FRIENDLY_NAME, CONF_HOST, @@ -18,21 +14,18 @@ from homeassistant.const import ( CONF_TRIGGER_TIME, Platform, ) -from homeassistant.core import HomeAssistant -from homeassistant.helpers import discovery +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType -from .const import ( - CONF_ZONES, - RAINBIRD_CONTROLLER, - SENSOR_TYPE_RAINDELAY, - SENSOR_TYPE_RAINSENSOR, -) +from .const import ATTR_CONFIG_ENTRY_ID, ATTR_DURATION, CONF_SERIAL_NUMBER, CONF_ZONES from .coordinator import RainbirdUpdateCoordinator -PLATFORMS = [Platform.SWITCH, Platform.SENSOR, Platform.BINARY_SENSOR] +PLATFORMS = [Platform.SWITCH, Platform.SENSOR, Platform.BINARY_SENSOR, Platform.NUMBER] _LOGGER = logging.getLogger(__name__) @@ -61,47 +54,121 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +SERVICE_SET_RAIN_DELAY = "set_rain_delay" +SERVICE_SCHEMA_RAIN_DELAY = vol.All( + vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY_ID): cv.string, + vol.Required(ATTR_DURATION): cv.positive_float, + } + ), +) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Rain Bird component.""" - return all( - await asyncio.gather( - *[ - _setup_controller(hass, controller_config, config) - for controller_config in config[DOMAIN] - ] - ) - ) + if DOMAIN not in config: + return True - -async def _setup_controller(hass, controller_config, config): - """Set up a controller.""" - server = controller_config[CONF_HOST] - password = controller_config[CONF_PASSWORD] - client = AsyncRainbirdClient(async_get_clientsession(hass), server, password) - controller = AsyncRainbirdController(client) - try: - await controller.get_serial_number() - except RainbirdApiException as exc: - _LOGGER.error("Unable to setup controller: %s", exc) - return False - - rain_coordinator = RainbirdUpdateCoordinator(hass, controller.get_rain_sensor_state) - delay_coordinator = RainbirdUpdateCoordinator(hass, controller.get_rain_delay) - - for platform in PLATFORMS: + for controller_config in config[DOMAIN]: hass.async_create_task( - discovery.async_load_platform( - hass, - platform, + hass.config_entries.flow.async_init( DOMAIN, - { - RAINBIRD_CONTROLLER: controller, - SENSOR_TYPE_RAINSENSOR: rain_coordinator, - SENSOR_TYPE_RAINDELAY: delay_coordinator, - **controller_config, - }, - config, + context={"source": SOURCE_IMPORT}, + data=controller_config, ) ) + + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.4.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the config entry for Rain Bird.""" + + hass.data.setdefault(DOMAIN, {}) + + controller = AsyncRainbirdController( + AsyncRainbirdClient( + async_get_clientsession(hass), + entry.data[CONF_HOST], + entry.data[CONF_PASSWORD], + ) + ) + coordinator = RainbirdUpdateCoordinator( + hass, + name=entry.title, + controller=controller, + serial_number=entry.data[CONF_SERIAL_NUMBER], + ) + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + async def set_rain_delay(call: ServiceCall) -> None: + """Service call to delay automatic irrigigation.""" + + entry_id = call.data[ATTR_CONFIG_ENTRY_ID] + duration = call.data[ATTR_DURATION] + if entry_id not in hass.data[DOMAIN]: + raise HomeAssistantError(f"Config entry id does not exist: {entry_id}") + coordinator = hass.data[DOMAIN][entry_id] + + entity_registry = er.async_get(hass) + entity_ids = ( + entry.entity_id + for entry in er.async_entries_for_config_entry(entity_registry, entry_id) + if entry.unique_id == f"{coordinator.serial_number}-rain-delay" + ) + async_create_issue( + hass, + DOMAIN, + "deprecated_raindelay", + breaks_in_ha_version="2023.4.0", + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.WARNING, + translation_key="deprecated_raindelay", + translation_placeholders={ + "alternate_target": next(entity_ids, "unknown"), + }, + ) + + await coordinator.controller.set_rain_delay(duration) + + hass.services.async_register( + DOMAIN, + SERVICE_SET_RAIN_DELAY, + set_rain_delay, + schema=SERVICE_SCHEMA_RAIN_DELAY, + ) + + 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) + + loaded_entries = [ + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + if len(loaded_entries) == 1: + hass.services.async_remove(DOMAIN, SERVICE_SET_RAIN_DELAY) + + return unload_ok diff --git a/homeassistant/components/rainbird/binary_sensor.py b/homeassistant/components/rainbird/binary_sensor.py index 02ea8b21bb1..ee5be0e4617 100644 --- a/homeassistant/components/rainbird/binary_sensor.py +++ b/homeassistant/components/rainbird/binary_sensor.py @@ -2,71 +2,54 @@ from __future__ import annotations import logging -from typing import Union from homeassistant.components.binary_sensor import ( BinarySensorEntity, BinarySensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import SENSOR_TYPE_RAINDELAY, SENSOR_TYPE_RAINSENSOR +from .const import DOMAIN from .coordinator import RainbirdUpdateCoordinator _LOGGER = logging.getLogger(__name__) -BINARY_SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = ( - BinarySensorEntityDescription( - key=SENSOR_TYPE_RAINSENSOR, - name="Rainsensor", - icon="mdi:water", - ), - BinarySensorEntityDescription( - key=SENSOR_TYPE_RAINDELAY, - name="Raindelay", - icon="mdi:water-off", - ), +RAIN_SENSOR_ENTITY_DESCRIPTION = BinarySensorEntityDescription( + key="rainsensor", + name="Rainsensor", + icon="mdi:water", ) -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up a Rain Bird sensor.""" - if discovery_info is None: - return - - async_add_entities( - [ - RainBirdSensor(discovery_info[description.key], description) - for description in BINARY_SENSOR_TYPES - ], - True, - ) + """Set up entry for a Rain Bird binary_sensor.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities([RainBirdSensor(coordinator, RAIN_SENSOR_ENTITY_DESCRIPTION)]) -class RainBirdSensor( - CoordinatorEntity[RainbirdUpdateCoordinator[Union[int, bool]]], BinarySensorEntity -): +class RainBirdSensor(CoordinatorEntity[RainbirdUpdateCoordinator], BinarySensorEntity): """A sensor implementation for Rain Bird device.""" def __init__( self, - coordinator: RainbirdUpdateCoordinator[int | bool], + coordinator: RainbirdUpdateCoordinator, description: BinarySensorEntityDescription, ) -> None: """Initialize the Rain Bird sensor.""" super().__init__(coordinator) self.entity_description = description + self._attr_unique_id = f"{coordinator.serial_number}-{description.key}" + self._attr_device_info = coordinator.device_info @property def is_on(self) -> bool | None: """Return True if entity is on.""" - return None if self.coordinator.data is None else bool(self.coordinator.data) + return self.coordinator.data.rain diff --git a/homeassistant/components/rainbird/config_flow.py b/homeassistant/components/rainbird/config_flow.py new file mode 100644 index 00000000000..057fc6fe396 --- /dev/null +++ b/homeassistant/components/rainbird/config_flow.py @@ -0,0 +1,194 @@ +"""Config flow for Rain Bird.""" + +from __future__ import annotations + +import asyncio +import logging +from typing import Any + +import async_timeout +from pyrainbird.async_client import ( + AsyncRainbirdClient, + AsyncRainbirdController, + RainbirdApiException, +) +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_FRIENDLY_NAME, CONF_HOST, CONF_PASSWORD +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv, selector +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ( + ATTR_DURATION, + CONF_IMPORTED_NAMES, + CONF_SERIAL_NUMBER, + CONF_ZONES, + DEFAULT_TRIGGER_TIME_MINUTES, + DOMAIN, + TIMEOUT_SECONDS, +) + +_LOGGER = logging.getLogger(__name__) + + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): selector.TextSelector(), + vol.Required(CONF_PASSWORD): selector.TextSelector( + selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD) + ), + } +) + + +class ConfigFlowError(Exception): + """Error raised during a config flow.""" + + def __init__(self, message: str, error_code: str) -> None: + """Initialize ConfigFlowError.""" + super().__init__(message) + self.error_code = error_code + + +class RainbirdConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Rain Bird.""" + + @staticmethod + @callback + def async_get_options_flow( + config_entry: ConfigEntry, + ) -> RainBirdOptionsFlowHandler: + """Define the config flow to handle options.""" + return RainBirdOptionsFlowHandler(config_entry) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Configure the Rain Bird device.""" + error_code: str | None = None + if user_input: + try: + serial_number = await self._test_connection( + user_input[CONF_HOST], user_input[CONF_PASSWORD] + ) + except ConfigFlowError as err: + _LOGGER.error("Error during config flow: %s", err) + error_code = err.error_code + else: + return await self.async_finish( + serial_number, + data={ + CONF_HOST: user_input[CONF_HOST], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_SERIAL_NUMBER: serial_number, + }, + options={ATTR_DURATION: DEFAULT_TRIGGER_TIME_MINUTES}, + ) + + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors={"base": error_code} if error_code else None, + ) + + async def _test_connection(self, host: str, password: str) -> str: + """Test the connection and return the device serial number. + + Raises a ConfigFlowError on failure. + """ + controller = AsyncRainbirdController( + AsyncRainbirdClient( + async_get_clientsession(self.hass), + host, + password, + ) + ) + try: + async with async_timeout.timeout(TIMEOUT_SECONDS): + return await controller.get_serial_number() + except asyncio.TimeoutError as err: + raise ConfigFlowError( + f"Timeout connecting to Rain Bird controller: {str(err)}", + "timeout_connect", + ) from err + except RainbirdApiException as err: + raise ConfigFlowError( + f"Error connecting to Rain Bird controller: {str(err)}", + "cannot_connect", + ) from err + + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Import a config entry from configuration.yaml.""" + self._async_abort_entries_match({CONF_HOST: config[CONF_HOST]}) + try: + serial_number = await self._test_connection( + config[CONF_HOST], config[CONF_PASSWORD] + ) + except ConfigFlowError as err: + _LOGGER.error("Error during config import: %s", err) + return self.async_abort(reason=err.error_code) + + data = { + CONF_HOST: config[CONF_HOST], + CONF_PASSWORD: config[CONF_PASSWORD], + CONF_SERIAL_NUMBER: serial_number, + } + names: dict[str, str] = {} + for (zone, zone_config) in config.get(CONF_ZONES, {}).items(): + if name := zone_config.get(CONF_FRIENDLY_NAME): + names[str(zone)] = name + if names: + data[CONF_IMPORTED_NAMES] = names + return await self.async_finish( + serial_number, + data=data, + options={ + ATTR_DURATION: config.get(ATTR_DURATION, DEFAULT_TRIGGER_TIME_MINUTES), + }, + ) + + async def async_finish( + self, + serial_number: str, + data: dict[str, Any], + options: dict[str, Any], + ) -> FlowResult: + """Create the config entry.""" + await self.async_set_unique_id(serial_number) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=data[CONF_HOST], + data=data, + options=options, + ) + + +class RainBirdOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a RainBird options flow.""" + + def __init__(self, config_entry: ConfigEntry) -> None: + """Initialize RainBirdOptionsFlowHandler.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + ATTR_DURATION, + default=self.config_entry.options[ATTR_DURATION], + ): cv.positive_int, + } + ), + ) diff --git a/homeassistant/components/rainbird/const.py b/homeassistant/components/rainbird/const.py index be06fdb8224..162e3a16b6c 100644 --- a/homeassistant/components/rainbird/const.py +++ b/homeassistant/components/rainbird/const.py @@ -1,10 +1,14 @@ """Constants for rainbird.""" DOMAIN = "rainbird" - -SENSOR_TYPE_RAINDELAY = "raindelay" -SENSOR_TYPE_RAINSENSOR = "rainsensor" - -RAINBIRD_CONTROLLER = "controller" +MANUFACTURER = "Rain Bird" +DEFAULT_TRIGGER_TIME_MINUTES = 6 CONF_ZONES = "zones" +CONF_SERIAL_NUMBER = "serial_number" +CONF_IMPORTED_NAMES = "imported_names" + +ATTR_DURATION = "duration" +ATTR_CONFIG_ENTRY_ID = "config_entry_id" + +TIMEOUT_SECONDS = 20 diff --git a/homeassistant/components/rainbird/coordinator.py b/homeassistant/components/rainbird/coordinator.py index ee6857fe93c..ddb2b70324d 100644 --- a/homeassistant/components/rainbird/coordinator.py +++ b/homeassistant/components/rainbird/coordinator.py @@ -2,18 +2,21 @@ from __future__ import annotations -from collections.abc import Awaitable, Callable +import asyncio +from dataclasses import dataclass import datetime import logging from typing import TypeVar import async_timeout -from pyrainbird.async_client import RainbirdApiException +from pyrainbird.async_client import AsyncRainbirdController, RainbirdApiException from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -TIMEOUT_SECONDS = 20 +from .const import DOMAIN, MANUFACTURER, TIMEOUT_SECONDS + UPDATE_INTERVAL = datetime.timedelta(minutes=1) _LOGGER = logging.getLogger(__name__) @@ -21,27 +24,87 @@ _LOGGER = logging.getLogger(__name__) _T = TypeVar("_T") -class RainbirdUpdateCoordinator(DataUpdateCoordinator[_T]): +@dataclass +class RainbirdDeviceState: + """Data retrieved from a Rain Bird device.""" + + zones: set[int] + active_zones: set[int] + rain: bool + rain_delay: int + + +class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]): """Coordinator for rainbird API calls.""" def __init__( self, hass: HomeAssistant, - update_method: Callable[[], Awaitable[_T]], + name: str, + controller: AsyncRainbirdController, + serial_number: str, ) -> None: """Initialize ZoneStateUpdateCoordinator.""" super().__init__( hass, _LOGGER, - name="Rainbird Zones", - update_method=update_method, + name=name, + update_method=self._async_update_data, update_interval=UPDATE_INTERVAL, ) + self._controller = controller + self._serial_number = serial_number + self._zones: set[int] | None = None - async def _async_update_data(self) -> _T: - """Fetch data from API endpoint.""" + @property + def controller(self) -> AsyncRainbirdController: + """Return the API client for the device.""" + return self._controller + + @property + def serial_number(self) -> str: + """Return the device serial number.""" + return self._serial_number + + @property + def device_info(self) -> DeviceInfo: + """Return information about the device.""" + return DeviceInfo( + default_name=f"{MANUFACTURER} Controller", + identifiers={(DOMAIN, self._serial_number)}, + manufacturer=MANUFACTURER, + ) + + async def _async_update_data(self) -> RainbirdDeviceState: + """Fetch data from Rain Bird device.""" try: async with async_timeout.timeout(TIMEOUT_SECONDS): - return await self.update_method() # type: ignore[misc] + return await self._fetch_data() except RainbirdApiException as err: - raise UpdateFailed(f"Error communicating with API: {err}") from err + raise UpdateFailed(f"Error communicating with Device: {err}") from err + + async def _fetch_data(self) -> RainbirdDeviceState: + """Fetch data from the Rain Bird device.""" + (zones, states, rain, rain_delay) = await asyncio.gather( + self._fetch_zones(), + self._controller.get_zone_states(), + self._controller.get_rain_sensor_state(), + self._controller.get_rain_delay(), + ) + return RainbirdDeviceState( + zones=set(zones), + active_zones={zone for zone in zones if states.active(zone)}, + rain=rain, + rain_delay=rain_delay, + ) + + async def _fetch_zones(self) -> set[int]: + """Fetch the zones from the device, caching the results.""" + if self._zones is None: + available_stations = await self._controller.get_available_stations() + self._zones = { + zone + for zone in range(1, available_stations.stations.count + 1) + if available_stations.stations.active(zone) + } + return self._zones diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index a7366fac4b5..50eb11c3fe9 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -1,8 +1,9 @@ { "domain": "rainbird", "name": "Rain Bird", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainbird", - "requirements": ["pyrainbird==0.7.1"], + "requirements": ["pyrainbird==1.1.0"], "codeowners": ["@konikvranik", "@allenporter"], "iot_class": "local_polling", "loggers": ["pyrainbird"] diff --git a/homeassistant/components/rainbird/number.py b/homeassistant/components/rainbird/number.py new file mode 100644 index 00000000000..ac1ea961870 --- /dev/null +++ b/homeassistant/components/rainbird/number.py @@ -0,0 +1,61 @@ +"""The number platform for rainbird.""" +from __future__ import annotations + +import logging + +from homeassistant.components.number import NumberEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfTime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import RainbirdUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up entry for a Rain Bird number platform.""" + async_add_entities( + [ + RainDelayNumber( + hass.data[DOMAIN][config_entry.entry_id], + ) + ] + ) + + +class RainDelayNumber(CoordinatorEntity[RainbirdUpdateCoordinator], NumberEntity): + """A number implemnetaiton for the rain delay.""" + + _attr_native_min_value = 0 + _attr_native_max_value = 14 + _attr_native_step = 1 + _attr_native_unit_of_measurement = UnitOfTime.DAYS + _attr_icon = "mdi:water-off" + _attr_name = "Rain delay" + _attr_has_entity_name = True + + def __init__( + self, + coordinator: RainbirdUpdateCoordinator, + ) -> None: + """Initialize the Rain Bird sensor.""" + super().__init__(coordinator) + self._attr_unique_id = f"{coordinator.serial_number}-rain-delay" + self._attr_device_info = coordinator.device_info + + @property + def native_value(self) -> float | None: + """Return the value reported by the sensor.""" + return self.coordinator.data.rain_delay + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + await self.coordinator.controller.set_rain_delay(value) diff --git a/homeassistant/components/rainbird/sensor.py b/homeassistant/components/rainbird/sensor.py index e1dd56d1fb3..de74943baf9 100644 --- a/homeassistant/components/rainbird/sensor.py +++ b/homeassistant/components/rainbird/sensor.py @@ -2,69 +2,58 @@ from __future__ import annotations import logging -from typing import Union 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 homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import SENSOR_TYPE_RAINDELAY, SENSOR_TYPE_RAINSENSOR +from .const import DOMAIN from .coordinator import RainbirdUpdateCoordinator _LOGGER = logging.getLogger(__name__) -SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( - SensorEntityDescription( - key=SENSOR_TYPE_RAINSENSOR, - name="Rainsensor", - icon="mdi:water", - ), - SensorEntityDescription( - key=SENSOR_TYPE_RAINDELAY, - name="Raindelay", - icon="mdi:water-off", - ), +RAIN_DELAY_ENTITY_DESCRIPTION = SensorEntityDescription( + key="raindelay", + name="Raindelay", + icon="mdi:water-off", ) -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up a Rain Bird sensor.""" - - if discovery_info is None: - return - + """Set up entry for a Rain Bird sensor.""" async_add_entities( [ - RainBirdSensor(discovery_info[description.key], description) - for description in SENSOR_TYPES - ], - True, + RainBirdSensor( + hass.data[DOMAIN][config_entry.entry_id], + RAIN_DELAY_ENTITY_DESCRIPTION, + ) + ] ) -class RainBirdSensor( - CoordinatorEntity[RainbirdUpdateCoordinator[Union[int, bool]]], SensorEntity -): +class RainBirdSensor(CoordinatorEntity[RainbirdUpdateCoordinator], SensorEntity): """A sensor implementation for Rain Bird device.""" def __init__( self, - coordinator: RainbirdUpdateCoordinator[int | bool], + coordinator: RainbirdUpdateCoordinator, description: SensorEntityDescription, ) -> None: """Initialize the Rain Bird sensor.""" super().__init__(coordinator) self.entity_description = description + self._attr_unique_id = f"{coordinator.serial_number}-{description.key}" + self._attr_device_info = coordinator.device_info @property def native_value(self) -> StateType: """Return the value reported by the sensor.""" - return self.coordinator.data + return self.coordinator.data.rain_delay diff --git a/homeassistant/components/rainbird/services.yaml b/homeassistant/components/rainbird/services.yaml index 3d5f55dba14..34f89ec279b 100644 --- a/homeassistant/components/rainbird/services.yaml +++ b/homeassistant/components/rainbird/services.yaml @@ -1,15 +1,11 @@ start_irrigation: name: Start irrigation description: Start the irrigation + target: + entity: + integration: rainbird + domain: switch fields: - entity_id: - name: Entity - description: Name of a single irrigation to turn on - required: true - selector: - entity: - integration: rainbird - domain: switch duration: name: Duration description: Duration for this sprinkler to be turned on @@ -23,6 +19,13 @@ set_rain_delay: name: Set rain delay description: Set how long automatic irrigation is turned off. fields: + config_entry_id: + name: Rainbird Controller Configuration Entry + description: The setting will be adjusted on the specified controller + required: true + selector: + config_entry: + integration: rainbird duration: name: Duration description: Duration for this system to be turned off. diff --git a/homeassistant/components/rainbird/strings.json b/homeassistant/components/rainbird/strings.json new file mode 100644 index 00000000000..f950146f160 --- /dev/null +++ b/homeassistant/components/rainbird/strings.json @@ -0,0 +1,48 @@ +{ + "config": { + "step": { + "user": { + "title": "Configure Rain Bird", + "description": "Please enter the LNK WiFi module information for your Rain Bird device.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]" + } + }, + "options": { + "step": { + "init": { + "title": "Configure Rain Bird", + "data": { + "duration": "Default irrigation time in minutes" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "The Rain Bird YAML configuration is being removed", + "description": "Configuring Rain Bird in configuration.yaml is being removed in Home Assistant 2023.4.\n\nYour configuration has been imported into the UI automatically, however default per-zone irrigation times are no longer supported. Remove the Rain Bird YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + }, + "deprecated_raindelay": { + "title": "The Rain Bird Rain Delay Service is being removed", + "fix_flow": { + "step": { + "confirm": { + "title": "The Rain Bird Rain Delay Service is being removed", + "description": "The Rain Bird service `rainbird.set_rain_delay` is being removed and replaced by a Number entity for managing the rain delay. Any existing automations or scripts will need to be updated to use `number.set_value` with a target of `{alternate_target}` instead." + } + } + } + } + } +} diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 5a9edee2753..38f3c03fb03 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -3,162 +3,100 @@ from __future__ import annotations import logging -from pyrainbird import AvailableStations -from pyrainbird.async_client import AsyncRainbirdController, RainbirdApiException -from pyrainbird.data import States import voluptuous as vol from homeassistant.components.switch import SwitchEntity -from homeassistant.const import ATTR_ENTITY_ID, CONF_FRIENDLY_NAME, CONF_TRIGGER_TIME -from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ConfigEntryNotReady, PlatformNotReady -from homeassistant.helpers import config_validation as cv +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_platform +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 .const import CONF_ZONES, DOMAIN, RAINBIRD_CONTROLLER +from .const import ATTR_DURATION, CONF_IMPORTED_NAMES, DOMAIN, MANUFACTURER from .coordinator import RainbirdUpdateCoordinator _LOGGER = logging.getLogger(__name__) -ATTR_DURATION = "duration" - SERVICE_START_IRRIGATION = "start_irrigation" -SERVICE_SET_RAIN_DELAY = "set_rain_delay" -SERVICE_SCHEMA_IRRIGATION = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_DURATION): cv.positive_float, - } -) - -SERVICE_SCHEMA_RAIN_DELAY = vol.Schema( - { - vol.Required(ATTR_DURATION): cv.positive_float, - } -) +SERVICE_SCHEMA_IRRIGATION = { + vol.Required(ATTR_DURATION): cv.positive_float, +} -async def async_setup_platform( +async def async_setup_entry( hass: HomeAssistant, - config: ConfigType, + config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up Rain Bird switches over a Rain Bird controller.""" - - if discovery_info is None: - return - - controller: AsyncRainbirdController = discovery_info[RAINBIRD_CONTROLLER] - try: - available_stations: AvailableStations = ( - await controller.get_available_stations() + """Set up entry for a Rain Bird irrigation switches.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + RainBirdSwitch( + coordinator, + zone, + config_entry.options[ATTR_DURATION], + config_entry.data.get(CONF_IMPORTED_NAMES, {}).get(str(zone)), ) - except RainbirdApiException as err: - raise PlatformNotReady(f"Failed to get stations: {str(err)}") from err - if not (available_stations and available_stations.stations): - return - coordinator = RainbirdUpdateCoordinator(hass, controller.get_zone_states) - devices = [] - for zone in range(1, available_stations.stations.count + 1): - if available_stations.stations.active(zone): - zone_config = discovery_info.get(CONF_ZONES, {}).get(zone, {}) - time = zone_config.get(CONF_TRIGGER_TIME, discovery_info[CONF_TRIGGER_TIME]) - name = zone_config.get(CONF_FRIENDLY_NAME) - devices.append( - RainBirdSwitch( - coordinator, - controller, - zone, - time, - name if name else f"Sprinkler {zone}", - ) - ) + for zone in coordinator.data.zones + ) - try: - await coordinator.async_config_entry_first_refresh() - except ConfigEntryNotReady as err: - raise PlatformNotReady(f"Failed to load zone state: {str(err)}") from err - - async_add_entities(devices) - - async def start_irrigation(service: ServiceCall) -> None: - entity_id = service.data[ATTR_ENTITY_ID] - duration = service.data[ATTR_DURATION] - - for device in devices: - if device.entity_id == entity_id: - await device.async_turn_on(duration=duration) - - hass.services.async_register( - DOMAIN, + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( SERVICE_START_IRRIGATION, - start_irrigation, - schema=SERVICE_SCHEMA_IRRIGATION, - ) - - async def set_rain_delay(service: ServiceCall) -> None: - duration = service.data[ATTR_DURATION] - - await controller.set_rain_delay(duration) - - hass.services.async_register( - DOMAIN, - SERVICE_SET_RAIN_DELAY, - set_rain_delay, - schema=SERVICE_SCHEMA_RAIN_DELAY, + SERVICE_SCHEMA_IRRIGATION, + "async_turn_on", ) -class RainBirdSwitch( - CoordinatorEntity[RainbirdUpdateCoordinator[States]], SwitchEntity -): +class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity): """Representation of a Rain Bird switch.""" def __init__( self, - coordinator: RainbirdUpdateCoordinator[States], - rainbird: AsyncRainbirdController, + coordinator: RainbirdUpdateCoordinator, zone: int, - time: int, - name: str, + duration_minutes: int, + imported_name: str | None, ) -> None: """Initialize a Rain Bird Switch Device.""" super().__init__(coordinator) - self._rainbird = rainbird self._zone = zone - self._name = name + if imported_name: + self._attr_name = imported_name + self._attr_has_entity_name = False + else: + self._attr_has_entity_name = True self._state = None - self._duration = time - self._attributes = {ATTR_DURATION: self._duration, "zone": self._zone} + self._duration_minutes = duration_minutes + self._attr_unique_id = f"{coordinator.serial_number}-{zone}" + self._attr_device_info = DeviceInfo( + default_name=f"{MANUFACTURER} Sprinkler {zone}", + identifiers={(DOMAIN, self._attr_unique_id)}, + manufacturer=MANUFACTURER, + via_device=(DOMAIN, coordinator.serial_number), + ) @property def extra_state_attributes(self): """Return state attributes.""" - return self._attributes - - @property - def name(self): - """Get the name of the switch.""" - return self._name + return {"zone": self._zone} async def async_turn_on(self, **kwargs): """Turn the switch on.""" - await self._rainbird.irrigate_zone( + await self.coordinator.controller.irrigate_zone( int(self._zone), - int(kwargs[ATTR_DURATION] if ATTR_DURATION in kwargs else self._duration), + int(kwargs.get(ATTR_DURATION, self._duration_minutes)), ) await self.coordinator.async_request_refresh() async def async_turn_off(self, **kwargs): """Turn the switch off.""" - await self._rainbird.stop_irrigation() + await self.coordinator.controller.stop_irrigation() await self.coordinator.async_request_refresh() @property def is_on(self): """Return true if switch is on.""" - return self.coordinator.data.active(self._zone) + return self._zone in self.coordinator.data.active_zones diff --git a/homeassistant/components/rainbird/translations/bg.json b/homeassistant/components/rainbird/translations/bg.json new file mode 100644 index 00000000000..6192059ddcd --- /dev/null +++ b/homeassistant/components/rainbird/translations/bg.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/ca.json b/homeassistant/components/rainbird/translations/ca.json new file mode 100644 index 00000000000..ad85329eb3b --- /dev/null +++ b/homeassistant/components/rainbird/translations/ca.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "timeout_connect": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya" + }, + "description": "Introdueix la informaci\u00f3 del m\u00f2dul WiFi LNK del teu dispositiu Rain Bird.", + "title": "Configuraci\u00f3 de Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3 de Rain Bird des del fitxer configuration.yaml s'eliminar\u00e0 de Home Assistant a la versi\u00f3 2023.4. \n\nLa configuraci\u00f3 existent s'ha importat autom\u00e0ticament a la interf\u00edcie d'usuari. Per\u00f2 els temps de reg per zona predeterminats ja no s\u00f3n compatibles. Elimina la configuraci\u00f3 YAML de Rain Bird corresponent del fitxer configuration.yaml i reinicia Home Assistant per solucionar aquest problema.", + "title": "La configuraci\u00f3 YAML de Radin Bird est\u00e0 sent eliminada" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Temps de reg predeterminat en minuts" + }, + "title": "Configuraci\u00f3 de Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/de.json b/homeassistant/components/rainbird/translations/de.json new file mode 100644 index 00000000000..8922195ac71 --- /dev/null +++ b/homeassistant/components/rainbird/translations/de.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "timeout_connect": "Zeit\u00fcberschreitung beim Verbindungsaufbau" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort" + }, + "description": "Bitte gib die LNK-WLAN-Modulinformationen f\u00fcr dein Rain Bird-Ger\u00e4t ein.", + "title": "Rain Bird konfigurieren" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Die Konfiguration von Rain Bird in configuration.yaml wird in Home Assistant 2023.4 entfernt. \n\nDeine Konfiguration wurde automatisch in die Benutzeroberfl\u00e4che importiert, jedoch werden standardm\u00e4\u00dfige Beregnungszeiten pro Zone nicht mehr unterst\u00fctzt. Entferne die Rain Bird YAML-Konfiguration aus deiner configuration.yaml-Datei und starte Home Assistant neu, um dieses Problem zu beheben.", + "title": "Die Rain Bird YAML-Konfiguration wird entfernt" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Standard-Bew\u00e4sserungszeit in Minuten" + }, + "title": "Rain Bird konfigurieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/el.json b/homeassistant/components/rainbird/translations/el.json new file mode 100644 index 00000000000..32816ebb1ca --- /dev/null +++ b/homeassistant/components/rainbird/translations/el.json @@ -0,0 +1,45 @@ +{ + "config": { + "error": { + "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", + "timeout_connect": "\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" + }, + "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" + }, + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 \u03c4\u03b7\u03c2 \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1\u03c2 LNK WiFi \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03c3\u03b1\u03c2 Rain Bird.", + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Rain Bird" + } + } + }, + "issues": { + "deprecated_raindelay": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 Rain Bird `rainbird.set_rain_delay` \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03b1\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03b8\u03af\u03c3\u03c4\u03b1\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03bc\u03b9\u03b1 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 Number \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03c7\u03b5\u03af\u03c1\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03ba\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7\u03c2 \u03b2\u03c1\u03bf\u03c7\u03ae\u03c2. \u03a4\u03c5\u03c7\u03cc\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03c3\u03b5\u03c2 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03b9\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b7\u03bc\u03b5\u03c1\u03c9\u03b8\u03bf\u03cd\u03bd \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 `number.set_value` \u03bc\u03b5 \u03c3\u03c4\u03cc\u03c7\u03bf `{alternate_target}`.", + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 Rain Bird Rain Delay \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + } + }, + "title": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 Rain Bird Rain Delay \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + }, + "deprecated_yaml": { + "description": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 Rain Bird \u03c3\u03c4\u03bf configuration.yaml \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant 2023.4. \n\n \u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03ae \u03c3\u03b1\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b1 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03c0\u03b1\u03c6\u03ae \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7, \u03c9\u03c3\u03c4\u03cc\u03c3\u03bf \u03bf\u03b9 \u03c0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03b9 \u03c7\u03c1\u03cc\u03bd\u03bf\u03b9 \u03ac\u03c1\u03b4\u03b5\u03c5\u03c3\u03b7\u03c2 \u03b1\u03bd\u03ac \u03b6\u03ce\u03bd\u03b7 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd. \u039a\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Rain Bird YAML \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf configuration.yaml \u03ba\u03b1\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03ba\u03ba\u03b9\u03bd\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf Home Assistant \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b9\u03bf\u03c1\u03b8\u03ce\u03c3\u03b5\u03c4\u03b5 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.", + "title": "\u0397 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Rain Bird YAML \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "\u03a0\u03c1\u03bf\u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03bf\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03ac\u03c1\u03b4\u03b5\u03c5\u03c3\u03b7\u03c2 \u03c3\u03b5 \u03bb\u03b5\u03c0\u03c4\u03ac" + }, + "title": "\u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/en.json b/homeassistant/components/rainbird/translations/en.json new file mode 100644 index 00000000000..86fafc8b771 --- /dev/null +++ b/homeassistant/components/rainbird/translations/en.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "timeout_connect": "Timeout establishing connection" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password" + }, + "description": "Please enter the LNK WiFi module information for your Rain Bird device.", + "title": "Configure Rain Bird" + } + } + }, + "issues": { + "deprecated_raindelay": { + "fix_flow": { + "step": { + "confirm": { + "description": "The Rain Bird service `rainbird.set_rain_delay` is being removed and replaced by a Number entity for managing the rain delay. Any existing automations or scripts will need to be updated to use `number.set_value` with a target of `{alternate_target}` instead.", + "title": "The Rain Bird Rain Delay Service is being removed" + } + } + }, + "title": "The Rain Bird Rain Delay Service is being removed" + }, + "deprecated_yaml": { + "description": "Configuring Rain Bird in configuration.yaml is being removed in Home Assistant 2023.4.\n\nYour configuration has been imported into the UI automatically, however default per-zone irrigation times are no longer supported. Remove the Rain Bird YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue.", + "title": "The Rain Bird YAML configuration is being removed" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Default irrigation time in minutes" + }, + "title": "Configure Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/es.json b/homeassistant/components/rainbird/translations/es.json new file mode 100644 index 00000000000..78ef494451a --- /dev/null +++ b/homeassistant/components/rainbird/translations/es.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar", + "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a" + }, + "description": "Por favor, introduce la informaci\u00f3n del m\u00f3dulo LNK WiFi para tu dispositivo Rain Bird.", + "title": "Configurar Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configuraci\u00f3n de Rain Bird en configuration.yaml se eliminar\u00e1 en Home Assistant 2023.4. \n\nTu configuraci\u00f3n se ha importado a la IU autom\u00e1ticamente, sin embargo, los tiempos de riego predeterminados por zona ya no son compatibles. Elimina la configuraci\u00f3n YAML de Rain Bird de tu archivo configuration.yaml y reinicia Home Assistant para solucionar este problema.", + "title": "Se va a eliminar la configuraci\u00f3n YAML de Rain Bird" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Tiempo de riego predeterminado en minutos" + }, + "title": "Configurar Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/et.json b/homeassistant/components/rainbird/translations/et.json new file mode 100644 index 00000000000..96036b5f02f --- /dev/null +++ b/homeassistant/components/rainbird/translations/et.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "timeout_connect": "\u00dchenduse ajal\u00f5pp" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na" + }, + "description": "Sisesta oma Rain Birdi seadme LNK WiFi mooduli teave.", + "title": "Seadista Rain Bird" + } + } + }, + "issues": { + "deprecated_raindelay": { + "fix_flow": { + "step": { + "confirm": { + "description": "Teenus Rain Bird 'rainbird.set_rain_delay' eemaldatakse ja asendatakse numbriolemiga vihma viivituse haldamiseks. K\u00f5iki olemasolevaid automatiseerimisi v\u00f5i skripte tuleb v\u00e4rskendada, et kasutada selle asemel atribuuti \"number.set_value\" sihtm\u00e4rgiga \"{alternate_target}\".", + "title": "Rain Bird Rain Delay Service eemaldatakse" + } + } + }, + "title": "Rain Bird Rain Delay Service eemaldatakse" + }, + "deprecated_yaml": { + "description": "Rain Birdi konfigureerimine failis configuration.yaml eemaldatakse rakendusest Home Assistant 2023.4. \n\n Teie konfiguratsioon imporditi kasutajaliidesesse automaatselt, kuid vaikimisi tsoonip\u00f5hiseid niisutusaegu enam ei toetata. Selle probleemi lahendamiseks eemaldage Rain Bird YAML-i konfiguratsioon failist configuration.yaml ja taask\u00e4ivitage Home Assistant.", + "title": "Rain Birdi YAML-konfiguratsioon eemaldatakse" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Vaikimisi kastmisaeg minutites" + }, + "title": "Seadista Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/hu.json b/homeassistant/components/rainbird/translations/hu.json new file mode 100644 index 00000000000..093f52cd85a --- /dev/null +++ b/homeassistant/components/rainbird/translations/hu.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "timeout_connect": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm", + "password": "Jelsz\u00f3" + }, + "description": "K\u00e9rem, adja meg a Rain Bird k\u00e9sz\u00fcl\u00e9k\u00e9hez tartoz\u00f3 LNK WiFi modul adatait.", + "title": "Rain Bird konfigur\u00e1l\u00e1sa" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A Rain Bird konfigur\u00e1l\u00e1sa a configuration.yaml f\u00e1jlban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl a Home Assistant 2023.4-ben. \n\n A konfigur\u00e1ci\u00f3 automatikusan import\u00e1l\u00e1sra ker\u00fclt a felhaszn\u00e1l\u00f3i fel\u00fcletre, azonban az alap\u00e9rtelmezett z\u00f3n\u00e1nk\u00e9nti \u00f6nt\u00f6z\u00e9si id\u0151k m\u00e1r nem t\u00e1mogatottak. T\u00e1vol\u00edtsa el a Rain Bird YAML konfigur\u00e1ci\u00f3t a configuration.yaml f\u00e1jlb\u00f3l, \u00e9s ind\u00edtsa \u00fajra a Home Assistant alkalmaz\u00e1st a probl\u00e9ma megold\u00e1s\u00e1hoz.", + "title": "A Rain Bird YAML konfigur\u00e1ci\u00f3 elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Alap\u00e9rtelmezett \u00f6nt\u00f6z\u00e9si id\u0151, percben" + }, + "title": "Rain Bird konfigur\u00e1l\u00e1sa" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/id.json b/homeassistant/components/rainbird/translations/id.json new file mode 100644 index 00000000000..35224b1222e --- /dev/null +++ b/homeassistant/components/rainbird/translations/id.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Gagal terhubung", + "timeout_connect": "Tenggang waktu pembuatan koneksi habis" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi" + }, + "description": "Masukkan informasi modul Wi-Fi LNK untuk perangkat Rain Bird Anda.", + "title": "Konfigurasi Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Proses konfigurasi Rain Bird di configuration.yaml akan dihapus di Home Assistant 2023.4.\n\nKonfigurasi Anda telah diimpor ke antarmuka secara otomatis, namun waktu irigasi per zona default tidak lagi didukung. Hapus konfigurasi integrasi Rain Bird YAML dari file configuration.yaml Anda dan mulai ulang Home Assistant untuk memperbaiki masalah ini.", + "title": "Konfigurasi YAML Integrasi Raid Bird dalam proses penghapusan" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Waktu irigasi default dalam menit" + }, + "title": "Konfigurasi Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/it.json b/homeassistant/components/rainbird/translations/it.json new file mode 100644 index 00000000000..eaffdb54801 --- /dev/null +++ b/homeassistant/components/rainbird/translations/it.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "timeout_connect": "Tempo scaduto per stabile la connessione." + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password" + }, + "description": "Inserisci le informazioni sul modulo WiFi LNK per il tuo dispositivo Rain Bird.", + "title": "Configura Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "La configurazione di Rain Bird in configuration.yaml \u00e8 stata rimossa in Home Assistant 2023.4. \n\nLa tua configurazione \u00e8 stata importata automaticamente nell'interfaccia utente, tuttavia i tempi di irrigazione predefiniti per zona non sono pi\u00f9 supportati. Rimuovi la configurazione YAML di Rain Bird dal file configuration.yaml e riavvia Home Assistant per risolvere il problema.", + "title": "La configurazione YAML di Rain Bird \u00e8 stata rimossa" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Tempo di irrigazione predefinito in minuti" + }, + "title": "Configura Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/nl.json b/homeassistant/components/rainbird/translations/nl.json new file mode 100644 index 00000000000..c057b7ffce6 --- /dev/null +++ b/homeassistant/components/rainbird/translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken", + "timeout_connect": "Time-out bij het maken van verbinding" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord" + }, + "title": "Rain Bird configureren" + } + } + }, + "options": { + "step": { + "init": { + "title": "Rain Bird configureren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/no.json b/homeassistant/components/rainbird/translations/no.json new file mode 100644 index 00000000000..9e01072e48a --- /dev/null +++ b/homeassistant/components/rainbird/translations/no.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "timeout_connect": "Tidsavbrudd oppretter forbindelse" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord" + }, + "description": "Vennligst skriv inn LNK WiFi-modulinformasjonen for din Rain Bird-enhet.", + "title": "Konfigurer Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigurering av Rain Bird i configuration.yaml blir fjernet i Home Assistant 2023.4. \n\n Konfigurasjonen din har blitt importert til brukergrensesnittet automatisk, men standard vanningstider per sone st\u00f8ttes ikke lenger. Fjern Rain Bird YAML-konfigurasjonen fra configuration.yaml-filen og start Home Assistant p\u00e5 nytt for \u00e5 fikse dette problemet.", + "title": "Rain Bird YAML-konfigurasjonen blir fjernet" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Standard vanningstid i minutter" + }, + "title": "Konfigurer Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/pl.json b/homeassistant/components/rainbird/translations/pl.json new file mode 100644 index 00000000000..ea7598e7860 --- /dev/null +++ b/homeassistant/components/rainbird/translations/pl.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "timeout_connect": "Limit czasu na nawi\u0105zanie po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o" + }, + "description": "Wprowad\u017a informacje o module LNK WiFi dla swojego urz\u0105dzenia Rain Bird.", + "title": "Konfiguracja Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfiguracja Rain Bird w configuration.yaml zostanie usuni\u0119ta w Home Assistant 2023.4. \n\nTwoja konfiguracja zosta\u0142a automatycznie zaimportowana do interfejsu u\u017cytkownika, jednak domy\u015blne czasy nawadniania dla poszczeg\u00f3lnych stref nie s\u0105 ju\u017c obs\u0142ugiwane. Usu\u0144 konfiguracj\u0119 YAML z pliku configuration.yaml i uruchom ponownie Home Assistanta, aby rozwi\u0105za\u0107 ten problem.", + "title": "Konfiguracja YAML dla Rain Bird zostanie usuni\u0119ta" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Domy\u015blny czas nawadniania w minutach" + }, + "title": "Konfiguracja Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/pt-BR.json b/homeassistant/components/rainbird/translations/pt-BR.json new file mode 100644 index 00000000000..3400240e160 --- /dev/null +++ b/homeassistant/components/rainbird/translations/pt-BR.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Falhou ao conectar", + "timeout_connect": "Timeout establishing connection" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Senha" + }, + "description": "Insira as informa\u00e7\u00f5es do m\u00f3dulo WiFi LNK para o seu dispositivo Rain Bird.", + "title": "Configurar Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "A configura\u00e7\u00e3o do Rain Bird em configuration.yaml est\u00e1 sendo removida no Home Assistant 2023.4. \n\n Sua configura\u00e7\u00e3o foi importada para a interface do usu\u00e1rio automaticamente, no entanto, os tempos de irriga\u00e7\u00e3o padr\u00e3o por zona n\u00e3o s\u00e3o mais suportados. Remova a configura\u00e7\u00e3o Rain Bird YAML do seu arquivo configuration.yaml e reinicie o Home Assistant para corrigir esse problema.", + "title": "A configura\u00e7\u00e3o YAML de Rain Bird est\u00e1 sendo removida" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Tempo de irriga\u00e7\u00e3o padr\u00e3o em minutos" + }, + "title": "Configurar Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/ru.json b/homeassistant/components/rainbird/translations/ru.json new file mode 100644 index 00000000000..2e9f7d159cf --- /dev/null +++ b/homeassistant/components/rainbird/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." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "timeout_connect": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043c\u043e\u0434\u0443\u043b\u0435 LNK WiFi \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Rain Bird.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Rain Bird \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430 \u0432 Home Assistant \u0432\u0435\u0440\u0441\u0438\u0438 2023.4.\n\n\u0412\u0430\u0448\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0431\u044b\u043b\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0430 \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, \u043e\u0434\u043d\u0430\u043a\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0438\u0432\u0430 \u043a\u0430\u0436\u0434\u043e\u0439 \u0437\u043e\u043d\u044b \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u0423\u0434\u0430\u043b\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e YAML Rain Bird \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml \u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 Home Assistant, \u0447\u0442\u043e\u0431\u044b \u0443\u0441\u0442\u0440\u0430\u043d\u0438\u0442\u044c \u044d\u0442\u0443 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Rain Bird \u0447\u0435\u0440\u0435\u0437 YAML \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d\u0430" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "\u0412\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0438\u0432\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/sk.json b/homeassistant/components/rainbird/translations/sk.json new file mode 100644 index 00000000000..8b565d747e7 --- /dev/null +++ b/homeassistant/components/rainbird/translations/sk.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "timeout_connect": "\u010casov\u00fd limit na nadviazanie spojenia" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e", + "password": "Heslo" + }, + "description": "Zadajte inform\u00e1cie o module WiFi LNK pre va\u0161e zariadenie Rain Bird.", + "title": "Konfigur\u00e1cia Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Konfigur\u00e1cia Rain Bird v s\u00fabore configuration.yaml sa odstra\u0148uje z Home Assistant 2023.4. \n\n Va\u0161a konfigur\u00e1cia bola importovan\u00e1 do pou\u017e\u00edvate\u013esk\u00e9ho rozhrania automaticky, av\u0161ak predvolen\u00e9 \u010dasy zavla\u017eovania pre jednotliv\u00e9 z\u00f3ny u\u017e nie s\u00fa podporovan\u00e9. Odstr\u00e1\u0148te konfigur\u00e1ciu Rain Bird YAML zo s\u00faboru configuration.yaml a re\u0161tartujte Home Assistant, aby ste tento probl\u00e9m vyrie\u0161ili.", + "title": "Konfigur\u00e1cia Rain Bird YAML sa odstra\u0148uje" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Predvolen\u00fd \u010das zavla\u017eovania v min\u00fatach" + }, + "title": "Nakonfigurujte Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/tr.json b/homeassistant/components/rainbird/translations/tr.json new file mode 100644 index 00000000000..1d67a7a2383 --- /dev/null +++ b/homeassistant/components/rainbird/translations/tr.json @@ -0,0 +1,34 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "timeout_connect": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131" + }, + "step": { + "user": { + "data": { + "host": "Sunucu", + "password": "Parola" + }, + "description": "L\u00fctfen Rain Bird cihaz\u0131n\u0131z i\u00e7in LNK WiFi mod\u00fcl\u00fc bilgilerini girin.", + "title": "Rain Bird'\u00fc yap\u0131land\u0131r\u0131n" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "Configuration.yaml'de Rain Bird yap\u0131land\u0131rmas\u0131, Home Assistant 2023.4'te kald\u0131r\u0131l\u0131yor. \n\n Yap\u0131land\u0131rman\u0131z kullan\u0131c\u0131 aray\u00fcz\u00fcne otomatik olarak aktar\u0131ld\u0131, ancak her b\u00f6lge i\u00e7in varsay\u0131lan sulama s\u00fcreleri art\u0131k desteklenmiyor. Rain Bird YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu \u00e7\u00f6zmek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Rain Bird YAML yap\u0131land\u0131rmas\u0131 kald\u0131r\u0131l\u0131yor" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "Dakika cinsinden varsay\u0131lan sulama s\u00fcresi" + }, + "title": "Rain Bird'\u00fc Yap\u0131land\u0131rma" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/uk.json b/homeassistant/components/rainbird/translations/uk.json new file mode 100644 index 00000000000..44f3286a1fd --- /dev/null +++ b/homeassistant/components/rainbird/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "timeout_connect": "\u0427\u0430\u0441 \u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0437\u2019\u0454\u0434\u043d\u0430\u043d\u043d\u044f" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Rain Bird" + } + } + }, + "options": { + "step": { + "init": { + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainbird/translations/zh-Hant.json b/homeassistant/components/rainbird/translations/zh-Hant.json new file mode 100644 index 00000000000..b460af00414 --- /dev/null +++ b/homeassistant/components/rainbird/translations/zh-Hant.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "timeout_connect": "\u5efa\u7acb\u9023\u7dda\u903e\u6642" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc" + }, + "description": "\u8acb\u8f38\u5165 Rain Bird \u88dd\u7f6e\u4e0a\u7684 LNK WiFi \u6a21\u7d44\u8cc7\u8a0a\u3002", + "title": "\u8a2d\u5b9a Rain Bird" + } + } + }, + "issues": { + "deprecated_yaml": { + "description": "\u4f7f\u7528 YAML \u8a2d\u5b9a\u7684 Rain Bird \u5373\u5c07\u65bc Home Assistant 2023.4 \u7248\u4e2d\u9032\u884c\u79fb\u9664\u3002\n\n\u65e2\u6709\u7684\u8a2d\u5b9a\u5c07\u81ea\u52d5\u532f\u5165\u81f3 UI \u5167\uff0c\u4f46\u662f\u9810\u8a2d\u6bcf\u5340\u704c\u6e89\u6642\u9593\u5c07\u4e0d\u518d\u652f\u63f4\u3002\u8acb\u65bc configuration.yaml \u6a94\u6848\u4e2d\u79fb\u9664 Rain Bird YAML \u8a2d\u5b9a\u4e26\u91cd\u65b0\u555f\u52d5 Home Assistant \u4ee5\u4fee\u6b63\u6b64\u554f\u984c\u3002", + "title": "Rain Bird YAML \u8a2d\u5b9a\u5373\u5c07\u79fb\u9664" + } + }, + "options": { + "step": { + "init": { + "data": { + "duration": "\u9810\u8a2d\u704c\u6e89\u6642\u9593\uff08\u5206\uff09" + }, + "title": "\u8a2d\u5b9a Rain Bird" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainforest_eagle/diagnostics.py b/homeassistant/components/rainforest_eagle/diagnostics.py index a7022048369..f20a20af9e2 100644 --- a/homeassistant/components/rainforest_eagle/diagnostics.py +++ b/homeassistant/components/rainforest_eagle/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Eagle.""" 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 @@ -13,7 +15,7 @@ TO_REDACT = {CONF_CLOUD_ID, CONF_INSTALL_CODE} async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: EagleDataCoordinator = hass.data[DOMAIN][config_entry.entry_id] diff --git a/homeassistant/components/rainforest_eagle/translations/lv.json b/homeassistant/components/rainforest_eagle/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/rainforest_eagle/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index b5ae42559bb..1ad97de7d0b 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -41,8 +41,8 @@ async def async_get_controller( await client.load_local(ip_address, password, port=port, use_ssl=ssl) except RainMachineError: return None - else: - return get_client_controller(client) + + return get_client_controller(client) class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -172,7 +172,7 @@ class RainMachineOptionsFlowHandler(config_entries.OptionsFlow): ) -> FlowResult: """Manage the options.""" if user_input is not None: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry(data=user_input) return self.async_show_form( step_id="init", diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 66081588f27..a539ea52a85 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -5,10 +5,9 @@ import asyncio from collections.abc import Awaitable, Callable, Coroutine from dataclasses import dataclass from datetime import datetime -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from regenmaschine.errors import RainMachineError -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription diff --git a/homeassistant/components/rainmachine/translations/lt.json b/homeassistant/components/rainmachine/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/rainmachine/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/lv.json b/homeassistant/components/rainmachine/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/rainmachine/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rdw/strings.json b/homeassistant/components/rdw/strings.json index 48bcd8c0c5d..840802a12b7 100644 --- a/homeassistant/components/rdw/strings.json +++ b/homeassistant/components/rdw/strings.json @@ -7,6 +7,9 @@ } } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown_license_plate": "Unknown license plate" diff --git a/homeassistant/components/rdw/translations/bg.json b/homeassistant/components/rdw/translations/bg.json index e9a9c468402..d6135212cb6 100644 --- a/homeassistant/components/rdw/translations/bg.json +++ b/homeassistant/components/rdw/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435" } diff --git a/homeassistant/components/rdw/translations/ca.json b/homeassistant/components/rdw/translations/ca.json index 3871995e288..cdf6e6145b9 100644 --- a/homeassistant/components/rdw/translations/ca.json +++ b/homeassistant/components/rdw/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "unknown_license_plate": "Matr\u00edcula desconeguda" diff --git a/homeassistant/components/rdw/translations/de.json b/homeassistant/components/rdw/translations/de.json index b7b25359183..21ccb203ae8 100644 --- a/homeassistant/components/rdw/translations/de.json +++ b/homeassistant/components/rdw/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "unknown_license_plate": "Unbekanntes Nummernschild" diff --git a/homeassistant/components/rdw/translations/en.json b/homeassistant/components/rdw/translations/en.json index 9d2827ed4de..7f0f94f0a4c 100644 --- a/homeassistant/components/rdw/translations/en.json +++ b/homeassistant/components/rdw/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Service is already configured" + }, "error": { "cannot_connect": "Failed to connect", "unknown_license_plate": "Unknown license plate" diff --git a/homeassistant/components/rdw/translations/et.json b/homeassistant/components/rdw/translations/et.json index 48911f2bcb7..06835b4904d 100644 --- a/homeassistant/components/rdw/translations/et.json +++ b/homeassistant/components/rdw/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud" + }, "error": { "cannot_connect": "\u00dchendamine nurjus", "unknown_license_plate": "Tundmatu numbrim\u00e4rk" diff --git a/homeassistant/components/rdw/translations/no.json b/homeassistant/components/rdw/translations/no.json index f6d742f1bac..5d2d4cd9494 100644 --- a/homeassistant/components/rdw/translations/no.json +++ b/homeassistant/components/rdw/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, "error": { "cannot_connect": "Tilkobling mislyktes", "unknown_license_plate": "Ukjent nummerskilt" diff --git a/homeassistant/components/rdw/translations/ru.json b/homeassistant/components/rdw/translations/ru.json index a885bf3067e..297be7ad700 100644 --- a/homeassistant/components/rdw/translations/ru.json +++ b/homeassistant/components/rdw/translations/ru.json @@ -1,5 +1,8 @@ { "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.", "unknown_license_plate": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440\u043d\u043e\u0439 \u0437\u043d\u0430\u043a." diff --git a/homeassistant/components/rdw/translations/zh-Hant.json b/homeassistant/components/rdw/translations/zh-Hant.json index fdf54dd247a..eebfab9de56 100644 --- a/homeassistant/components/rdw/translations/zh-Hant.json +++ b/homeassistant/components/rdw/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "unknown_license_plate": "\u672a\u77e5\u8eca\u724c" diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index 0d7f35b6e62..21cf574d548 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -1,7 +1,7 @@ """The ReCollect Waste integration.""" from __future__ import annotations -from datetime import date, timedelta +from datetime import timedelta from typing import Any from aiorecollect.client import Client, PickupEvent @@ -18,7 +18,7 @@ from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DOMAIN, LOGGER DEFAULT_NAME = "recollect_waste" DEFAULT_UPDATE_INTERVAL = timedelta(days=1) -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.CALENDAR, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -31,9 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_get_pickup_events() -> list[PickupEvent]: """Get the next pickup.""" try: - return await client.async_get_pickup_events( - start_date=date.today(), end_date=date.today() + timedelta(weeks=4) - ) + return await client.async_get_pickup_events() except RecollectError as err: raise UpdateFailed( f"Error while requesting data from ReCollect: {err}" diff --git a/homeassistant/components/recollect_waste/calendar.py b/homeassistant/components/recollect_waste/calendar.py new file mode 100644 index 00000000000..120ab77c3b3 --- /dev/null +++ b/homeassistant/components/recollect_waste/calendar.py @@ -0,0 +1,96 @@ +"""Support for ReCollect Waste calendars.""" +from __future__ import annotations + +import datetime + +from aiorecollect.client import PickupEvent + +from homeassistant.components.calendar import CalendarEntity, CalendarEvent +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 DOMAIN +from .entity import ReCollectWasteEntity +from .util import async_get_pickup_type_names + + +@callback +def async_get_calendar_event_from_pickup_event( + entry: ConfigEntry, pickup_event: PickupEvent +) -> CalendarEvent: + """Get a HASS CalendarEvent from an aiorecollect PickupEvent.""" + pickup_type_string = ", ".join( + async_get_pickup_type_names(entry, pickup_event.pickup_types) + ) + return CalendarEvent( + summary="ReCollect Waste Pickup", + description=f"Pickup types: {pickup_type_string}", + location=pickup_event.area_name, + start=pickup_event.date, + end=pickup_event.date + datetime.timedelta(days=1), + ) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up ReCollect Waste sensors based on a config entry.""" + coordinator: DataUpdateCoordinator[list[PickupEvent]] = hass.data[DOMAIN][ + entry.entry_id + ] + + async_add_entities([ReCollectWasteCalendar(coordinator, entry)]) + + +class ReCollectWasteCalendar(ReCollectWasteEntity, CalendarEntity): + """Define a ReCollect Waste calendar.""" + + _attr_icon = "mdi:delete-empty" + + def __init__( + self, + coordinator: DataUpdateCoordinator[list[PickupEvent]], + entry: ConfigEntry, + ) -> None: + """Initialize the ReCollect Waste entity.""" + super().__init__(coordinator, entry) + + self._attr_unique_id = self._identifier + self._event: CalendarEvent | None = None + + @property + def event(self) -> CalendarEvent | None: + """Return the next upcoming event.""" + return self._event + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + try: + current_event = next( + event + for event in self.coordinator.data + if event.date >= datetime.date.today() + ) + except StopIteration: + self._event = None + else: + self._event = async_get_calendar_event_from_pickup_event( + self._entry, current_event + ) + + super()._handle_coordinator_update() + + async def async_get_events( + self, + hass: HomeAssistant, + start_date: datetime.datetime, + end_date: datetime.datetime, + ) -> list[CalendarEvent]: + """Return calendar events within a datetime range.""" + return [ + async_get_calendar_event_from_pickup_event(self._entry, event) + for event in self.coordinator.data + ] diff --git a/homeassistant/components/recollect_waste/diagnostics.py b/homeassistant/components/recollect_waste/diagnostics.py index d410eb40085..35bc1b56896 100644 --- a/homeassistant/components/recollect_waste/diagnostics.py +++ b/homeassistant/components/recollect_waste/diagnostics.py @@ -4,6 +4,8 @@ from __future__ import annotations import dataclasses from typing import Any +from aiorecollect.client import PickupEvent + from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_UNIQUE_ID @@ -28,7 +30,9 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: DataUpdateCoordinator[list[PickupEvent]] = hass.data[DOMAIN][ + entry.entry_id + ] return async_redact_data( { diff --git a/homeassistant/components/recollect_waste/entity.py b/homeassistant/components/recollect_waste/entity.py new file mode 100644 index 00000000000..41781b10355 --- /dev/null +++ b/homeassistant/components/recollect_waste/entity.py @@ -0,0 +1,42 @@ +"""Define a base ReCollect Waste entity.""" +from aiorecollect.client import PickupEvent + +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DOMAIN + + +class ReCollectWasteEntity(CoordinatorEntity[DataUpdateCoordinator[list[PickupEvent]]]): + """Define a base ReCollect Waste entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: DataUpdateCoordinator[list[PickupEvent]], + entry: ConfigEntry, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + + self._identifier = f"{entry.data[CONF_PLACE_ID]}_{entry.data[CONF_SERVICE_ID]}" + + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, self._identifier)}, + manufacturer="ReCollect Waste", + name="ReCollect Waste", + ) + self._attr_extra_state_attributes = {} + self._entry = entry + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + await super().async_added_to_hass() + self._handle_coordinator_update() diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 7d527ac56c6..4883734f47e 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -1,7 +1,9 @@ """Support for ReCollect Waste sensors.""" from __future__ import annotations -from aiorecollect.client import PickupType +from datetime import date + +from aiorecollect.client import PickupEvent from homeassistant.components.sensor import ( SensorDeviceClass, @@ -9,15 +11,13 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_FRIENDLY_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DOMAIN, LOGGER +from .const import DOMAIN, LOGGER +from .entity import ReCollectWasteEntity +from .util import async_get_pickup_type_names ATTR_PICKUP_TYPES = "pickup_types" ATTR_AREA_NAME = "area_name" @@ -37,86 +37,58 @@ SENSOR_DESCRIPTIONS = ( ) -@callback -def async_get_pickup_type_names( - entry: ConfigEntry, pickup_types: list[PickupType] -) -> list[str]: - """Return proper pickup type names from their associated objects.""" - return [ - t.friendly_name - if entry.options.get(CONF_FRIENDLY_NAME) and t.friendly_name - else t.name - for t in pickup_types - ] - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up ReCollect Waste sensors based on a config entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: DataUpdateCoordinator[list[PickupEvent]] = hass.data[DOMAIN][ + entry.entry_id + ] async_add_entities( - [ - ReCollectWasteSensor(coordinator, entry, description) - for description in SENSOR_DESCRIPTIONS - ] + ReCollectWasteSensor(coordinator, entry, description) + for description in SENSOR_DESCRIPTIONS ) -class ReCollectWasteSensor(CoordinatorEntity, SensorEntity): - """ReCollect Waste Sensor.""" +class ReCollectWasteSensor(ReCollectWasteEntity, SensorEntity): + """Define a ReCollect Waste sensor.""" _attr_device_class = SensorDeviceClass.DATE - _attr_has_entity_name = True + + PICKUP_INDEX_MAP = { + SENSOR_TYPE_CURRENT_PICKUP: 1, + SENSOR_TYPE_NEXT_PICKUP: 2, + } def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[list[PickupEvent]], entry: ConfigEntry, description: SensorEntityDescription, ) -> None: - """Initialize the sensor.""" - super().__init__(coordinator) + """Initialize.""" + super().__init__(coordinator, entry) - self._attr_extra_state_attributes = {} - self._attr_unique_id = f"{entry.data[CONF_PLACE_ID]}_{entry.data[CONF_SERVICE_ID]}_{description.key}" - self._entry = entry + self._attr_unique_id = f"{self._identifier}_{description.key}" self.entity_description = description @callback def _handle_coordinator_update(self) -> None: - """Respond to a DataUpdateCoordinator update.""" - self.update_from_latest_data() - self.async_write_ha_state() + """Handle updated data from the coordinator.""" + relevant_events = (e for e in self.coordinator.data if e.date >= date.today()) + pickup_index = self.PICKUP_INDEX_MAP[self.entity_description.key] - async def async_added_to_hass(self) -> None: - """Handle entity which will be added.""" - await super().async_added_to_hass() - self.update_from_latest_data() - - @callback - def update_from_latest_data(self) -> None: - """Update the state.""" - if self.entity_description.key == SENSOR_TYPE_CURRENT_PICKUP: - try: - event = self.coordinator.data[0] - except IndexError: - LOGGER.error("No current pickup found") - return + try: + for _ in range(pickup_index): + event = next(relevant_events) + except StopIteration: + LOGGER.debug("No pickup event found for %s", self.entity_description.key) + self._attr_extra_state_attributes = {} + self._attr_native_value = None else: - try: - event = self.coordinator.data[1] - except IndexError: - LOGGER.info("No next pickup found") - return - - self._attr_extra_state_attributes.update( - { - ATTR_PICKUP_TYPES: async_get_pickup_type_names( - self._entry, event.pickup_types - ), - ATTR_AREA_NAME: event.area_name, - } - ) - self._attr_native_value = event.date + self._attr_extra_state_attributes[ATTR_AREA_NAME] = event.area_name + self._attr_extra_state_attributes[ + ATTR_PICKUP_TYPES + ] = async_get_pickup_type_names(self._entry, event.pickup_types) + self._attr_native_value = event.date diff --git a/homeassistant/components/recollect_waste/translations/lv.json b/homeassistant/components/recollect_waste/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/util.py b/homeassistant/components/recollect_waste/util.py new file mode 100644 index 00000000000..185078f297c --- /dev/null +++ b/homeassistant/components/recollect_waste/util.py @@ -0,0 +1,19 @@ +"""Define ReCollect Waste utilities.""" +from aiorecollect.client import PickupType + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_FRIENDLY_NAME +from homeassistant.core import callback + + +@callback +def async_get_pickup_type_names( + entry: ConfigEntry, pickup_types: list[PickupType] +) -> list[str]: + """Return proper pickup type names from their associated objects.""" + return [ + t.friendly_name + if entry.options.get(CONF_FRIENDLY_NAME) and t.friendly_name + else t.name + for t in pickup_types + ] diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 3e1e8264642..71795bfa664 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -43,7 +43,7 @@ DEFAULT_DB_FILE = "home-assistant_v2.db" DEFAULT_DB_INTEGRITY_CHECK = True DEFAULT_DB_MAX_RETRIES = 10 DEFAULT_DB_RETRY_WAIT = 3 -DEFAULT_COMMIT_INTERVAL = 1 +DEFAULT_COMMIT_INTERVAL = 5 CONF_AUTO_PURGE = "auto_purge" CONF_AUTO_REPACK = "auto_repack" diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index 8db4b43e04e..379185bec7b 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -19,7 +19,7 @@ EVENT_RECORDER_HOURLY_STATISTICS_GENERATED = "recorder_hourly_statistics_generat CONF_DB_INTEGRITY_CHECK = "db_integrity_check" -MAX_QUEUE_BACKLOG = 40000 +MAX_QUEUE_BACKLOG = 65000 # The maximum number of rows (events) we purge in one delete statement diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 66e85eac2b3..ddca32e6970 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -186,7 +186,7 @@ class Recorder(threading.Thread): self.run_history = RunHistory() self.entity_filter = entity_filter - self.exclude_t = exclude_t + self.exclude_t = set(exclude_t) self.schema_version = 0 self._commits_without_expire = 0 @@ -377,7 +377,8 @@ class Recorder(threading.Thread): # Unknown what it is. return True - def _empty_queue(self, event: Event) -> None: + @callback + def _async_empty_queue(self, event: Event) -> None: """Empty the queue if its still present at final write.""" # If the queue is full of events to be processed because @@ -411,7 +412,7 @@ class Recorder(threading.Thread): def async_register(self) -> None: """Post connection initialize.""" bus = self.hass.bus - bus.async_listen_once(EVENT_HOMEASSISTANT_FINAL_WRITE, self._empty_queue) + bus.async_listen_once(EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_empty_queue) bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown) async_at_started(self.hass, self._async_hass_started) @@ -835,7 +836,9 @@ class Recorder(threading.Thread): return try: - shared_data_bytes = EventData.shared_data_bytes_from_event(event) + shared_data_bytes = EventData.shared_data_bytes_from_event( + event, self.dialect_name + ) except JSON_ENCODE_EXCEPTIONS as ex: _LOGGER.warning("Event is not JSON serializable: %s: %s", event, ex) return @@ -868,7 +871,7 @@ class Recorder(threading.Thread): try: dbstate = States.from_event(event) shared_attrs_bytes = StateAttributes.shared_attrs_bytes_from_event( - event, self._exclude_attributes_by_domain + event, self._exclude_attributes_by_domain, self.dialect_name ) except JSON_ENCODE_EXCEPTIONS as ex: _LOGGER.warning( @@ -1021,6 +1024,12 @@ class Recorder(threading.Thread): self.event_session = self.get_session() self.event_session.expire_on_commit = False + def _post_schema_migration(self, old_version: int, new_version: int) -> None: + """Run post schema migration tasks.""" + migration.post_schema_migration( + self.engine, self.event_session, old_version, new_version + ) + def _send_keep_alive(self) -> None: """Send a keep alive to keep the db connection open.""" assert self.event_session is not None @@ -1035,6 +1044,8 @@ class Recorder(threading.Thread): async def async_block_till_done(self) -> None: """Async version of block_till_done.""" + if self._queue.empty() and not self._event_session_has_pending_writes(): + return event = asyncio.Event() self.queue_task(SynchronizeTask(event)) await event.wait() diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 2c8c1ad2fff..1fef18573ea 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from datetime import datetime, timedelta import logging +import time from typing import Any, TypeVar, cast import ciso8601 @@ -42,18 +43,19 @@ from homeassistant.helpers.json import ( JSON_DECODE_EXCEPTIONS, JSON_DUMP, json_bytes, + json_bytes_strip_null, json_loads, ) import homeassistant.util.dt as dt_util -from .const import ALL_DOMAIN_EXCLUDE_ATTRS +from .const import ALL_DOMAIN_EXCLUDE_ATTRS, SupportedDialect from .models import StatisticData, StatisticMetaData, process_timestamp # SQLAlchemy Schema # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 30 +SCHEMA_VERSION = 33 _StatisticsBaseSelfT = TypeVar("_StatisticsBaseSelfT", bound="StatisticsBase") @@ -90,8 +92,8 @@ TABLES_TO_CHECK = [ TABLE_SCHEMA_CHANGES, ] -LAST_UPDATED_INDEX = "ix_states_last_updated" -ENTITY_ID_LAST_UPDATED_INDEX = "ix_states_entity_id_last_updated" +LAST_UPDATED_INDEX_TS = "ix_states_last_updated_ts" +ENTITY_ID_LAST_UPDATED_INDEX_TS = "ix_states_entity_id_last_updated_ts" EVENTS_CONTEXT_ID_INDEX = "ix_events_context_id" STATES_CONTEXT_ID_INDEX = "ix_states_context_id" @@ -122,6 +124,8 @@ DOUBLE_TYPE = ( .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") ) +TIMESTAMP_TYPE = DOUBLE_TYPE + class JSONLiteral(JSON): # type: ignore[misc] """Teach SA how to literalize json.""" @@ -146,7 +150,7 @@ class Events(Base): # type: ignore[misc,valid-type] __table_args__ = ( # Used for fetching events at a specific time # see logbook - Index("ix_events_event_type_time_fired", "event_type", "time_fired"), + Index("ix_events_event_type_time_fired_ts", "event_type", "time_fired_ts"), {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, ) __tablename__ = TABLE_EVENTS @@ -155,7 +159,8 @@ class Events(Base): # type: ignore[misc,valid-type] event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) # no longer used for new rows origin_idx = Column(SmallInteger) - time_fired = Column(DATETIME_TYPE, index=True) + time_fired = Column(DATETIME_TYPE) # no longer used for new rows + time_fired_ts = Column(TIMESTAMP_TYPE, index=True) context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) @@ -167,10 +172,19 @@ class Events(Base): # type: ignore[misc,valid-type] return ( "" ) + @property + def _time_fired_isotime(self) -> str: + """Return time_fired as an isotime string.""" + if self.time_fired_ts is not None: + date_time = dt_util.utc_from_timestamp(self.time_fired_ts) + else: + date_time = process_timestamp(self.time_fired) + return date_time.isoformat(sep=" ", timespec="seconds") + @staticmethod def from_event(event: Event) -> Events: """Create an event database object from a native event.""" @@ -178,7 +192,8 @@ class Events(Base): # type: ignore[misc,valid-type] event_type=event.event_type, event_data=None, origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), - time_fired=event.time_fired, + time_fired=None, + time_fired_ts=dt_util.utc_to_timestamp(event.time_fired), context_id=event.context.id, context_user_id=event.context.user_id, context_parent_id=event.context.parent_id, @@ -198,7 +213,7 @@ class Events(Base): # type: ignore[misc,valid-type] EventOrigin(self.origin) if self.origin else EVENT_ORIGIN_ORDER[self.origin_idx], - process_timestamp(self.time_fired), + dt_util.utc_from_timestamp(self.time_fired_ts), context=context, ) except JSON_DECODE_EXCEPTIONS: @@ -237,8 +252,12 @@ class EventData(Base): # type: ignore[misc,valid-type] ) @staticmethod - def shared_data_bytes_from_event(event: Event) -> bytes: + def shared_data_bytes_from_event( + event: Event, dialect: SupportedDialect | None + ) -> bytes: """Create shared_data from an event.""" + if dialect == SupportedDialect.POSTGRESQL: + return json_bytes_strip_null(event.data) return json_bytes(event.data) @staticmethod @@ -261,7 +280,7 @@ class States(Base): # type: ignore[misc,valid-type] __table_args__ = ( # Used for fetching the state of entities at a specific time # (get_states in history.py) - Index(ENTITY_ID_LAST_UPDATED_INDEX, "entity_id", "last_updated"), + Index(ENTITY_ID_LAST_UPDATED_INDEX_TS, "entity_id", "last_updated_ts"), {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, ) __tablename__ = TABLE_STATES @@ -274,8 +293,10 @@ class States(Base): # type: ignore[misc,valid-type] event_id = Column( # no longer used for new rows Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True ) - last_changed = Column(DATETIME_TYPE) - last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) + last_changed = Column(DATETIME_TYPE) # no longer used for new rows + last_changed_ts = Column(TIMESTAMP_TYPE) + last_updated = Column(DATETIME_TYPE) # no longer used for new rows + last_updated_ts = Column(TIMESTAMP_TYPE, default=time.time, index=True) old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) attributes_id = Column( Integer, ForeignKey("state_attributes.attributes_id"), index=True @@ -292,10 +313,19 @@ class States(Base): # type: ignore[misc,valid-type] return ( f"" ) + @property + def _last_updated_isotime(self) -> str: + """Return last_updated as an isotime string.""" + if self.last_updated_ts is not None: + date_time = dt_util.utc_from_timestamp(self.last_updated_ts) + else: + date_time = process_timestamp(self.last_updated) + return date_time.isoformat(sep=" ", timespec="seconds") + @staticmethod def from_event(event: Event) -> States: """Create object from a state_changed event.""" @@ -308,21 +338,22 @@ class States(Base): # type: ignore[misc,valid-type] context_user_id=event.context.user_id, context_parent_id=event.context.parent_id, origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), + last_updated=None, + last_changed=None, ) - # None state means the state was removed from the state machine if state is None: dbstate.state = "" - dbstate.last_updated = event.time_fired - dbstate.last_changed = None + dbstate.last_updated_ts = dt_util.utc_to_timestamp(event.time_fired) + dbstate.last_changed_ts = None return dbstate dbstate.state = state.state - dbstate.last_updated = state.last_updated + dbstate.last_updated_ts = dt_util.utc_to_timestamp(state.last_updated) if state.last_updated == state.last_changed: - dbstate.last_changed = None + dbstate.last_changed_ts = None else: - dbstate.last_changed = state.last_changed + dbstate.last_changed_ts = dt_util.utc_to_timestamp(state.last_changed) return dbstate @@ -339,11 +370,13 @@ class States(Base): # type: ignore[misc,valid-type] # When json_loads fails _LOGGER.exception("Error converting row to state: %s", self) return None - if self.last_changed is None or self.last_changed == self.last_updated: - last_changed = last_updated = process_timestamp(self.last_updated) + if self.last_changed_ts is None or self.last_changed_ts == self.last_updated_ts: + last_changed = last_updated = dt_util.utc_from_timestamp( + self.last_updated_ts or 0 + ) else: - last_updated = process_timestamp(self.last_updated) - last_changed = process_timestamp(self.last_changed) + last_updated = dt_util.utc_from_timestamp(self.last_updated_ts or 0) + last_changed = dt_util.utc_from_timestamp(self.last_changed_ts or 0) return State( self.entity_id, self.state, @@ -388,7 +421,9 @@ class StateAttributes(Base): # type: ignore[misc,valid-type] @staticmethod def shared_attrs_bytes_from_event( - event: Event, exclude_attrs_by_domain: dict[str, set[str]] + event: Event, + exclude_attrs_by_domain: dict[str, set[str]], + dialect: SupportedDialect | None, ) -> bytes: """Create shared_attrs from a state_changed event.""" state: State | None = event.data.get("new_state") @@ -399,6 +434,10 @@ class StateAttributes(Base): # type: ignore[misc,valid-type] exclude_attrs = ( exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS ) + if dialect == SupportedDialect.POSTGRESQL: + return json_bytes_strip_null( + {k: v for k, v in state.attributes.items() if k not in exclude_attrs} + ) return json_bytes( {k: v for k, v in state.attributes.items() if k not in exclude_attrs} ) diff --git a/homeassistant/components/recorder/executor.py b/homeassistant/components/recorder/executor.py index 0d913310e74..6eea2f651c3 100644 --- a/homeassistant/components/recorder/executor.py +++ b/homeassistant/components/recorder/executor.py @@ -39,7 +39,11 @@ class DBInterruptibleThreadPoolExecutor(InterruptibleThreadPoolExecutor): # When the executor gets lost, the weakref callback will wake up # the worker threads. - def weakref_cb(_: Any, q=self._work_queue) -> None: # type: ignore[no-untyped-def] # pylint: disable=invalid-name + # pylint: disable=invalid-name + def weakref_cb( # type: ignore[no-untyped-def] + _: Any, + q=self._work_queue, + ) -> None: q.put(None) num_threads = len(self._threads) diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 72cfa784074..48251b6db59 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -93,10 +93,13 @@ class Filters: """Return human readable excludes/includes.""" return ( "" + f" excluded_entities={self.excluded_entities}" + f" excluded_domains={self.excluded_domains}" + f" excluded_entity_globs={self.excluded_entity_globs}" + f" included_entities={self.included_entities}" + f" included_domains={self.included_domains}" + f" included_entity_globs={self.included_entity_globs}" + ">" ) @property diff --git a/homeassistant/components/recorder/history.py b/homeassistant/components/recorder/history.py index 5c3f47c02ed..e4f5a39f1cc 100644 --- a/homeassistant/components/recorder/history.py +++ b/homeassistant/components/recorder/history.py @@ -17,10 +17,7 @@ from sqlalchemy.sql.expression import literal from sqlalchemy.sql.lambdas import StatementLambdaElement from sqlalchemy.sql.selectable import Subquery -from homeassistant.components.websocket_api import ( - COMPRESSED_STATE_LAST_UPDATED, - COMPRESSED_STATE_STATE, -) +from homeassistant.const import COMPRESSED_STATE_LAST_UPDATED, COMPRESSED_STATE_STATE from homeassistant.core import HomeAssistant, State, split_entity_id import homeassistant.util.dt as dt_util @@ -29,10 +26,12 @@ from .db_schema import RecorderRuns, StateAttributes, States from .filters import Filters from .models import ( LazyState, + LazyStatePreSchema31, process_datetime_to_timestamp, process_timestamp, process_timestamp_to_utc_isoformat, row_to_compressed_state, + row_to_compressed_state_pre_schema_31, ) from .util import execute_stmt_lambda_element, session_scope @@ -59,49 +58,84 @@ NEED_ATTRIBUTE_DOMAINS = { "water_heater", } -BASE_STATES = [ + +_BASE_STATES = [ + States.entity_id, + States.state, + States.last_changed_ts, + States.last_updated_ts, +] +_BASE_STATES_NO_LAST_CHANGED = [ + States.entity_id, + States.state, + literal(value=None).label("last_changed_ts"), + States.last_updated_ts, +] +_QUERY_STATE_NO_ATTR = [ + *_BASE_STATES, + literal(value=None, type_=Text).label("attributes"), + literal(value=None, type_=Text).label("shared_attrs"), +] +_QUERY_STATE_NO_ATTR_NO_LAST_CHANGED = [ + *_BASE_STATES_NO_LAST_CHANGED, + literal(value=None, type_=Text).label("attributes"), + literal(value=None, type_=Text).label("shared_attrs"), +] +_BASE_STATES_PRE_SCHEMA_31 = [ States.entity_id, States.state, States.last_changed, States.last_updated, ] -BASE_STATES_NO_LAST_CHANGED = [ +_BASE_STATES_NO_LAST_CHANGED_PRE_SCHEMA_31 = [ States.entity_id, States.state, literal(value=None, type_=Text).label("last_changed"), States.last_updated, ] -QUERY_STATE_NO_ATTR = [ - *BASE_STATES, +_QUERY_STATE_NO_ATTR_PRE_SCHEMA_31 = [ + *_BASE_STATES_PRE_SCHEMA_31, literal(value=None, type_=Text).label("attributes"), literal(value=None, type_=Text).label("shared_attrs"), ] -QUERY_STATE_NO_ATTR_NO_LAST_CHANGED = [ - *BASE_STATES_NO_LAST_CHANGED, +_QUERY_STATE_NO_ATTR_NO_LAST_CHANGED_PRE_SCHEMA_31 = [ + *_BASE_STATES_NO_LAST_CHANGED_PRE_SCHEMA_31, literal(value=None, type_=Text).label("attributes"), literal(value=None, type_=Text).label("shared_attrs"), ] # Remove QUERY_STATES_PRE_SCHEMA_25 # and the migration_in_progress check # once schema 26 is created -QUERY_STATES_PRE_SCHEMA_25 = [ - *BASE_STATES, +_QUERY_STATES_PRE_SCHEMA_25 = [ + *_BASE_STATES_PRE_SCHEMA_31, States.attributes, literal(value=None, type_=Text).label("shared_attrs"), ] -QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED = [ - *BASE_STATES_NO_LAST_CHANGED, +_QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED = [ + *_BASE_STATES_NO_LAST_CHANGED_PRE_SCHEMA_31, States.attributes, literal(value=None, type_=Text).label("shared_attrs"), ] -QUERY_STATES = [ - *BASE_STATES, +_QUERY_STATES_PRE_SCHEMA_31 = [ + *_BASE_STATES_PRE_SCHEMA_31, # Remove States.attributes once all attributes are in StateAttributes.shared_attrs States.attributes, StateAttributes.shared_attrs, ] -QUERY_STATES_NO_LAST_CHANGED = [ - *BASE_STATES_NO_LAST_CHANGED, +_QUERY_STATES_NO_LAST_CHANGED_PRE_SCHEMA_31 = [ + *_BASE_STATES_NO_LAST_CHANGED_PRE_SCHEMA_31, + # Remove States.attributes once all attributes are in StateAttributes.shared_attrs + States.attributes, + StateAttributes.shared_attrs, +] +_QUERY_STATES = [ + *_BASE_STATES, + # Remove States.attributes once all attributes are in StateAttributes.shared_attrs + States.attributes, + StateAttributes.shared_attrs, +] +_QUERY_STATES_NO_LAST_CHANGED = [ + *_BASE_STATES_NO_LAST_CHANGED, # Remove States.attributes once all attributes are in StateAttributes.shared_attrs States.attributes, StateAttributes.shared_attrs, @@ -124,10 +158,25 @@ def lambda_stmt_and_join_attributes( # without the attributes fields and do not join the # state_attributes table if no_attributes: + if schema_version >= 31: + if include_last_changed: + return ( + lambda_stmt(lambda: select(*_QUERY_STATE_NO_ATTR)), + False, + ) + return ( + lambda_stmt(lambda: select(*_QUERY_STATE_NO_ATTR_NO_LAST_CHANGED)), + False, + ) if include_last_changed: - return lambda_stmt(lambda: select(*QUERY_STATE_NO_ATTR)), False + return ( + lambda_stmt(lambda: select(*_QUERY_STATE_NO_ATTR_PRE_SCHEMA_31)), + False, + ) return ( - lambda_stmt(lambda: select(*QUERY_STATE_NO_ATTR_NO_LAST_CHANGED)), + lambda_stmt( + lambda: select(*_QUERY_STATE_NO_ATTR_NO_LAST_CHANGED_PRE_SCHEMA_31) + ), False, ) # If we in the process of migrating schema we do @@ -136,19 +185,27 @@ def lambda_stmt_and_join_attributes( if schema_version < 25: if include_last_changed: return ( - lambda_stmt(lambda: select(*QUERY_STATES_PRE_SCHEMA_25)), + lambda_stmt(lambda: select(*_QUERY_STATES_PRE_SCHEMA_25)), False, ) return ( - lambda_stmt(lambda: select(*QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED)), + lambda_stmt(lambda: select(*_QUERY_STATES_PRE_SCHEMA_25_NO_LAST_CHANGED)), False, ) + + if schema_version >= 31: + if include_last_changed: + return lambda_stmt(lambda: select(*_QUERY_STATES)), True + return lambda_stmt(lambda: select(*_QUERY_STATES_NO_LAST_CHANGED)), True # Finally if no migration is in progress and no_attributes # was not requested, we query both attributes columns and # join state_attributes if include_last_changed: - return lambda_stmt(lambda: select(*QUERY_STATES)), True - return lambda_stmt(lambda: select(*QUERY_STATES_NO_LAST_CHANGED)), True + return lambda_stmt(lambda: select(*_QUERY_STATES_PRE_SCHEMA_31)), True + return ( + lambda_stmt(lambda: select(*_QUERY_STATES_NO_LAST_CHANGED_PRE_SCHEMA_31)), + True, + ) def get_significant_states( @@ -211,22 +268,41 @@ def _significant_states_stmt( and significant_changes_only and split_entity_id(entity_ids[0])[0] not in SIGNIFICANT_DOMAINS ): + if schema_version >= 31: + stmt += lambda q: q.filter( + (States.last_changed_ts == States.last_updated_ts) + | States.last_changed_ts.is_(None) + ) stmt += lambda q: q.filter( (States.last_changed == States.last_updated) | States.last_changed.is_(None) ) elif significant_changes_only: - stmt += lambda q: q.filter( - or_( - *[ - States.entity_id.like(entity_domain) - for entity_domain in SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE - ], - ( - (States.last_changed == States.last_updated) - | States.last_changed.is_(None) - ), + if schema_version >= 31: + stmt += lambda q: q.filter( + or_( + *[ + States.entity_id.like(entity_domain) + for entity_domain in SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE + ], + ( + (States.last_changed_ts == States.last_updated_ts) + | States.last_changed_ts.is_(None) + ), + ) + ) + else: + stmt += lambda q: q.filter( + or_( + *[ + States.entity_id.like(entity_domain) + for entity_domain in SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE + ], + ( + (States.last_changed == States.last_updated) + | States.last_changed.is_(None) + ), + ) ) - ) if entity_ids: stmt += lambda q: q.filter(States.entity_id.in_(entity_ids)) @@ -238,15 +314,25 @@ def _significant_states_stmt( lambda q: q.filter(entity_filter), track_on=[filters] ) - stmt += lambda q: q.filter(States.last_updated > start_time) - if end_time: - stmt += lambda q: q.filter(States.last_updated < end_time) + if schema_version >= 31: + start_time_ts = start_time.timestamp() + stmt += lambda q: q.filter(States.last_updated_ts > start_time_ts) + if end_time: + end_time_ts = end_time.timestamp() + stmt += lambda q: q.filter(States.last_updated_ts < end_time_ts) + else: + stmt += lambda q: q.filter(States.last_updated > start_time) + if end_time: + stmt += lambda q: q.filter(States.last_updated < end_time) if join_attributes: stmt += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) - stmt += lambda q: q.order_by(States.entity_id, States.last_updated) + if schema_version >= 31: + stmt += lambda q: q.order_by(States.entity_id, States.last_updated_ts) + else: + stmt += lambda q: q.order_by(States.entity_id, States.last_updated) return stmt @@ -312,7 +398,11 @@ def get_full_significant_states_with_session( significant_changes_only: bool = True, no_attributes: bool = False, ) -> MutableMapping[str, list[State]]: - """Variant of get_significant_states_with_session that does not return minimal responses.""" + """Variant of get_significant_states_with_session. + + Difference with get_significant_states_with_session is that it does not + return minimal responses. + """ return cast( MutableMapping[str, list[State]], get_significant_states_with_session( @@ -342,12 +432,29 @@ def _state_changed_during_period_stmt( stmt, join_attributes = lambda_stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=False ) - stmt += lambda q: q.filter( - ((States.last_changed == States.last_updated) | States.last_changed.is_(None)) - & (States.last_updated > start_time) - ) + if schema_version >= 31: + start_time_ts = start_time.timestamp() + stmt += lambda q: q.filter( + ( + (States.last_changed_ts == States.last_updated_ts) + | States.last_changed_ts.is_(None) + ) + & (States.last_updated_ts > start_time_ts) + ) + else: + stmt += lambda q: q.filter( + ( + (States.last_changed == States.last_updated) + | States.last_changed.is_(None) + ) + & (States.last_updated > start_time) + ) if end_time: - stmt += lambda q: q.filter(States.last_updated < end_time) + if schema_version >= 31: + end_time_ts = end_time.timestamp() + stmt += lambda q: q.filter(States.last_updated_ts < end_time_ts) + else: + stmt += lambda q: q.filter(States.last_updated < end_time) if entity_id: stmt += lambda q: q.filter(States.entity_id == entity_id) if join_attributes: @@ -355,9 +462,17 @@ def _state_changed_during_period_stmt( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) if descending: - stmt += lambda q: q.order_by(States.entity_id, States.last_updated.desc()) + if schema_version >= 31: + stmt += lambda q: q.order_by( + States.entity_id, States.last_updated_ts.desc() + ) + else: + stmt += lambda q: q.order_by(States.entity_id, States.last_updated.desc()) else: - stmt += lambda q: q.order_by(States.entity_id, States.last_updated) + if schema_version >= 31: + stmt += lambda q: q.order_by(States.entity_id, States.last_updated_ts) + else: + stmt += lambda q: q.order_by(States.entity_id, States.last_updated) if limit: stmt += lambda q: q.limit(limit) return stmt @@ -409,18 +524,29 @@ def _get_last_state_changes_stmt( stmt, join_attributes = lambda_stmt_and_join_attributes( schema_version, False, include_last_changed=False ) - stmt += lambda q: q.filter( - (States.last_changed == States.last_updated) | States.last_changed.is_(None) - ) + if schema_version >= 31: + stmt += lambda q: q.filter( + (States.last_changed_ts == States.last_updated_ts) + | States.last_changed_ts.is_(None) + ) + else: + stmt += lambda q: q.filter( + (States.last_changed == States.last_updated) | States.last_changed.is_(None) + ) if entity_id: stmt += lambda q: q.filter(States.entity_id == entity_id) if join_attributes: stmt += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id ) - stmt += lambda q: q.order_by(States.entity_id, States.last_updated.desc()).limit( - number_of_states - ) + if schema_version >= 31: + stmt += lambda q: q.order_by( + States.entity_id, States.last_updated_ts.desc() + ).limit(number_of_states) + else: + stmt += lambda q: q.order_by( + States.entity_id, States.last_updated.desc() + ).limit(number_of_states) return stmt @@ -463,19 +589,36 @@ def _get_states_for_entites_stmt( ) # We got an include-list of entities, accelerate the query by filtering already # in the inner query. - stmt += lambda q: q.where( - States.state_id - == ( - select(func.max(States.state_id).label("max_state_id")) - .filter( - (States.last_updated >= run_start) - & (States.last_updated < utc_point_in_time) - ) - .filter(States.entity_id.in_(entity_ids)) - .group_by(States.entity_id) - .subquery() - ).c.max_state_id - ) + if schema_version >= 31: + run_start_ts = process_timestamp(run_start).timestamp() + utc_point_in_time_ts = dt_util.utc_to_timestamp(utc_point_in_time) + stmt += lambda q: q.where( + States.state_id + == ( + select(func.max(States.state_id).label("max_state_id")) + .filter( + (States.last_updated_ts >= run_start_ts) + & (States.last_updated_ts < utc_point_in_time_ts) + ) + .filter(States.entity_id.in_(entity_ids)) + .group_by(States.entity_id) + .subquery() + ).c.max_state_id + ) + else: + stmt += lambda q: q.where( + States.state_id + == ( + select(func.max(States.state_id).label("max_state_id")) + .filter( + (States.last_updated >= run_start) + & (States.last_updated < utc_point_in_time) + ) + .filter(States.entity_id.in_(entity_ids)) + .group_by(States.entity_id) + .subquery() + ).c.max_state_id + ) if join_attributes: stmt += lambda q: q.outerjoin( StateAttributes, (States.attributes_id == StateAttributes.attributes_id) @@ -484,10 +627,26 @@ def _get_states_for_entites_stmt( def _generate_most_recent_states_by_date( + schema_version: int, run_start: datetime, utc_point_in_time: datetime, ) -> Subquery: """Generate the sub query for the most recent states by data.""" + if schema_version >= 31: + run_start_ts = process_timestamp(run_start).timestamp() + utc_point_in_time_ts = dt_util.utc_to_timestamp(utc_point_in_time) + return ( + select( + States.entity_id.label("max_entity_id"), + func.max(States.last_updated_ts).label("max_last_updated"), + ) + .filter( + (States.last_updated_ts >= run_start_ts) + & (States.last_updated_ts < utc_point_in_time_ts) + ) + .group_by(States.entity_id) + .subquery() + ) return ( select( States.entity_id.label("max_entity_id"), @@ -518,24 +677,42 @@ def _get_states_for_all_stmt( # This filtering can't be done in the inner query because the domain column is # not indexed and we can't control what's in the custom filter. most_recent_states_by_date = _generate_most_recent_states_by_date( - run_start, utc_point_in_time - ) - stmt += lambda q: q.where( - States.state_id - == ( - select(func.max(States.state_id).label("max_state_id")) - .join( - most_recent_states_by_date, - and_( - States.entity_id == most_recent_states_by_date.c.max_entity_id, - States.last_updated - == most_recent_states_by_date.c.max_last_updated, - ), - ) - .group_by(States.entity_id) - .subquery() - ).c.max_state_id, + schema_version, run_start, utc_point_in_time ) + if schema_version >= 31: + stmt += lambda q: q.where( + States.state_id + == ( + select(func.max(States.state_id).label("max_state_id")) + .join( + most_recent_states_by_date, + and_( + States.entity_id == most_recent_states_by_date.c.max_entity_id, + States.last_updated_ts + == most_recent_states_by_date.c.max_last_updated, + ), + ) + .group_by(States.entity_id) + .subquery() + ).c.max_state_id, + ) + else: + stmt += lambda q: q.where( + States.state_id + == ( + select(func.max(States.state_id).label("max_state_id")) + .join( + most_recent_states_by_date, + and_( + States.entity_id == most_recent_states_by_date.c.max_entity_id, + States.last_updated + == most_recent_states_by_date.c.max_last_updated, + ), + ) + .group_by(States.entity_id) + .subquery() + ).c.max_state_id, + ) stmt += _ignore_domains_filter if filters and filters.has_config: entity_filter = filters.states_entity_filter() @@ -598,14 +775,25 @@ def _get_single_entity_states_stmt( stmt, join_attributes = lambda_stmt_and_join_attributes( schema_version, no_attributes, include_last_changed=True ) - stmt += ( - lambda q: q.filter( - States.last_updated < utc_point_in_time, - States.entity_id == entity_id, + if schema_version >= 31: + utc_point_in_time_ts = dt_util.utc_to_timestamp(utc_point_in_time) + stmt += ( + lambda q: q.filter( + States.last_updated_ts < utc_point_in_time_ts, + States.entity_id == entity_id, + ) + .order_by(States.last_updated_ts.desc()) + .limit(1) + ) + else: + stmt += ( + lambda q: q.filter( + States.last_updated < utc_point_in_time, + States.entity_id == entity_id, + ) + .order_by(States.last_updated.desc()) + .limit(1) ) - .order_by(States.last_updated.desc()) - .limit(1) - ) if join_attributes: stmt += lambda q: q.outerjoin( StateAttributes, States.attributes_id == StateAttributes.attributes_id @@ -636,15 +824,24 @@ def _sorted_states_to_dict( each list of states, otherwise our graphs won't start on the Y axis correctly. """ + schema_version = _schema_version(hass) + _process_timestamp: Callable[[datetime], float | str] + state_class: Callable[ + [Row, dict[str, dict[str, Any]], datetime | None], State | dict[str, Any] + ] if compressed_state_format: - state_class = row_to_compressed_state - _process_timestamp: Callable[ - [datetime], float | str - ] = process_datetime_to_timestamp + if schema_version >= 31: + state_class = row_to_compressed_state + else: + state_class = row_to_compressed_state_pre_schema_31 + _process_timestamp = process_datetime_to_timestamp attr_time = COMPRESSED_STATE_LAST_UPDATED attr_state = COMPRESSED_STATE_STATE else: - state_class = LazyState # type: ignore[assignment] + if schema_version >= 31: + state_class = LazyState + else: + state_class = LazyStatePreSchema31 _process_timestamp = process_timestamp_to_utc_isoformat attr_time = LAST_CHANGED_KEY attr_state = STATE_KEY @@ -692,7 +889,9 @@ def _sorted_states_to_dict( ent_results.append(state_class(row, attr_cache, start_time)) if not minimal_response or split_entity_id(ent_id)[0] in NEED_ATTRIBUTE_DOMAINS: - ent_results.extend(state_class(db_state, attr_cache) for db_state in group) + ent_results.extend( + state_class(db_state, attr_cache, None) for db_state in group + ) continue # With minimal response we only provide a native @@ -703,26 +902,49 @@ def _sorted_states_to_dict( if (first_state := next(group, None)) is None: continue prev_state = first_state.state - ent_results.append(state_class(first_state, attr_cache)) + ent_results.append(state_class(first_state, attr_cache, None)) + + # + # minimal_response only makes sense with last_updated == last_updated + # + # We use last_updated for for last_changed since its the same + # + # With minimal response we do not care about attribute + # changes so we can filter out duplicate states + if schema_version < 31: + for row in group: + if (state := row.state) != prev_state: + ent_results.append( + { + attr_state: state, + attr_time: _process_timestamp(row.last_updated), + } + ) + prev_state = state + continue + + if compressed_state_format: + for row in group: + if (state := row.state) != prev_state: + ent_results.append( + { + attr_state: state, + attr_time: row.last_updated_ts, + } + ) + prev_state = state for row in group: - # With minimal response we do not care about attribute - # changes so we can filter out duplicate states - if (state := row.state) == prev_state: - continue - - ent_results.append( - { - attr_state: state, - # - # minimal_response only makes sense with last_updated == last_updated - # - # We use last_updated for for last_changed since its the same - # - attr_time: _process_timestamp(row.last_updated), - } - ) - prev_state = state + if (state := row.state) != prev_state: + ent_results.append( + { + attr_state: state, + attr_time: process_timestamp_to_utc_isoformat( + dt_util.utc_from_timestamp(row.last_updated_ts) + ), + } + ) + prev_state = state # If there are no states beyond the initial state, # the state a was never popped from initial_states diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 3fb873bfc90..c9e05cb17f2 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.4.44", "fnvhash==0.1.0"], + "requirements": ["sqlalchemy==1.4.45", "fnvhash==0.1.0"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal", "iot_class": "local_push", diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 5b4b3afb3d9..5b1f048aead 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING import sqlalchemy from sqlalchemy import ForeignKeyConstraint, MetaData, Table, func, text -from sqlalchemy.engine import Engine +from sqlalchemy.engine import CursorResult, Engine from sqlalchemy.exc import ( DatabaseError, InternalError, @@ -43,6 +43,7 @@ from .statistics import ( get_start_time, validate_db_schema as statistics_validate_db_schema, ) +from .tasks import CommitTask, PostSchemaMigrationTask from .util import session_scope if TYPE_CHECKING: @@ -54,7 +55,7 @@ _LOGGER = logging.getLogger(__name__) def raise_if_exception_missing_str(ex: Exception, match_substrs: Iterable[str]) -> None: - """Raise an exception if the exception and cause do not contain the match substrs.""" + """Raise if the exception and cause do not contain the match substrs.""" lower_ex_strs = [str(ex).lower(), str(ex.__cause__).lower()] for str_sub in match_substrs: for exc_str in lower_ex_strs: @@ -163,6 +164,12 @@ def migrate_schema( ) statistics_correct_db_schema(instance, engine, session_maker, schema_errors) + if current_version != SCHEMA_VERSION: + instance.queue_task(PostSchemaMigrationTask(current_version, SCHEMA_VERSION)) + # Make sure the post schema migration task is committed in case + # the next task does not have commit_before = True + instance.queue_task(CommitTask()) + def _create_index( session_maker: Callable[[], Session], table_name: str, index_name: str @@ -267,9 +274,13 @@ def _drop_index( "Finished dropping index %s from table %s", index_name, table_name ) else: - if index_name == "ix_states_context_parent_id": - # Was only there on nightly so we do not want + if index_name in ("ix_states_entity_id", "ix_states_context_parent_id"): + # ix_states_context_parent_id was only there on nightly so we do not want # to generate log noise or issues about it. + # + # ix_states_entity_id was only there for users who upgraded from schema + # version 8 or earlier. Newer installs will not have it so we do not + # want to generate log noise or issues about it. return _LOGGER.warning( @@ -492,14 +503,18 @@ def _apply_update( # noqa: C901 """Perform operations to bring schema up to date.""" dialect = engine.dialect.name big_int = "INTEGER(20)" if dialect == SupportedDialect.MYSQL else "INTEGER" + if dialect in (SupportedDialect.MYSQL, SupportedDialect.POSTGRESQL): + timestamp_type = "DOUBLE PRECISION" + else: + timestamp_type = "FLOAT" if new_version == 1: - _create_index(session_maker, "events", "ix_events_time_fired") + # This used to create ix_events_time_fired, but it was removed in version 32 + pass elif new_version == 2: # Create compound start/end index for recorder_runs _create_index(session_maker, "recorder_runs", "ix_recorder_runs_start_end") - # Create indexes for states - _create_index(session_maker, "states", "ix_states_last_updated") + # This used to create ix_states_last_updated bit it was removed in version 32 elif new_version == 3: # There used to be a new index here, but it was removed in version 4. pass @@ -518,8 +533,7 @@ def _apply_update( # noqa: C901 _drop_index(session_maker, "states", "states__state_changes") _drop_index(session_maker, "states", "states__significant_changes") _drop_index(session_maker, "states", "ix_states_entity_id_created") - - _create_index(session_maker, "states", "ix_states_entity_id_last_updated") + # This used to create ix_states_entity_id_last_updated, but it was removed in version 32 elif new_version == 5: # Create supporting index for States.event_id foreign key _create_index(session_maker, "states", "ix_states_event_id") @@ -530,20 +544,21 @@ def _apply_update( # noqa: C901 ["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"], ) _create_index(session_maker, "events", "ix_events_context_id") - _create_index(session_maker, "events", "ix_events_context_user_id") + # This used to create ix_events_context_user_id, but it was removed in version 28 _add_columns( session_maker, "states", ["context_id CHARACTER(36)", "context_user_id CHARACTER(36)"], ) _create_index(session_maker, "states", "ix_states_context_id") - _create_index(session_maker, "states", "ix_states_context_user_id") + # This used to create ix_states_context_user_id, but it was removed in version 28 elif new_version == 7: - _create_index(session_maker, "states", "ix_states_entity_id") + # There used to be a ix_states_entity_id index here, but it was removed in later schema + pass elif new_version == 8: _add_columns(session_maker, "events", ["context_parent_id CHARACTER(36)"]) _add_columns(session_maker, "states", ["old_state_id INTEGER"]) - _create_index(session_maker, "events", "ix_events_context_parent_id") + # This used to create ix_events_context_parent_id, but it was removed in version 28 elif new_version == 9: # We now get the context from events with a join # since its always there on state_changed events @@ -561,7 +576,7 @@ def _apply_update( # noqa: C901 # Redundant keys on composite index: # We already have ix_states_entity_id_last_updated _drop_index(session_maker, "states", "ix_states_entity_id") - _create_index(session_maker, "events", "ix_events_event_type_time_fired") + # This used to create ix_events_event_type_time_fired, but it was removed in version 32 _drop_index(session_maker, "events", "ix_events_event_type") elif new_version == 10: # Now done in step 11 @@ -657,7 +672,8 @@ def _apply_update( # noqa: C901 with session_scope(session=session_maker()) as session: connection = session.connection() connection.execute( - # Using LOCK=EXCLUSIVE to prevent the database from corrupting + # Using LOCK=EXCLUSIVE to prevent + # the database from corrupting # https://github.com/home-assistant/core/issues/56104 text( f"ALTER TABLE {table} CONVERT TO CHARACTER SET utf8mb4" @@ -798,7 +814,8 @@ def _apply_update( # noqa: C901 with contextlib.suppress(SQLAlchemyError): with session_scope(session=session_maker()) as session: connection = session.connection() - # This is safe to run multiple times and fast since the table is small + # This is safe to run multiple times and fast + # since the table is small. connection.execute( text("ALTER TABLE statistics_meta ROW_FORMAT=DYNAMIC") ) @@ -821,12 +838,203 @@ def _apply_update( # noqa: C901 # Once we require SQLite >= 3.35.5, we should drop the column: # ALTER TABLE statistics_meta DROP COLUMN state_unit_of_measurement pass + elif new_version == 31: + # Once we require SQLite >= 3.35.5, we should drop the column: + # ALTER TABLE events DROP COLUMN time_fired + # ALTER TABLE states DROP COLUMN last_updated + # ALTER TABLE states DROP COLUMN last_changed + _add_columns(session_maker, "events", [f"time_fired_ts {timestamp_type}"]) + _add_columns( + session_maker, + "states", + [f"last_updated_ts {timestamp_type}", f"last_changed_ts {timestamp_type}"], + ) + _create_index(session_maker, "events", "ix_events_time_fired_ts") + _create_index(session_maker, "events", "ix_events_event_type_time_fired_ts") + _create_index(session_maker, "states", "ix_states_entity_id_last_updated_ts") + _create_index(session_maker, "states", "ix_states_last_updated_ts") + _migrate_columns_to_timestamp(session_maker, engine) + elif new_version == 32: + # Migration is done in two steps to ensure we can start using + # the new columns before we wipe the old ones. + _drop_index(session_maker, "states", "ix_states_entity_id_last_updated") + _drop_index(session_maker, "events", "ix_events_event_type_time_fired") + _drop_index(session_maker, "states", "ix_states_last_updated") + _drop_index(session_maker, "events", "ix_events_time_fired") + elif new_version == 33: + # This index is no longer used and can cause MySQL to use the wrong index + # when querying the states table. + # https://github.com/home-assistant/core/issues/83787 + _drop_index(session_maker, "states", "ix_states_entity_id") else: raise ValueError(f"No schema migration defined for version {new_version}") +def post_schema_migration( + engine: Engine, + session: Session, + old_version: int, + new_version: int, +) -> None: + """Post schema migration. + + Run any housekeeping tasks after the schema migration has completed. + + Post schema migration is run after the schema migration has completed + and the queue has been processed to ensure that we reduce the memory + pressure since events are held in memory until the queue is processed + which is blocked from being processed until the schema migration is + complete. + """ + if old_version < 32 <= new_version: + # In version 31 we migrated all the time_fired, last_updated, and last_changed + # columns to be timestamps. In version 32 we need to wipe the old columns + # since they are no longer used and take up a significant amount of space. + _wipe_old_string_time_columns(engine, session) + + +def _wipe_old_string_time_columns(engine: Engine, session: Session) -> None: + """Wipe old string time columns to save space.""" + # Wipe Events.time_fired since its been replaced by Events.time_fired_ts + # Wipe States.last_updated since its been replaced by States.last_updated_ts + # Wipe States.last_changed since its been replaced by States.last_changed_ts + # + if engine.dialect.name == SupportedDialect.SQLITE: + session.execute(text("UPDATE events set time_fired=NULL;")) + session.commit() + session.execute(text("UPDATE states set last_updated=NULL, last_changed=NULL;")) + session.commit() + elif engine.dialect.name == SupportedDialect.MYSQL: + # + # Since this is only to save space we limit the number of rows we update + # to 10,000,000 per table since we do not want to block the database for too long + # or run out of innodb_buffer_pool_size on MySQL. The old data will eventually + # be cleaned up by the recorder purge if we do not do it now. + # + session.execute(text("UPDATE events set time_fired=NULL LIMIT 10000000;")) + session.commit() + session.execute( + text( + "UPDATE states set last_updated=NULL, last_changed=NULL " + " LIMIT 10000000;" + ) + ) + session.commit() + elif engine.dialect.name == SupportedDialect.POSTGRESQL: + # + # Since this is only to save space we limit the number of rows we update + # to 250,000 per table since we do not want to block the database for too long + # or run out ram with postgresql. The old data will eventually + # be cleaned up by the recorder purge if we do not do it now. + # + session.execute( + text( + "UPDATE events set time_fired=NULL " + "where event_id in " + "(select event_id from events where time_fired_ts is NOT NULL LIMIT 250000);" + ) + ) + session.commit() + session.execute( + text( + "UPDATE states set last_updated=NULL, last_changed=NULL " + "where state_id in " + "(select state_id from states where last_updated_ts is NOT NULL LIMIT 250000);" + ) + ) + session.commit() + + +def _migrate_columns_to_timestamp( + session_maker: Callable[[], Session], engine: Engine +) -> None: + """Migrate columns to use timestamp.""" + # Migrate all data in Events.time_fired to Events.time_fired_ts + # Migrate all data in States.last_updated to States.last_updated_ts + # Migrate all data in States.last_changed to States.last_changed_ts + result: CursorResult | None = None + if engine.dialect.name == SupportedDialect.SQLITE: + # With SQLite we do this in one go since it is faster + with session_scope(session=session_maker()) as session: + connection = session.connection() + connection.execute( + text( + 'UPDATE events set time_fired_ts=strftime("%s",time_fired) + ' + "cast(substr(time_fired,-7) AS FLOAT);" + ) + ) + connection.execute( + text( + 'UPDATE states set last_updated_ts=strftime("%s",last_updated) + ' + "cast(substr(last_updated,-7) AS FLOAT), " + 'last_changed_ts=strftime("%s",last_changed) + ' + "cast(substr(last_changed,-7) AS FLOAT);" + ) + ) + elif engine.dialect.name == SupportedDialect.MYSQL: + # With MySQL we do this in chunks to avoid hitting the `innodb_buffer_pool_size` limit + # We also need to do this in a loop since we can't be sure that we have + # updated all rows in the table until the rowcount is 0 + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + result = session.connection().execute( + text( + "UPDATE events set time_fired_ts=" + "IF(time_fired is NULL,0," + "UNIX_TIMESTAMP(CONVERT_TZ(time_fired,'+00:00',@@global.time_zone))" + ") " + "where time_fired_ts is NULL " + "LIMIT 250000;" + ) + ) + result = None + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + result = session.connection().execute( + text( + "UPDATE states set last_updated_ts=" + "IF(last_updated is NULL,0," + "UNIX_TIMESTAMP(CONVERT_TZ(last_updated,'+00:00',@@global.time_zone)) " + "), " + "last_changed_ts=" + "UNIX_TIMESTAMP(CONVERT_TZ(last_changed,'+00:00',@@global.time_zone)) " + "where last_updated_ts is NULL " + "LIMIT 250000;" + ) + ) + elif engine.dialect.name == SupportedDialect.POSTGRESQL: + # With Postgresql we do this in chunks to avoid using too much memory + # We also need to do this in a loop since we can't be sure that we have + # updated all rows in the table until the rowcount is 0 + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + result = session.connection().execute( + text( + "UPDATE events SET " + "time_fired_ts= " + "(case when time_fired is NULL then 0 else EXTRACT(EPOCH FROM time_fired) end) " + "WHERE event_id IN ( " + "SELECT event_id FROM events where time_fired_ts is NULL LIMIT 250000 " + " );" + ) + ) + result = None + while result is None or result.rowcount > 0: + with session_scope(session=session_maker()) as session: + result = session.connection().execute( + text( + "UPDATE states set last_updated_ts=" + "(case when last_updated is NULL then 0 else EXTRACT(EPOCH FROM last_updated) end), " + "last_changed_ts=EXTRACT(EPOCH FROM last_changed) " + "where state_id IN ( " + "SELECT state_id FROM states where last_updated_ts is NULL LIMIT 250000 " + " );" + ) + ) + + def _initialize_database(session: Session) -> bool: - """Initialize a new database, or a database created before introducing schema changes. + """Initialize a new database. The function determines the schema version by inspecting the db structure. @@ -840,7 +1048,7 @@ def _initialize_database(session: Session) -> bool: indexes = inspector.get_indexes("events") for index in indexes: - if index["column_names"] == ["time_fired"]: + if index["column_names"] in (["time_fired"], ["time_fired_ts"]): # Schema addition from version 1 detected. New DB. session.add(StatisticsRuns(start=get_start_time())) session.add(SchemaChanges(schema_version=SCHEMA_VERSION)) @@ -853,7 +1061,7 @@ def _initialize_database(session: Session) -> bool: def initialize_database(session_maker: Callable[[], Session]) -> bool: - """Initialize a new database, or a database created before introducing schema changes.""" + """Initialize a new database.""" try: with session_scope(session=session_maker()) as session: if _get_schema_version(session) is not None: diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 48b45b4da2e..3bbd9f173a3 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -7,7 +7,7 @@ from typing import Any, Literal, TypedDict, overload from sqlalchemy.engine.row import Row -from homeassistant.components.websocket_api import ( +from homeassistant.const import ( COMPRESSED_STATE_ATTRIBUTES, COMPRESSED_STATE_LAST_CHANGED, COMPRESSED_STATE_LAST_UPDATED, @@ -120,8 +120,8 @@ def process_datetime_to_timestamp(ts: datetime) -> float: return ts.timestamp() -class LazyState(State): - """A lazy version of core State.""" +class LazyStatePreSchema31(State): + """A lazy version of core State before schema 31.""" __slots__ = [ "_row", @@ -136,7 +136,7 @@ class LazyState(State): self, row: Row, attr_cache: dict[str, dict[str, Any]], - start_time: datetime | None = None, + start_time: datetime | None, ) -> None: """Init the lazy state.""" self._row = row @@ -243,6 +243,114 @@ class LazyState(State): ) +class LazyState(State): + """A lazy version of core State after schema 31.""" + + __slots__ = [ + "_row", + "_attributes", + "_last_changed_ts", + "_last_updated_ts", + "_context", + "attr_cache", + ] + + def __init__( # pylint: disable=super-init-not-called + self, + row: Row, + attr_cache: dict[str, dict[str, Any]], + start_time: datetime | None, + ) -> None: + """Init the lazy state.""" + self._row = row + self.entity_id: str = self._row.entity_id + self.state = self._row.state or "" + self._attributes: dict[str, Any] | None = None + self._last_updated_ts: float | None = self._row.last_updated_ts or ( + dt_util.utc_to_timestamp(start_time) if start_time else None + ) + self._last_changed_ts: float | None = ( + self._row.last_changed_ts or self._last_updated_ts + ) + self._context: Context | None = None + self.attr_cache = attr_cache + + @property # type: ignore[override] + def attributes(self) -> dict[str, Any]: + """State attributes.""" + if self._attributes is None: + self._attributes = decode_attributes_from_row(self._row, self.attr_cache) + return self._attributes + + @attributes.setter + def attributes(self, value: dict[str, Any]) -> None: + """Set attributes.""" + self._attributes = value + + @property + def context(self) -> Context: + """State context.""" + if self._context is None: + self._context = Context(id=None) + return self._context + + @context.setter + def context(self, value: Context) -> None: + """Set context.""" + self._context = value + + @property + def last_changed(self) -> datetime: + """Last changed datetime.""" + assert self._last_changed_ts is not None + return dt_util.utc_from_timestamp(self._last_changed_ts) + + @last_changed.setter + def last_changed(self, value: datetime) -> None: + """Set last changed datetime.""" + self._last_changed_ts = process_timestamp(value).timestamp() + + @property + def last_updated(self) -> datetime: + """Last updated datetime.""" + assert self._last_updated_ts is not None + return dt_util.utc_from_timestamp(self._last_updated_ts) + + @last_updated.setter + def last_updated(self, value: datetime) -> None: + """Set last updated datetime.""" + self._last_updated_ts = process_timestamp(value).timestamp() + + def as_dict(self) -> dict[str, Any]: # type: ignore[override] + """Return a dict representation of the LazyState. + + Async friendly. + + To be used for JSON serialization. + """ + last_updated_isoformat = self.last_updated.isoformat() + if self._last_changed_ts == self._last_updated_ts: + last_changed_isoformat = last_updated_isoformat + else: + last_changed_isoformat = self.last_changed.isoformat() + return { + "entity_id": self.entity_id, + "state": self.state, + "attributes": self._attributes or self.attributes, + "last_changed": last_changed_isoformat, + "last_updated": last_updated_isoformat, + } + + def __eq__(self, other: Any) -> bool: + """Return the comparison.""" + return ( + other.__class__ in [self.__class__, State] + and self.entity_id == other.entity_id + and self.state == other.state + and self.attributes == other.attributes + ) + + def decode_attributes_from_row( row: Row, attr_cache: dict[str, dict[str, Any]] ) -> dict[str, Any]: @@ -263,9 +371,31 @@ def decode_attributes_from_row( def row_to_compressed_state( row: Row, attr_cache: dict[str, dict[str, Any]], - start_time: datetime | None = None, + start_time: datetime | None, ) -> dict[str, Any]: - """Convert a database row to a compressed state.""" + """Convert a database row to a compressed state schema 31 and later.""" + comp_state = { + COMPRESSED_STATE_STATE: row.state, + COMPRESSED_STATE_ATTRIBUTES: decode_attributes_from_row(row, attr_cache), + } + if start_time: + comp_state[COMPRESSED_STATE_LAST_UPDATED] = dt_util.utc_to_timestamp(start_time) + else: + row_last_updated_ts: float = row.last_updated_ts + comp_state[COMPRESSED_STATE_LAST_UPDATED] = row_last_updated_ts + if ( + row_changed_changed_ts := row.last_changed_ts + ) and row_last_updated_ts != row_changed_changed_ts: + comp_state[COMPRESSED_STATE_LAST_CHANGED] = row_changed_changed_ts + return comp_state + + +def row_to_compressed_state_pre_schema_31( + row: Row, + attr_cache: dict[str, dict[str, Any]], + start_time: datetime | None, +) -> dict[str, Any]: + """Convert a database row to a compressed state before schema 31.""" comp_state = { COMPRESSED_STATE_STATE: row.state, COMPRESSED_STATE_ATTRIBUTES: decode_attributes_from_row(row, attr_cache), diff --git a/homeassistant/components/recorder/pool.py b/homeassistant/components/recorder/pool.py index 4a0fbddc10b..d839f183125 100644 --- a/homeassistant/components/recorder/pool.py +++ b/homeassistant/components/recorder/pool.py @@ -131,7 +131,7 @@ class MutexPool(StaticPool): # type: ignore[misc] if DEBUG_MUTEX_POOL: _LOGGER.debug("%s wait conn%s", threading.current_thread().name, trace_msg) # pylint: disable-next=consider-using-with - got_lock = MutexPool.pool_lock.acquire(timeout=1) + got_lock = MutexPool.pool_lock.acquire(timeout=10) if not got_lock: raise SQLAlchemyError conn = super()._do_get() diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 30f4d34e331..01b10ea966a 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -12,6 +12,7 @@ from sqlalchemy.orm.session import Session from sqlalchemy.sql.expression import distinct from homeassistant.const import EVENT_STATE_CHANGED +import homeassistant.util.dt as dt_util from .const import MAX_ROWS_TO_PURGE, SupportedDialect from .db_schema import Events, StateAttributes, States @@ -233,7 +234,9 @@ def _select_state_attributes_ids_to_purge( """Return sets of state and attribute ids to purge.""" state_ids = set() attributes_ids = set() - for state in session.execute(find_states_to_purge(purge_before)).all(): + for state in session.execute( + find_states_to_purge(dt_util.utc_to_timestamp(purge_before)) + ).all(): state_ids.add(state.state_id) if state.attributes_id: attributes_ids.add(state.attributes_id) @@ -251,7 +254,9 @@ def _select_event_data_ids_to_purge( """Return sets of event and data ids to purge.""" event_ids = set() data_ids = set() - for event in session.execute(find_events_to_purge(purge_before)).all(): + for event in session.execute( + find_events_to_purge(dt_util.utc_to_timestamp(purge_before)) + ).all(): event_ids.add(event.event_id) if event.data_id: data_ids.add(event.data_id) @@ -264,21 +269,22 @@ def _select_event_data_ids_to_purge( def _select_unused_attributes_ids( session: Session, attributes_ids: set[int], using_sqlite: bool ) -> set[int]: - """Return a set of attributes ids that are not used by any states in the database.""" + """Return a set of attributes ids that are not used by any states in the db.""" if not attributes_ids: return set() if using_sqlite: # - # SQLite has a superior query optimizer for the distinct query below as it uses the - # covering index without having to examine the rows directly for both of the queries - # below. + # SQLite has a superior query optimizer for the distinct query below as it uses + # the covering index without having to examine the rows directly for both of the + # queries below. # # We use the distinct query for SQLite since the query in the other branch can # generate more than 500 unions which SQLite does not support. # # How MariaDB's query optimizer handles this query: - # > explain select distinct attributes_id from states where attributes_id in (136723); + # > explain select distinct attributes_id from states where attributes_id in + # (136723); # ...Using index # seen_ids = { @@ -289,15 +295,16 @@ def _select_unused_attributes_ids( } else: # - # This branch is for DBMS that cannot optimize the distinct query well and has to examine - # all the rows that match. + # This branch is for DBMS that cannot optimize the distinct query well and has + # to examine all the rows that match. # - # This branch uses a union of simple queries, as each query is optimized away as the answer - # to the query can be found in the index. + # This branch uses a union of simple queries, as each query is optimized away + # as the answer to the query can be found in the index. # - # The below query works for SQLite as long as there are no more than 500 attributes_id - # to be selected. We currently do not have MySQL or PostgreSQL servers running in the - # test suite; we test this path using SQLite when there are less than 500 attributes_id. + # The below query works for SQLite as long as there are no more than 500 + # attributes_id to be selected. We currently do not have MySQL or PostgreSQL + # servers running in the test suite; we test this path using SQLite when there + # are less than 500 attributes_id. # # How MariaDB's query optimizer handles this query: # > explain select min(attributes_id) from states where attributes_id = 136723; @@ -344,7 +351,7 @@ def _purge_unused_attributes_ids( def _select_unused_event_data_ids( session: Session, data_ids: set[int], using_sqlite: bool ) -> set[int]: - """Return a set of event data ids that are not used by any events in the database.""" + """Return a set of event data ids that are not used by any events in the db.""" if not data_ids: return set() @@ -386,7 +393,10 @@ def _purge_unused_data_ids( def _select_statistics_runs_to_purge( session: Session, purge_before: datetime ) -> list[int]: - """Return a list of statistic runs to purge, but take care to keep the newest run.""" + """Return a list of statistic runs to purge. + + Takes care to keep the newest run. + """ statistic_runs = session.execute(find_statistics_runs_to_purge(purge_before)).all() statistic_runs_list = [run.run_id for run in statistic_runs] # Exclude the newest statistics run @@ -413,14 +423,16 @@ def _select_short_term_statistics_to_purge( def _select_legacy_event_state_and_attributes_and_data_ids_to_purge( session: Session, purge_before: datetime ) -> tuple[set[int], set[int], set[int], set[int]]: - """Return a list of event, state, and attribute ids to purge that are linked by the event_id. + """Return a list of event, state, and attribute ids to purge linked by the event_id. We do not link these anymore since state_change events do not exist in the events table anymore, however we still need to be able to purge them. """ events = session.execute( - find_legacy_event_state_and_attributes_and_data_ids_to_purge(purge_before) + find_legacy_event_state_and_attributes_and_data_ids_to_purge( + dt_util.utc_to_timestamp(purge_before) + ) ).all() _LOGGER.debug("Selected %s event ids to remove", len(events)) event_ids = set() @@ -669,7 +681,8 @@ def purge_entity_data(instance: Recorder, entity_filter: Callable[[str], bool]) ] _LOGGER.debug("Purging entity data for %s", selected_entity_ids) if len(selected_entity_ids) > 0: - # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states or events record + # Purge a max of MAX_ROWS_TO_PURGE, based on the oldest states + # or events record. _purge_filtered_states(instance, session, selected_entity_ids, using_sqlite) _LOGGER.debug("Purging entity data hasn't fully completed yet") return False diff --git a/homeassistant/components/recorder/queries.py b/homeassistant/components/recorder/queries.py index 4b4488d4dad..0591fda4713 100644 --- a/homeassistant/components/recorder/queries.py +++ b/homeassistant/components/recorder/queries.py @@ -578,20 +578,20 @@ def delete_recorder_runs_rows( ) -def find_events_to_purge(purge_before: datetime) -> StatementLambdaElement: +def find_events_to_purge(purge_before: float) -> StatementLambdaElement: """Find events to purge.""" return lambda_stmt( lambda: select(Events.event_id, Events.data_id) - .filter(Events.time_fired < purge_before) + .filter(Events.time_fired_ts < purge_before) .limit(MAX_ROWS_TO_PURGE) ) -def find_states_to_purge(purge_before: datetime) -> StatementLambdaElement: +def find_states_to_purge(purge_before: float) -> StatementLambdaElement: """Find states to purge.""" return lambda_stmt( lambda: select(States.state_id, States.attributes_id) - .filter(States.last_updated < purge_before) + .filter(States.last_updated_ts < purge_before) .limit(MAX_ROWS_TO_PURGE) ) @@ -624,7 +624,7 @@ def find_latest_statistics_runs_run_id() -> StatementLambdaElement: def find_legacy_event_state_and_attributes_and_data_ids_to_purge( - purge_before: datetime, + purge_before: float, ) -> StatementLambdaElement: """Find the latest row in the legacy format to purge.""" return lambda_stmt( @@ -632,7 +632,7 @@ def find_legacy_event_state_and_attributes_and_data_ids_to_purge( Events.event_id, Events.data_id, States.state_id, States.attributes_id ) .outerjoin(States, Events.event_id == States.event_id) - .filter(Events.time_fired < purge_before) + .filter(Events.time_fired_ts < purge_before) .limit(MAX_ROWS_TO_PURGE) ) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index fab4a67a749..4a39abd9f63 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -43,6 +43,7 @@ from homeassistant.util.unit_conversion import ( PressureConverter, SpeedConverter, TemperatureConverter, + UnitlessRatioConverter, VolumeConverter, ) @@ -134,6 +135,7 @@ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { **{unit: PressureConverter for unit in PressureConverter.VALID_UNITS}, **{unit: SpeedConverter for unit in SpeedConverter.VALID_UNITS}, **{unit: TemperatureConverter for unit in TemperatureConverter.VALID_UNITS}, + **{unit: UnitlessRatioConverter for unit in UnitlessRatioConverter.VALID_UNITS}, **{unit: VolumeConverter for unit in VolumeConverter.VALID_UNITS}, } @@ -155,9 +157,6 @@ def get_display_unit( ) -> str | None: """Return the unit which the statistic will be displayed in.""" - if statistic_unit is None: - return None - if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None: return statistic_unit @@ -183,9 +182,6 @@ def _get_statistic_to_display_unit_converter( """Return val.""" return val - if statistic_unit is None: - return no_conversion - if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None: return no_conversion @@ -226,9 +222,6 @@ def _get_display_to_statistic_unit_converter( """Return val.""" return val - if statistic_unit is None: - return no_conversion - if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None: return no_conversion @@ -1555,17 +1548,10 @@ def statistic_during_period( else: result["change"] = None - def no_conversion(val: float | None) -> float | None: - """Return val.""" - return val - state_unit = unit = metadata[statistic_id][1]["unit_of_measurement"] if state := hass.states.get(statistic_id): state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if unit is not None: - convert = _get_statistic_to_display_unit_converter(unit, state_unit, units) - else: - convert = no_conversion + convert = _get_statistic_to_display_unit_converter(unit, state_unit, units) return {key: convert(value) for key, value in result.items()} @@ -1686,7 +1672,10 @@ def _get_last_statistics_short_term_stmt( metadata_id: int, number_of_stats: int, ) -> StatementLambdaElement: - """Generate a statement for number_of_stats short term statistics for a given statistic_id.""" + """Generate a statement for number_of_stats short term statistics. + + For a given statistic_id. + """ return lambda_stmt( lambda: select(*QUERY_STATISTICS_SHORT_TERM) .filter_by(metadata_id=metadata_id) @@ -1895,7 +1884,10 @@ def _sorted_statistics_to_dict( 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[no-any-return] + 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) @@ -1911,12 +1903,15 @@ def _sorted_statistics_to_dict( 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[no-any-return] + for meta_id, group in groupby( + stats, + lambda stat: stat.metadata_id, # type: ignore[no-any-return] + ): state_unit = unit = metadata[meta_id]["unit_of_measurement"] statistic_id = metadata[meta_id]["statistic_id"] if state := hass.states.get(statistic_id): state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if unit is not None and convert_units: + if convert_units: convert = _get_statistic_to_display_unit_converter(unit, state_unit, units) else: convert = no_conversion @@ -1978,7 +1973,7 @@ def _async_import_statistics( metadata: StatisticMetaData, statistics: Iterable[StatisticData], ) -> None: - """Validate timestamps and insert an import_statistics job in the recorder's queue.""" + """Validate timestamps and insert an import_statistics job in the queue.""" for statistic in statistics: start = statistic["start"] if start.tzinfo is None or start.tzinfo.utcoffset(start) is None: diff --git a/homeassistant/components/recorder/strings.json b/homeassistant/components/recorder/strings.json index 9b616372adf..7af67f10e25 100644 --- a/homeassistant/components/recorder/strings.json +++ b/homeassistant/components/recorder/strings.json @@ -7,5 +7,11 @@ "database_engine": "Database Engine", "database_version": "Database Version" } + }, + "issues": { + "maria_db_range_index_regression": { + "title": "Update MariaDB to {min_version} or later resolve a significant performance issue", + "description": "Older versions of MariaDB suffer from a significant performance regression when retrieving history data or purging the database. Update to MariaDB version {min_version} or later and restart Home Assistant. If you are using the MariaDB core add-on, make sure to update it to the latest version." + } } } diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index 01723a50960..63dc8e9d2e3 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -121,7 +121,10 @@ class PurgeEntitiesTask(RecorderTask): @dataclass class PerodicCleanupTask(RecorderTask): - """An object to insert into the recorder to trigger cleanup tasks when auto purge is disabled.""" + """An object to insert into the recorder to trigger cleanup tasks. + + Trigger cleanup tasks when auto purge is disabled. + """ def run(self, instance: Recorder) -> None: """Handle the task.""" @@ -195,7 +198,10 @@ class AdjustStatisticsTask(RecorderTask): @dataclass class WaitTask(RecorderTask): - """An object to insert into the recorder queue to tell it set the _queue_watch event.""" + """An object to insert into the recorder queue. + + Tell it set the _queue_watch event. + """ commit_before = False @@ -297,3 +303,17 @@ class SynchronizeTask(RecorderTask): # Does not use a tracked task to avoid # blocking shutdown if the recorder is broken instance.hass.loop.call_soon_threadsafe(self.event.set) + + +@dataclass +class PostSchemaMigrationTask(RecorderTask): + """Post migration task to update schema.""" + + old_version: int + new_version: int + + def run(self, instance: Recorder) -> None: + """Handle the task.""" + instance._post_schema_migration( # pylint: disable=[protected-access] + self.old_version, self.new_version + ) diff --git a/homeassistant/components/recorder/translations/en.json b/homeassistant/components/recorder/translations/en.json index c9ceffc7397..30c17b854ca 100644 --- a/homeassistant/components/recorder/translations/en.json +++ b/homeassistant/components/recorder/translations/en.json @@ -1,4 +1,10 @@ { + "issues": { + "maria_db_range_index_regression": { + "description": "Older versions of MariaDB suffer from a significant performance regression when retrieving history data or purging the database. Update to MariaDB version {min_version} or later and restart Home Assistant. If you are using the MariaDB core add-on, make sure to update it to the latest version.", + "title": "Update MariaDB to {min_version} or later resolve a significant performance issue" + } + }, "system_health": { "info": { "current_recorder_run": "Current Run Start Time", diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index ffef4d64773..0469a71009a 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -8,7 +8,7 @@ import functools import logging import os import time -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any, Concatenate, NoReturn, ParamSpec, TypeVar from awesomeversion import ( AwesomeVersion, @@ -23,14 +23,13 @@ from sqlalchemy.exc import OperationalError, SQLAlchemyError from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session from sqlalchemy.sql.lambdas import StatementLambdaElement -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, issue_registry as ir import homeassistant.util.dt as dt_util -from .const import DATA_INSTANCE, SQLITE_URL_PREFIX, SupportedDialect +from .const import DATA_INSTANCE, DOMAIN, SQLITE_URL_PREFIX, SupportedDialect from .db_schema import ( TABLE_RECORDER_RUNS, TABLE_SCHEMA_CHANGES, @@ -52,9 +51,35 @@ QUERY_RETRY_WAIT = 0.1 SQLITE3_POSTFIXES = ["", "-wal", "-shm"] DEFAULT_YIELD_STATES_ROWS = 32768 +# Our minimum versions for each database +# +# Older MariaDB suffers https://jira.mariadb.org/browse/MDEV-25020 +# which is fixed in 10.5.17, 10.6.9, 10.7.5, 10.8.4 +# MIN_VERSION_MARIA_DB = AwesomeVersion( "10.3.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER ) +RECOMMENDED_MIN_VERSION_MARIA_DB = AwesomeVersion( + "10.5.17", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +MARIA_DB_106 = AwesomeVersion( + "10.6.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +RECOMMENDED_MIN_VERSION_MARIA_DB_106 = AwesomeVersion( + "10.6.9", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +MARIA_DB_107 = AwesomeVersion( + "10.7.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +RECOMMENDED_MIN_VERSION_MARIA_DB_107 = AwesomeVersion( + "10.7.5", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +MARIA_DB_108 = AwesomeVersion( + "10.8.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) +RECOMMENDED_MIN_VERSION_MARIA_DB_108 = AwesomeVersion( + "10.8.4", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER +) MIN_VERSION_MYSQL = AwesomeVersion( "8.0.0", ensure_strategy=AwesomeVersionStrategy.SIMPLEVER ) @@ -196,9 +221,11 @@ def execute_stmt_lambda_element( """ executed = session.execute(stmt) use_all = not start_time or ((end_time or dt_util.utcnow()) - start_time).days <= 1 - for tryno in range(0, RETRIES): + for tryno in range(RETRIES): try: - return executed.all() if use_all else executed.yield_per(yield_per) # type: ignore[no-any-return] + if use_all: + return executed.all() # type: ignore[no-any-return] + return executed.yield_per(yield_per) # type: ignore[no-any-return] except SQLAlchemyError as err: _LOGGER.error("Error executing query: %s", err) if tryno == RETRIES - 1: @@ -225,7 +252,7 @@ def validate_or_move_away_sqlite_database(dburl: str) -> bool: def dburl_to_path(dburl: str) -> str: """Convert the db url into a filesystem path.""" - return dburl[len(SQLITE_URL_PREFIX) :] + return dburl.removeprefix(SQLITE_URL_PREFIX) def last_run_was_recently_clean(cursor: CursorFetchStrategy) -> bool: @@ -341,7 +368,7 @@ def query_on_connection(dbapi_connection: Any, statement: str) -> Any: return result -def _fail_unsupported_dialect(dialect_name: str) -> None: +def _fail_unsupported_dialect(dialect_name: str) -> NoReturn: """Warn about unsupported database version.""" _LOGGER.error( ( @@ -357,7 +384,7 @@ def _fail_unsupported_dialect(dialect_name: str) -> None: def _fail_unsupported_version( server_version: str, dialect_name: str, minimum_version: str -) -> None: +) -> NoReturn: """Warn about unsupported database version.""" _LOGGER.error( ( @@ -400,16 +427,41 @@ def _datetime_or_none(value: str) -> datetime | None: def build_mysqldb_conv() -> dict: """Build a MySQLDB conv dict that uses cisco8601 to parse datetimes.""" # Late imports since we only call this if they are using mysqldb - from MySQLdb.constants import ( # pylint: disable=import-outside-toplevel,import-error - FIELD_TYPE, - ) - from MySQLdb.converters import ( # pylint: disable=import-outside-toplevel,import-error - conversions, - ) + # pylint: disable=import-outside-toplevel,import-error + from MySQLdb.constants import FIELD_TYPE + from MySQLdb.converters import conversions return {**conversions, FIELD_TYPE.DATETIME: _datetime_or_none} +@callback +def _async_create_mariadb_range_index_regression_issue( + hass: HomeAssistant, version: AwesomeVersion +) -> None: + """Create an issue for the index range regression in older MariaDB. + + The range scan issue was fixed in MariaDB 10.5.17, 10.6.9, 10.7.5, 10.8.4 and later. + """ + if version >= MARIA_DB_108: + min_version = RECOMMENDED_MIN_VERSION_MARIA_DB_108 + elif version >= MARIA_DB_107: + min_version = RECOMMENDED_MIN_VERSION_MARIA_DB_107 + elif version >= MARIA_DB_106: + min_version = RECOMMENDED_MIN_VERSION_MARIA_DB_106 + else: + min_version = RECOMMENDED_MIN_VERSION_MARIA_DB + ir.async_create_issue( + hass, + DOMAIN, + "maria_db_range_index_regression", + is_fixable=False, + severity=ir.IssueSeverity.CRITICAL, + learn_more_url="https://jira.mariadb.org/browse/MDEV-25020", + translation_key="maria_db_range_index_regression", + translation_placeholders={"min_version": str(min_version)}, + ) + + def setup_connection_for_dialect( instance: Recorder, dialect_name: str, @@ -444,7 +496,8 @@ def setup_connection_for_dialect( # or NORMAL if they do not. # # https://sqlite.org/pragma.html#pragma_synchronous - # The synchronous=NORMAL setting is a good choice for most applications running in WAL mode. + # The synchronous=NORMAL setting is a good choice for most applications + # running in WAL mode. # synchronous = "NORMAL" if instance.commit_interval else "FULL" execute_on_connection(dbapi_connection, f"PRAGMA synchronous={synchronous}") @@ -465,6 +518,18 @@ def setup_connection_for_dialect( _fail_unsupported_version( version or version_string, "MariaDB", MIN_VERSION_MARIA_DB ) + if version and ( + (version < RECOMMENDED_MIN_VERSION_MARIA_DB) + or (MARIA_DB_106 <= version < RECOMMENDED_MIN_VERSION_MARIA_DB_106) + or (MARIA_DB_107 <= version < RECOMMENDED_MIN_VERSION_MARIA_DB_107) + or (MARIA_DB_108 <= version < RECOMMENDED_MIN_VERSION_MARIA_DB_108) + ): + instance.hass.add_job( + _async_create_mariadb_range_index_regression_issue, + instance.hass, + version, + ) + else: if not version or version < MIN_VERSION_MYSQL: _fail_unsupported_version( diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 35879bfc076..c63e5bc43ca 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -242,7 +242,10 @@ def _ws_get_list_statistic_ids( msg_id: int, statistic_type: Literal["mean"] | Literal["sum"] | None = None, ) -> str: - """Fetch a list of available statistic_id and convert them to json in the executor.""" + """Fetch a list of available statistic_id and convert them to JSON. + + Runs in the executor. + """ return JSON_DUMP( messages.result_message(msg_id, list_statistic_ids(hass, None, statistic_type)) ) diff --git a/homeassistant/components/renault/button.py b/homeassistant/components/renault/button.py index 71d197ac335..b34e14d365a 100644 --- a/homeassistant/components/renault/button.py +++ b/homeassistant/components/renault/button.py @@ -1,8 +1,9 @@ """Support for Renault button entities.""" from __future__ import annotations -from collections.abc import Awaitable, Callable +from collections.abc import Callable, Coroutine from dataclasses import dataclass +from typing import Any from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.config_entries import ConfigEntry @@ -18,7 +19,7 @@ from .renault_hub import RenaultHub class RenaultButtonRequiredKeysMixin: """Mixin for required keys.""" - async_press: Callable[[RenaultButtonEntity], Awaitable] + async_press: Callable[[RenaultButtonEntity], Coroutine[Any, Any, Any]] @dataclass @@ -56,25 +57,15 @@ class RenaultButtonEntity(RenaultEntity, ButtonEntity): await self.entity_description.async_press(self) -async def _start_charge(entity: RenaultButtonEntity) -> None: - """Start charge on the vehicle.""" - await entity.vehicle.vehicle.set_charge_start() - - -async def _start_air_conditioner(entity: RenaultButtonEntity) -> None: - """Start air conditioner on the vehicle.""" - await entity.vehicle.vehicle.set_ac_start(21, None) - - BUTTON_TYPES: tuple[RenaultButtonEntityDescription, ...] = ( RenaultButtonEntityDescription( - async_press=_start_air_conditioner, + async_press=lambda x: x.vehicle.set_ac_start(21, None), key="start_air_conditioner", icon="mdi:air-conditioner", name="Start air conditioner", ), RenaultButtonEntityDescription( - async_press=_start_charge, + async_press=lambda x: x.vehicle.set_charge_start(), key="start_charge", icon="mdi:ev-station", name="Start charge", diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index a9f65197502..8c75d93c2f8 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -7,5 +7,6 @@ "requirements": ["renault-api==0.1.11"], "codeowners": ["@epenet"], "iot_class": "cloud_polling", - "loggers": ["renault_api"] + "loggers": ["renault_api"], + "quality_scale": "platinum" } diff --git a/homeassistant/components/renault/renault_coordinator.py b/homeassistant/components/renault/renault_coordinator.py index 7db5ed0c4e1..d101b551dfe 100644 --- a/homeassistant/components/renault/renault_coordinator.py +++ b/homeassistant/components/renault/renault_coordinator.py @@ -1,10 +1,11 @@ """Proxy to handle account communication with Renault servers.""" from __future__ import annotations +import asyncio from collections.abc import Awaitable, Callable from datetime import timedelta import logging -from typing import Optional, TypeVar +from typing import TypeVar from renault_api.kamereon.exceptions import ( AccessDeniedException, @@ -16,7 +17,10 @@ from renault_api.kamereon.models import KamereonVehicleDataAttributes from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -T = TypeVar("T", bound=Optional[KamereonVehicleDataAttributes]) +T = TypeVar("T", bound=KamereonVehicleDataAttributes | None) + +# We have potentially 7 coordinators per vehicle +_PARALLEL_SEMAPHORE = asyncio.Semaphore(1) class RenaultDataUpdateCoordinator(DataUpdateCoordinator[T]): @@ -47,7 +51,8 @@ class RenaultDataUpdateCoordinator(DataUpdateCoordinator[T]): if self.update_method is None: raise NotImplementedError("Update method not implemented") try: - return await self.update_method() + async with _PARALLEL_SEMAPHORE: + return await self.update_method() except AccessDeniedException as err: # Disable because the account is not allowed to access this Renault endpoint. self.update_interval = None diff --git a/homeassistant/components/renault/renault_vehicle.py b/homeassistant/components/renault/renault_vehicle.py index 2d15e9c14a3..69835552ba4 100644 --- a/homeassistant/components/renault/renault_vehicle.py +++ b/homeassistant/components/renault/renault_vehicle.py @@ -2,22 +2,47 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable, Callable +from collections.abc import Awaitable, Callable, Coroutine from dataclasses import dataclass -from datetime import timedelta +from datetime import datetime, timedelta +from functools import wraps import logging -from typing import cast +from typing import Any, Concatenate, ParamSpec, TypeVar, cast +from renault_api.exceptions import RenaultException from renault_api.kamereon import models from renault_api.renault_vehicle import RenaultVehicle from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity import DeviceInfo from .const import DOMAIN from .renault_coordinator import RenaultDataUpdateCoordinator LOGGER = logging.getLogger(__name__) +_T = TypeVar("_T") +_P = ParamSpec("_P") + + +def with_error_wrapping( + func: Callable[Concatenate[RenaultVehicleProxy, _P], Awaitable[_T]] +) -> Callable[Concatenate[RenaultVehicleProxy, _P], Coroutine[Any, Any, _T]]: + """Catch Renault errors.""" + + @wraps(func) + async def wrapper( + self: RenaultVehicleProxy, + *args: _P.args, + **kwargs: _P.kwargs, + ) -> _T: + """Catch RenaultException errors and raise HomeAssistantError.""" + try: + return await func(self, *args, **kwargs) + except RenaultException as err: + raise HomeAssistantError(err) from err + + return wrapper @dataclass @@ -69,11 +94,6 @@ class RenaultVehicleProxy: """Return a device description for device registry.""" return self._device_info - @property - def vehicle(self) -> RenaultVehicle: - """Return the underlying vehicle.""" - return self._vehicle - async def async_initialise(self) -> None: """Load available coordinators.""" self.coordinators = { @@ -119,6 +139,42 @@ class RenaultVehicleProxy: ) del self.coordinators[key] + @with_error_wrapping + async def set_charge_mode( + self, charge_mode: str + ) -> models.KamereonVehicleChargeModeActionData: + """Set vehicle charge mode.""" + return await self._vehicle.set_charge_mode(charge_mode) + + @with_error_wrapping + async def set_charge_start(self) -> models.KamereonVehicleChargingStartActionData: + """Start vehicle charge.""" + return await self._vehicle.set_charge_start() + + @with_error_wrapping + async def set_ac_stop(self) -> models.KamereonVehicleHvacStartActionData: + """Stop vehicle ac.""" + return await self._vehicle.set_ac_stop() + + @with_error_wrapping + async def set_ac_start( + self, temperature: float, when: datetime | None = None + ) -> models.KamereonVehicleHvacStartActionData: + """Start vehicle ac.""" + return await self._vehicle.set_ac_start(temperature, when) + + @with_error_wrapping + async def get_charging_settings(self) -> models.KamereonVehicleChargingSettingsData: + """Get vehicle charging settings.""" + return await self._vehicle.get_charging_settings() + + @with_error_wrapping + async def set_charge_schedules( + self, schedules: list[models.ChargeSchedule] + ) -> models.KamereonVehicleChargeScheduleActionData: + """Set vehicle charge schedules.""" + return await self._vehicle.set_charge_schedules(schedules) + COORDINATORS: tuple[RenaultCoordinatorDescription, ...] = ( RenaultCoordinatorDescription( diff --git a/homeassistant/components/renault/select.py b/homeassistant/components/renault/select.py index 1d34c9fdf2b..8fef7d9aee0 100644 --- a/homeassistant/components/renault/select.py +++ b/homeassistant/components/renault/select.py @@ -75,7 +75,7 @@ class RenaultSelectEntity( async def async_select_option(self, option: str) -> None: """Change the selected option.""" - await self.vehicle.vehicle.set_charge_mode(option) + await self.vehicle.set_charge_mode(option) def _get_charge_mode_icon(entity: RenaultSelectEntity) -> str: diff --git a/homeassistant/components/renault/services.py b/homeassistant/components/renault/services.py index 91dc31d17f7..d25b73cafc2 100644 --- a/homeassistant/components/renault/services.py +++ b/homeassistant/components/renault/services.py @@ -63,13 +63,7 @@ SERVICE_CHARGE_SET_SCHEDULES_SCHEMA = SERVICE_VEHICLE_SCHEMA.extend( SERVICE_AC_CANCEL = "ac_cancel" SERVICE_AC_START = "ac_start" SERVICE_CHARGE_SET_SCHEDULES = "charge_set_schedules" -SERVICE_CHARGE_START = "charge_start" -SERVICES = [ - SERVICE_AC_CANCEL, - SERVICE_AC_START, - SERVICE_CHARGE_SET_SCHEDULES, - SERVICE_CHARGE_START, -] +SERVICES = [SERVICE_AC_CANCEL, SERVICE_AC_START, SERVICE_CHARGE_SET_SCHEDULES] def setup_services(hass: HomeAssistant) -> None: @@ -80,7 +74,7 @@ def setup_services(hass: HomeAssistant) -> None: proxy = get_vehicle_proxy(service_call.data) LOGGER.debug("A/C cancel attempt") - result = await proxy.vehicle.set_ac_stop() + result = await proxy.set_ac_stop() LOGGER.debug("A/C cancel result: %s", result) async def ac_start(service_call: ServiceCall) -> None: @@ -90,42 +84,27 @@ def setup_services(hass: HomeAssistant) -> None: proxy = get_vehicle_proxy(service_call.data) LOGGER.debug("A/C start attempt: %s / %s", temperature, when) - result = await proxy.vehicle.set_ac_start(temperature, when) + result = await proxy.set_ac_start(temperature, when) LOGGER.debug("A/C start result: %s", result.raw_data) async def charge_set_schedules(service_call: ServiceCall) -> None: """Set charge schedules.""" schedules: list[dict[str, Any]] = service_call.data[ATTR_SCHEDULES] proxy = get_vehicle_proxy(service_call.data) - charge_schedules = await proxy.vehicle.get_charging_settings() + charge_schedules = await proxy.get_charging_settings() for schedule in schedules: charge_schedules.update(schedule) if TYPE_CHECKING: assert charge_schedules.schedules is not None LOGGER.debug("Charge set schedules attempt: %s", schedules) - result = await proxy.vehicle.set_charge_schedules(charge_schedules.schedules) + result = await proxy.set_charge_schedules(charge_schedules.schedules) + LOGGER.debug("Charge set schedules result: %s", result) LOGGER.debug( "It may take some time before these changes are reflected in your vehicle" ) - async def charge_start(service_call: ServiceCall) -> None: - """Start charge.""" - # The Renault start charge service has been replaced by a - # dedicated button entity and marked as deprecated - LOGGER.warning( - "The 'renault.charge_start' service is deprecated and " - "replaced by a dedicated start charge button entity; please " - "use that entity to start the charge instead" - ) - - proxy = get_vehicle_proxy(service_call.data) - - LOGGER.debug("Charge start attempt") - result = await proxy.vehicle.set_charge_start() - LOGGER.debug("Charge start result: %s", result) - def get_vehicle_proxy(service_call_data: Mapping) -> RenaultVehicleProxy: """Get vehicle from service_call data.""" device_registry = dr.async_get(hass) @@ -159,12 +138,6 @@ def setup_services(hass: HomeAssistant) -> None: charge_set_schedules, schema=SERVICE_CHARGE_SET_SCHEDULES_SCHEMA, ) - hass.services.async_register( - DOMAIN, - SERVICE_CHARGE_START, - charge_start, - schema=SERVICE_VEHICLE_SCHEMA, - ) def unload_services(hass: HomeAssistant) -> None: diff --git a/homeassistant/components/renault/translations/fr.json b/homeassistant/components/renault/translations/fr.json index 32dcac2929f..9ad8e648188 100644 --- a/homeassistant/components/renault/translations/fr.json +++ b/homeassistant/components/renault/translations/fr.json @@ -31,5 +31,14 @@ "title": "D\u00e9finir les informations d'identification de Renault" } } + }, + "entity": { + "sensor": { + "charge_state": { + "state": { + "waiting_for_a_planned_charge": "En attente d'une charge planifi\u00e9e" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/renault/translations/he.json b/homeassistant/components/renault/translations/he.json index 1518df4599e..05001562a01 100644 --- a/homeassistant/components/renault/translations/he.json +++ b/homeassistant/components/renault/translations/he.json @@ -23,5 +23,14 @@ "title": "\u05d4\u05d2\u05d3\u05e8\u05ea \u05d0\u05d9\u05e9\u05d5\u05e8\u05d9 \u05e8\u05e0\u05d5" } } + }, + "entity": { + "sensor": { + "charge_state": { + "state": { + "energy_flap_opened": "\u05de\u05d3\u05e3 \u05d0\u05e0\u05e8\u05d2\u05d9\u05d4 \u05e0\u05e4\u05ea\u05d7" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/renault/translations/nl.json b/homeassistant/components/renault/translations/nl.json index 197ed32f6de..4e226b0c260 100644 --- a/homeassistant/components/renault/translations/nl.json +++ b/homeassistant/components/renault/translations/nl.json @@ -36,7 +36,8 @@ "sensor": { "charge_state": { "state": { - "charge_in_progress": "Opladen" + "charge_in_progress": "Opladen", + "unavailable": "Niet beschikbaar" } } } diff --git a/homeassistant/components/renault/translations/tr.json b/homeassistant/components/renault/translations/tr.json index e2da3f97c16..f5c755a4da6 100644 --- a/homeassistant/components/renault/translations/tr.json +++ b/homeassistant/components/renault/translations/tr.json @@ -31,5 +31,38 @@ "title": "Renault kimlik bilgilerini ayarla" } } + }, + "entity": { + "select": { + "charge_mode": { + "state": { + "always": "An\u0131nda", + "always_charging": "An\u0131nda", + "schedule_mode": "Planlay\u0131c\u0131" + } + } + }, + "sensor": { + "charge_state": { + "state": { + "charge_ended": "\u015earj bitti", + "charge_error": "\u015earj olmuyor veya prize tak\u0131l\u0131 de\u011fil", + "charge_in_progress": "\u015earj Oluyor", + "energy_flap_opened": "Enerji kapa\u011f\u0131 a\u00e7\u0131ld\u0131", + "not_in_charge": "\u015earj olmuyor", + "unavailable": "Mevcut de\u011fil", + "waiting_for_a_planned_charge": "Planlanan \u015farj i\u00e7in bekleniyor", + "waiting_for_current_charge": "Mevcut \u015farj i\u00e7in bekleniyor" + } + }, + "plug_state": { + "state": { + "plug_error": "Fi\u015f hatas\u0131", + "plug_unknown": "Fi\u015f bilinmiyor", + "plugged": "Tak\u0131l\u0131", + "unplugged": "Fi\u015fi \u00e7ekildi" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/renault/translations/uk.json b/homeassistant/components/renault/translations/uk.json new file mode 100644 index 00000000000..bcb61fa899b --- /dev/null +++ b/homeassistant/components/renault/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041e\u043d\u043e\u0432\u0456\u0442\u044c \u0441\u0432\u0456\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0415\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430 \u043f\u043e\u0448\u0442\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 6f7ab9d68b7..c20aff637ec 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -9,21 +9,28 @@ import logging from aiohttp import ClientConnectorError import async_timeout -from reolink_aio.exceptions import ApiError, InvalidContentTypeError +from reolink_aio.exceptions import ( + ApiError, + InvalidContentTypeError, + LoginError, + NoDataError, + ReolinkError, + UnexpectedDataError, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN -from .exceptions import UserNotAdmin +from .exceptions import ReolinkException, UserNotAdmin from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.CAMERA] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.CAMERA] DEVICE_UPDATE_INTERVAL = 60 @@ -40,23 +47,22 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b host = ReolinkHost(hass, config_entry.data, config_entry.options) try: - if not await host.async_init(): - await host.stop() - raise ConfigEntryNotReady( - f"Error while trying to setup {host.api.host}:{host.api.port}: " - "failed to obtain data from device." - ) + await host.async_init() except UserNotAdmin as err: - raise ConfigEntryAuthFailed(err) from UserNotAdmin + raise ConfigEntryAuthFailed(err) from err except ( ClientConnectorError, asyncio.TimeoutError, ApiError, InvalidContentTypeError, + LoginError, + NoDataError, + ReolinkException, + UnexpectedDataError, ) as err: await host.stop() raise ConfigEntryNotReady( - f'Error while trying to setup {host.api.host}:{host.api.port}: "{str(err)}".' + f"Error while trying to setup {host.api.host}:{host.api.port}: {str(err)}" ) from err config_entry.async_on_unload( @@ -66,8 +72,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_device_config_update(): """Update the host state cache and renew the ONVIF-subscription.""" async with async_timeout.timeout(host.api.timeout): - # Login session is implicitly updated here - await host.update_states() + try: + await host.update_states() + except ReolinkError as err: + raise UpdateFailed( + f"Error updating Reolink {host.api.nvr_name}" + ) from err + + async with async_timeout.timeout(host.api.timeout): + await host.renew() coordinator_device_config_update = DataUpdateCoordinator( hass, diff --git a/homeassistant/components/reolink/binary_sensor.py b/homeassistant/components/reolink/binary_sensor.py new file mode 100644 index 00000000000..5e7718f4180 --- /dev/null +++ b/homeassistant/components/reolink/binary_sensor.py @@ -0,0 +1,168 @@ +"""This component provides support for Reolink binary sensors.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from reolink_aio.api import ( + FACE_DETECTION_TYPE, + PERSON_DETECTION_TYPE, + PET_DETECTION_TYPE, + VEHICLE_DETECTION_TYPE, + Host, +) + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ReolinkData +from .const import DOMAIN +from .entity import ReolinkCoordinatorEntity + + +@dataclass +class ReolinkBinarySensorEntityDescriptionMixin: + """Mixin values for Reolink binary sensor entities.""" + + value: Callable[[Host, int | None], bool] + + +@dataclass +class ReolinkBinarySensorEntityDescription( + BinarySensorEntityDescription, ReolinkBinarySensorEntityDescriptionMixin +): + """A class that describes binary sensor entities.""" + + icon: str = "mdi:motion-sensor" + icon_off: str = "mdi:motion-sensor-off" + supported: Callable[[Host, int | None], bool] = lambda host, ch: True + + +BINARY_SENSORS = ( + ReolinkBinarySensorEntityDescription( + key="motion", + name="Motion", + device_class=BinarySensorDeviceClass.MOTION, + value=lambda api, ch: api.motion_detected(ch), + ), + ReolinkBinarySensorEntityDescription( + key=FACE_DETECTION_TYPE, + name="Face", + icon="mdi:face-recognition", + value=lambda api, ch: api.ai_detected(ch, FACE_DETECTION_TYPE), + supported=lambda api, ch: api.ai_supported(ch, FACE_DETECTION_TYPE), + ), + ReolinkBinarySensorEntityDescription( + key=PERSON_DETECTION_TYPE, + name="Person", + value=lambda api, ch: api.ai_detected(ch, PERSON_DETECTION_TYPE), + supported=lambda api, ch: api.ai_supported(ch, PERSON_DETECTION_TYPE), + ), + ReolinkBinarySensorEntityDescription( + key=VEHICLE_DETECTION_TYPE, + name="Vehicle", + icon="mdi:car", + icon_off="mdi:car-off", + value=lambda api, ch: api.ai_detected(ch, VEHICLE_DETECTION_TYPE), + supported=lambda api, ch: api.ai_supported(ch, VEHICLE_DETECTION_TYPE), + ), + ReolinkBinarySensorEntityDescription( + key=PET_DETECTION_TYPE, + name="Pet", + icon="mdi:dog-side", + icon_off="mdi:dog-side-off", + value=lambda api, ch: api.ai_detected(ch, PET_DETECTION_TYPE), + supported=lambda api, ch: api.ai_supported(ch, PET_DETECTION_TYPE), + ), + ReolinkBinarySensorEntityDescription( + key="visitor", + name="Visitor", + icon="mdi:bell-ring-outline", + icon_off="mdi:doorbell", + value=lambda api, ch: api.visitor_detected(ch), + supported=lambda api, ch: api.is_doorbell_enabled(ch), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a Reolink IP Camera.""" + reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id] + + entities: list[ReolinkBinarySensorEntity] = [] + for channel in reolink_data.host.api.channels: + entities.extend( + [ + ReolinkBinarySensorEntity(reolink_data, channel, entity_description) + for entity_description in BINARY_SENSORS + if entity_description.supported(reolink_data.host.api, channel) + ] + ) + + async_add_entities(entities) + + +class ReolinkBinarySensorEntity(ReolinkCoordinatorEntity, BinarySensorEntity): + """Base binary-sensor class for Reolink IP camera motion sensors.""" + + _attr_has_entity_name = True + entity_description: ReolinkBinarySensorEntityDescription + + def __init__( + self, + reolink_data: ReolinkData, + channel: int, + entity_description: ReolinkBinarySensorEntityDescription, + ) -> None: + """Initialize Reolink binary sensor.""" + super().__init__(reolink_data, channel) + self.entity_description = entity_description + + self._attr_unique_id = ( + f"{self._host.unique_id}_{self._channel}_{entity_description.key}" + ) + + @property + def icon(self) -> str | None: + """Icon of the sensor.""" + if self.is_on is False: + return self.entity_description.icon_off + return super().icon + + @property + def is_on(self) -> bool: + """State of the sensor.""" + return self.entity_description.value(self._host.api, self._channel) + + async def async_added_to_hass(self) -> None: + """Entity created.""" + await super().async_added_to_hass() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{self._host.webhook_id}_{self._channel}", + self._async_handle_event, + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{self._host.webhook_id}_all", + self._async_handle_event, + ) + ) + + async def _async_handle_event(self, event): + """Handle incoming event for motion detection.""" + self.async_write_ha_state() diff --git a/homeassistant/components/reolink/camera.py b/homeassistant/components/reolink/camera.py index aafc9686eea..5ccada7269d 100644 --- a/homeassistant/components/reolink/camera.py +++ b/homeassistant/components/reolink/camera.py @@ -27,13 +27,16 @@ async def async_setup_entry( cameras = [] for channel in host.api.channels: streams = ["sub", "main", "snapshots"] - if host.api.protocol == "rtmp": + if host.api.protocol in ["rtmp", "flv"]: streams.append("ext") for stream in streams: - cameras.append(ReolinkCamera(reolink_data, config_entry, channel, stream)) + stream_url = await host.api.get_stream_source(channel, stream) + if stream_url is None and stream != "snapshots": + continue + cameras.append(ReolinkCamera(reolink_data, channel, stream)) - async_add_entities(cameras, update_before_add=True) + async_add_entities(cameras) class ReolinkCamera(ReolinkCoordinatorEntity, Camera): @@ -45,12 +48,11 @@ class ReolinkCamera(ReolinkCoordinatorEntity, Camera): def __init__( self, reolink_data: ReolinkData, - config_entry: ConfigEntry, channel: int, stream: str, ) -> None: """Initialize Reolink camera stream.""" - ReolinkCoordinatorEntity.__init__(self, reolink_data, config_entry, channel) + ReolinkCoordinatorEntity.__init__(self, reolink_data, channel) Camera.__init__(self) self._stream = stream diff --git a/homeassistant/components/reolink/config_flow.py b/homeassistant/components/reolink/config_flow.py index fdbbf201756..e4bc98cc0f8 100644 --- a/homeassistant/components/reolink/config_flow.py +++ b/homeassistant/components/reolink/config_flow.py @@ -5,21 +5,24 @@ from collections.abc import Mapping import logging from typing import Any -from reolink_aio.exceptions import ApiError, CredentialsInvalidError +from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError import voluptuous as vol -from homeassistant import config_entries, exceptions +from homeassistant import config_entries +from homeassistant.components import dhcp from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.device_registry import format_mac -from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DEFAULT_PROTOCOL, DOMAIN -from .exceptions import UserNotAdmin +from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DOMAIN +from .exceptions import ReolinkException, UserNotAdmin from .host import ReolinkHost _LOGGER = logging.getLogger(__name__) +DEFAULT_PROTOCOL = "rtsp" DEFAULT_OPTIONS = {CONF_PROTOCOL: DEFAULT_PROTOCOL} @@ -44,7 +47,7 @@ class ReolinkOptionsFlowHandler(config_entries.OptionsFlow): vol.Required( CONF_PROTOCOL, default=self.config_entry.options[CONF_PROTOCOL], - ): vol.In(["rtsp", "rtmp"]), + ): vol.In(["rtsp", "rtmp", "flv"]), } ), ) @@ -86,6 +89,21 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_user() return self.async_show_form(step_id="reauth_confirm") + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Handle discovery via dhcp.""" + mac_address = format_mac(discovery_info.macaddress) + await self.async_set_unique_id(mac_address) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) + + short_mac = mac_address[-8:].upper() + self.context["title_placeholders"] = { + "short_mac": short_mac, + "ip_address": discovery_info.ip, + } + + self._host = discovery_info.ip + return await self.async_step_user() + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -94,24 +112,30 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): placeholders = {"error": ""} if user_input is not None: + if CONF_HOST not in user_input: + user_input[CONF_HOST] = self._host + host = ReolinkHost(self.hass, user_input, DEFAULT_OPTIONS) try: - await async_obtain_host_settings(host) + await host.async_init() except UserNotAdmin: errors[CONF_USERNAME] = "not_admin" placeholders["username"] = host.api.username placeholders["userlevel"] = host.api.user_level - except CannotConnect: - errors[CONF_HOST] = "cannot_connect" except CredentialsInvalidError: errors[CONF_HOST] = "invalid_auth" except ApiError as err: placeholders["error"] = str(err) errors[CONF_HOST] = "api_error" + except (ReolinkError, ReolinkException) as err: + placeholders["error"] = str(err) + errors[CONF_HOST] = "cannot_connect" except Exception as err: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") placeholders["error"] = str(err) errors[CONF_HOST] = "unknown" + finally: + await host.stop() if not errors: user_input[CONF_PORT] = host.api.port @@ -140,9 +164,14 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): { vol.Required(CONF_USERNAME, default=self._username): str, vol.Required(CONF_PASSWORD, default=self._password): str, - vol.Required(CONF_HOST, default=self._host): str, } ) + if self._host is None or errors: + data_schema = data_schema.extend( + { + vol.Required(CONF_HOST, default=self._host): str, + } + ) if errors: data_schema = data_schema.extend( { @@ -157,16 +186,3 @@ class ReolinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, description_placeholders=placeholders, ) - - -async def async_obtain_host_settings(host: ReolinkHost) -> None: - """Initialize the Reolink host and get the host information.""" - try: - if not await host.async_init(): - raise CannotConnect - finally: - await host.stop() - - -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/reolink/const.py b/homeassistant/components/reolink/const.py index 180c3ccae11..2a35a0f723d 100644 --- a/homeassistant/components/reolink/const.py +++ b/homeassistant/components/reolink/const.py @@ -4,6 +4,3 @@ DOMAIN = "reolink" CONF_USE_HTTPS = "use_https" CONF_PROTOCOL = "protocol" - -DEFAULT_PROTOCOL = "rtsp" -DEFAULT_TIMEOUT = 60 diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index 403ea278889..bcf39814c9a 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -1,7 +1,6 @@ """Reolink parent entity class.""" from __future__ import annotations -from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -11,12 +10,10 @@ from .const import DOMAIN class ReolinkCoordinatorEntity(CoordinatorEntity): - """Parent class for Reolink Entities.""" + """Parent class for Reolink hardware camera entities.""" - def __init__( - self, reolink_data: ReolinkData, config_entry: ConfigEntry, channel: int | None - ) -> None: - """Initialize ReolinkCoordinatorEntity.""" + def __init__(self, reolink_data: ReolinkData, channel: int) -> None: + """Initialize ReolinkCoordinatorEntity for a hardware camera.""" coordinator = reolink_data.device_coordinator super().__init__(coordinator) @@ -25,7 +22,7 @@ class ReolinkCoordinatorEntity(CoordinatorEntity): http_s = "https" if self._host.api.use_https else "http" conf_url = f"{http_s}://{self._host.api.host}:{self._host.api.port}" - if self._host.api.is_nvr and self._channel is not None: + if self._host.api.is_nvr: self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"{self._host.unique_id}_ch{self._channel}")}, via_device=(DOMAIN, self._host.unique_id), diff --git a/homeassistant/components/reolink/exceptions.py b/homeassistant/components/reolink/exceptions.py index ad95625cfa7..f3e9e0158cd 100644 --- a/homeassistant/components/reolink/exceptions.py +++ b/homeassistant/components/reolink/exceptions.py @@ -2,5 +2,17 @@ from homeassistant.exceptions import HomeAssistantError -class UserNotAdmin(HomeAssistantError): +class ReolinkException(HomeAssistantError): + """BaseException for the Reolink integration.""" + + +class ReolinkSetupException(ReolinkException): + """Raised when setting up the Reolink host failed.""" + + +class ReolinkWebhookException(ReolinkException): + """Raised when registering the reolink webhook failed.""" + + +class UserNotAdmin(ReolinkException): """Raised when user is not admin.""" diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 5c744f0c5fd..3e0731ac8ce 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -7,19 +7,22 @@ import logging from typing import Any import aiohttp +from aiohttp.web import Request from reolink_aio.api import Host -from reolink_aio.exceptions import ( - ApiError, - CredentialsInvalidError, - InvalidContentTypeError, -) +from reolink_aio.exceptions import ReolinkError +from homeassistant.components import webhook from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.network import NoURLAvailableError, get_url -from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DEFAULT_TIMEOUT -from .exceptions import UserNotAdmin +from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DOMAIN +from .exceptions import ReolinkSetupException, ReolinkWebhookException, UserNotAdmin + +DEFAULT_TIMEOUT = 60 +SUBSCRIPTION_RENEW_THRESHOLD = 300 _LOGGER = logging.getLogger(__name__) @@ -49,6 +52,10 @@ class ReolinkHost: timeout=DEFAULT_TIMEOUT, ) + self.webhook_id: str | None = None + self._webhook_url: str | None = None + self._lost_subscription: bool = False + @property def unique_id(self) -> str: """Create the unique ID, base for all entities.""" @@ -59,20 +66,20 @@ class ReolinkHost: """Return the API object.""" return self._api - async def async_init(self) -> bool: + async def async_init(self) -> None: """Connect to Reolink host.""" self._api.expire_session() - if not await self._api.get_host_data(): - return False + await self._api.get_host_data() if self._api.mac_address is None: - return False + raise ReolinkSetupException("Could not get mac address") if not self._api.is_admin: await self.stop() raise UserNotAdmin( - f"User '{self._api.username}' has authorization level '{self._api.user_level}', only admin users can change camera settings" + f"User '{self._api.username}' has authorization level " + f"'{self._api.user_level}', only admin users can change camera settings" ) enable_onvif = None @@ -97,14 +104,17 @@ class ReolinkHost: enable_rtsp = True if enable_onvif or enable_rtmp or enable_rtsp: - if not await self._api.set_net_port( - enable_onvif=enable_onvif, - enable_rtmp=enable_rtmp, - enable_rtsp=enable_rtsp, - ): + try: + await self._api.set_net_port( + enable_onvif=enable_onvif, + enable_rtmp=enable_rtmp, + enable_rtsp=enable_rtsp, + ) + except ReolinkError: if enable_onvif: _LOGGER.error( - "Failed to enable ONVIF on %s. Set it to ON to receive notifications", + "Failed to enable ONVIF on %s. " + "Set it to ON to receive notifications", self._api.nvr_name, ) @@ -121,15 +131,15 @@ class ReolinkHost: self._unique_id = format_mac(self._api.mac_address) - return True + await self.subscribe() - async def update_states(self) -> bool: - """Call the API of the camera device to update the states.""" - return await self._api.get_states() + async def update_states(self) -> None: + """Call the API of the camera device to update the internal states.""" + await self._api.get_states() async def disconnect(self): """Disconnect from the API, so the connection will be released.""" - await self._api.unsubscribe_all() + await self._api.unsubscribe() try: await self._api.logout() @@ -146,22 +156,9 @@ class ReolinkHost: self._api.host, self._api.port, ) - except ApiError as err: + except ReolinkError as err: _LOGGER.error( - "Reolink API error while logging out for host %s:%s: %s", - self._api.host, - self._api.port, - str(err), - ) - except CredentialsInvalidError: - _LOGGER.error( - "Reolink credentials error while logging out for host %s:%s", - self._api.host, - self._api.port, - ) - except InvalidContentTypeError as err: - _LOGGER.error( - "Reolink content type error while logging out for host %s:%s: %s", + "Reolink error while logging out for host %s:%s: %s", self._api.host, self._api.port, str(err), @@ -169,4 +166,139 @@ class ReolinkHost: async def stop(self, event=None): """Disconnect the API.""" + await self.unregister_webhook() await self.disconnect() + + async def subscribe(self) -> None: + """Subscribe to motion events and register the webhook as a callback.""" + if self.webhook_id is None: + await self.register_webhook() + + if self._api.subscribed: + _LOGGER.debug( + "Host %s: is already subscribed to webhook %s", + self._api.host, + self._webhook_url, + ) + return + + if await self._api.subscribe(self._webhook_url): + _LOGGER.debug( + "Host %s: subscribed successfully to webhook %s", + self._api.host, + self._webhook_url, + ) + else: + raise ReolinkWebhookException( + f"Host {self._api.host}: webhook subscription failed" + ) + + async def renew(self) -> None: + """Renew the subscription of motion events (lease time is 15 minutes).""" + try: + await self._renew() + except ReolinkWebhookException as err: + if not self._lost_subscription: + self._lost_subscription = True + _LOGGER.error( + "Reolink %s event subscription lost: %s", + self._api.nvr_name, + str(err), + ) + else: + self._lost_subscription = False + + async def _renew(self) -> None: + """Execute the renew of the subscription.""" + if not self._api.subscribed: + _LOGGER.debug( + "Host %s: requested to renew a non-existing Reolink subscription, " + "trying to subscribe from scratch", + self._api.host, + ) + await self.subscribe() + return + + timer = self._api.renewtimer + if timer > SUBSCRIPTION_RENEW_THRESHOLD: + return + + if timer > 0: + if await self._api.renew(): + _LOGGER.debug( + "Host %s successfully renewed Reolink subscription", self._api.host + ) + return + _LOGGER.debug( + "Host %s: error renewing Reolink subscription, " + "trying to subscribe again", + self._api.host, + ) + + if not await self._api.subscribe(self._webhook_url): + raise ReolinkWebhookException( + f"Host {self._api.host}: webhook re-subscription failed" + ) + _LOGGER.debug( + "Host %s: Reolink re-subscription successful after it was expired", + self._api.host, + ) + + async def register_webhook(self) -> None: + """Register the webhook for motion events.""" + self.webhook_id = f"{DOMAIN}_{self.unique_id.replace(':', '')}" + event_id = self.webhook_id + + webhook.async_register( + self._hass, DOMAIN, event_id, event_id, self.handle_webhook + ) + + try: + base_url = get_url(self._hass, prefer_external=False) + except NoURLAvailableError: + try: + base_url = get_url(self._hass, prefer_external=True) + except NoURLAvailableError as err: + webhook.async_unregister(self._hass, event_id) + self.webhook_id = None + raise ReolinkWebhookException( + f"Error registering URL for webhook {event_id}: " + "HomeAssistant URL is not available" + ) from err + + webhook_path = webhook.async_generate_path(event_id) + self._webhook_url = f"{base_url}{webhook_path}" + + _LOGGER.debug("Registered webhook: %s", event_id) + + async def unregister_webhook(self): + """Unregister the webhook for motion events.""" + if self.webhook_id: + _LOGGER.debug("Unregistering webhook %s", self.webhook_id) + webhook.async_unregister(self._hass, self.webhook_id) + self.webhook_id = None + + async def handle_webhook( + self, hass: HomeAssistant, webhook_id: str, request: Request + ): + """Handle incoming webhook from Reolink for inbound messages and calls.""" + + _LOGGER.debug("Webhook '%s' called", webhook_id) + + if not request.body_exists: + _LOGGER.debug("Webhook '%s' triggered without payload", webhook_id) + return + + data = await request.text() + if not data: + _LOGGER.debug( + "Webhook '%s' triggered with unknown payload: %s", webhook_id, data + ) + return + + channel = await self._api.ONVIF_event_callback(data) + + if channel is None: + async_dispatcher_send(hass, f"{webhook_id}_all", {}) + else: + async_dispatcher_send(hass, f"{webhook_id}_{channel}", {}) diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 6fb26ea60fe..88e2e3b7730 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -3,8 +3,15 @@ "name": "Reolink IP NVR/camera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/reolink", - "requirements": ["reolink-aio==0.2.1"], + "requirements": ["reolink-aio==0.3.0"], + "dependencies": ["webhook"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", - "loggers": ["reolink-aio"] + "loggers": ["reolink_aio"], + "dhcp": [ + { + "hostname": "reolink*", + "macaddress": "EC71DB*" + } + ] } diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index 1c82a43c8a2..8cd78e2ed7b 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{short_mac} ({ip_address})", "step": { "user": { "description": "{error}", diff --git a/homeassistant/components/reolink/translations/bg.json b/homeassistant/components/reolink/translations/bg.json new file mode 100644 index 00000000000..cdee795acd8 --- /dev/null +++ b/homeassistant/components/reolink/translations/bg.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430: {error}" + }, + "flow_title": "{short_mac} ({ip_address})", + "step": { + "reauth_confirm": { + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 Reolink \u0442\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u0438 \u043e\u0442\u043d\u043e\u0432\u043e \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u0437\u0430 \u0432\u0430\u0448\u0430\u0442\u0430 \u0432\u0440\u044a\u0437\u043a\u0430", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u0442\u0430" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "port": "\u041f\u043e\u0440\u0442", + "use_https": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 HTTPS", + "username": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435" + }, + "description": "{error}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/ca.json b/homeassistant/components/reolink/translations/ca.json index c5bcbecd22a..63e7e81c40d 100644 --- a/homeassistant/components/reolink/translations/ca.json +++ b/homeassistant/components/reolink/translations/ca.json @@ -1,23 +1,31 @@ { "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": { - "api_error": "S'ha produ\u00eft un error a l'API: {error}", + "api_error": "S'ha produ\u00eft un error a l'API", "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", - "unknown": "Error inesperat: {error}" + "not_admin": "L'usuari ha de ser administrador, l'usuari ''{username}'' t\u00e9 nivell d'autoritzaci\u00f3 ''{userlevel}''", + "unknown": "Error inesperat" }, + "flow_title": "{short_mac} ({ip_address})", "step": { + "reauth_confirm": { + "description": "La integraci\u00f3 Reolink ha de tornar a autenticar la teva connexi\u00f3", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" + }, "user": { "data": { "host": "Amfitri\u00f3", "password": "Contrasenya", "port": "Port", - "use_https": "Activa HTTPS", + "use_https": "Activa l'HTTPS", "username": "Nom d'usuari" - } + }, + "description": "{error}" } } }, diff --git a/homeassistant/components/reolink/translations/cs.json b/homeassistant/components/reolink/translations/cs.json new file mode 100644 index 00000000000..6096b67d358 --- /dev/null +++ b/homeassistant/components/reolink/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba: {error}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/de.json b/homeassistant/components/reolink/translations/de.json new file mode 100644 index 00000000000..fb145dd4aa2 --- /dev/null +++ b/homeassistant/components/reolink/translations/de.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "api_error": "API-Fehler aufgetreten", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "not_admin": "Benutzer muss Administrator sein, Benutzer ''{username}'' hat Autorisierungslevel ''{userlevel}''", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "{short_mac} ({ip_address})", + "step": { + "reauth_confirm": { + "description": "Die Reolink-Integration muss deine Verbindungsdaten neu authentifizieren", + "title": "Integration erneut authentifizieren" + }, + "user": { + "data": { + "host": "Host", + "password": "Passwort", + "port": "Port", + "use_https": "HTTPS aktivieren", + "username": "Benutzername" + }, + "description": "{error}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protokoll" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/el.json b/homeassistant/components/reolink/translations/el.json new file mode 100644 index 00000000000..1d8e0c8a6b5 --- /dev/null +++ b/homeassistant/components/reolink/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 \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": { + "api_error": "\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 API: {error}", + "cannot_connect": "\u0397 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5", + "invalid_auth": "\u0395\u03c3\u03c6\u03b1\u03bb\u03bc\u03ad\u03bd\u03b7 \u03c4\u03b1\u03c5\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7", + "not_admin": "\u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae\u03c2, \u03bf \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 ''{username}'' \u03ad\u03c7\u03b5\u03b9 \u03b5\u03c0\u03af\u03c0\u03b5\u03b4\u03bf \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 ''{userlevel}''", + "unknown": "\u0391\u03c0\u03c1\u03bf\u03c3\u03b4\u03cc\u03ba\u03b7\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1: {error}" + }, + "flow_title": "{short_mac} ({ip_address})", + "step": { + "reauth_confirm": { + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Reolink \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\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03b7\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03ae\u03c2 \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", + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "port": "\u0398\u03cd\u03c1\u03b1", + "use_https": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 HTTPS", + "username": "\u039f\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" + }, + "description": "{error}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "\u03a0\u03c1\u03c9\u03c4\u03cc\u03ba\u03bf\u03bb\u03bb\u03bf" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/en.json b/homeassistant/components/reolink/translations/en.json index beb366e8b39..7ea0e2df1e8 100644 --- a/homeassistant/components/reolink/translations/en.json +++ b/homeassistant/components/reolink/translations/en.json @@ -11,6 +11,7 @@ "not_admin": "User needs to be admin, user ''{username}'' has authorisation level ''{userlevel}''", "unknown": "Unexpected error" }, + "flow_title": "{short_mac} ({ip_address})", "step": { "reauth_confirm": { "description": "The Reolink integration needs to re-authenticate your connection details", diff --git a/homeassistant/components/reolink/translations/es-419.json b/homeassistant/components/reolink/translations/es-419.json new file mode 100644 index 00000000000..efbd7e606c0 --- /dev/null +++ b/homeassistant/components/reolink/translations/es-419.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "{error}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/es.json b/homeassistant/components/reolink/translations/es.json index 44a9939d4d7..2a8ea63673d 100644 --- a/homeassistant/components/reolink/translations/es.json +++ b/homeassistant/components/reolink/translations/es.json @@ -1,15 +1,22 @@ { "config": { "abort": { - "already_configured": "El dispositivo ya est\u00e1 configurado" + "already_configured": "El dispositivo ya est\u00e1 configurado", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente" }, "error": { - "api_error": "Ocurri\u00f3 un error de API: {error}", + "api_error": "Se ha producido un error de API", "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", - "unknown": "Error inesperado: {error}" + "not_admin": "El usuario debe ser administrador, el usuario ''{username}'' tiene nivel de autorizaci\u00f3n ''{userlevel}''", + "unknown": "Error inesperado" }, + "flow_title": "{short_mac} ({ip_address})", "step": { + "reauth_confirm": { + "description": "La integraci\u00f3n de Reolink necesita volver a autenticar los detalles de tu conexi\u00f3n", + "title": "Volver a autenticar la integraci\u00f3n" + }, "user": { "data": { "host": "Host", @@ -17,7 +24,8 @@ "port": "Puerto", "use_https": "Habilitar HTTPS", "username": "Nombre de usuario" - } + }, + "description": "{error}" } } }, diff --git a/homeassistant/components/reolink/translations/et.json b/homeassistant/components/reolink/translations/et.json new file mode 100644 index 00000000000..7ee4e0b98fd --- /dev/null +++ b/homeassistant/components/reolink/translations/et.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "api_error": "Ilmnes API t\u00f5rge", + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "not_admin": "Kasutaja peab olema administraator, kasutajal ''{username}'' on volituste tase ''{userlevel}''.", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "{short_mac} ({ip_address})", + "step": { + "reauth_confirm": { + "description": "Reolinki sidumine peab \u00fchenduse andmed uuesti autentima.", + "title": "Taastuvasta sidumine" + }, + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na", + "port": "Port", + "use_https": "Luba HTTPS", + "username": "Kasutajanimi" + }, + "description": "{error}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protokoll" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/fr.json b/homeassistant/components/reolink/translations/fr.json new file mode 100644 index 00000000000..e3139f49975 --- /dev/null +++ b/homeassistant/components/reolink/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "use_https": "Activer HTTPS" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protocole" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/he.json b/homeassistant/components/reolink/translations/he.json new file mode 100644 index 00000000000..4aab941b8a9 --- /dev/null +++ b/homeassistant/components/reolink/translations/he.json @@ -0,0 +1,23 @@ +{ + "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", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4: {error}" + }, + "flow_title": "{short_mac} ({ip_address})", + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7", + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "port": "\u05e4\u05ea\u05d7\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/hu.json b/homeassistant/components/reolink/translations/hu.json new file mode 100644 index 00000000000..0bf8246e903 --- /dev/null +++ b/homeassistant/components/reolink/translations/hu.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." + }, + "error": { + "api_error": "API hiba t\u00f6rt\u00e9nt", + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "not_admin": "A felhaszn\u00e1l\u00f3nak adminisztr\u00e1tornak kell lennie, \"{username}\" felhaszn\u00e1l\u00f3 jogosults\u00e1gi szintje pedig \"{userlevel}\".", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "reauth_confirm": { + "description": "A Reolink integr\u00e1ci\u00f3nak \u00fajra kell hiteles\u00edtenie a kapcsolaot.", + "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" + }, + "user": { + "data": { + "host": "C\u00edm", + "password": "Jelsz\u00f3", + "port": "Port", + "use_https": "HTTPS enged\u00e9lyez\u00e9se", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "{error}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protokoll" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/id.json b/homeassistant/components/reolink/translations/id.json new file mode 100644 index 00000000000..b0236fe3bbf --- /dev/null +++ b/homeassistant/components/reolink/translations/id.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi", + "reauth_successful": "Autentikasi ulang berhasil" + }, + "error": { + "api_error": "Terjadi kesalahan API", + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "not_admin": "Pengguna harus memiliki izin admin, pengguna ''{username}'' memiliki tingkat otorisasi ''{userlevel}''", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "flow_title": "{short_mac} ({ip_address})", + "step": { + "reauth_confirm": { + "description": "Integrasi Reolink perlu mengautentikasi ulang koneksi Anda", + "title": "Autentikasi Ulang Integrasi" + }, + "user": { + "data": { + "host": "Host", + "password": "Kata Sandi", + "port": "Port", + "use_https": "Aktifkan HTTPS", + "username": "Nama Pengguna" + }, + "description": "{error}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protokol" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/it.json b/homeassistant/components/reolink/translations/it.json new file mode 100644 index 00000000000..4b668c9adbd --- /dev/null +++ b/homeassistant/components/reolink/translations/it.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente" + }, + "error": { + "api_error": "Si \u00e8 verificato un errore di API", + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "not_admin": "L'utente deve essere un amministratore, l'utente \"{username}\" ha il livello di autorizzazione \"{userlevel}\"", + "unknown": "Errore imprevisto" + }, + "step": { + "reauth_confirm": { + "description": "L'integrazione Reolink ha bisogno di riautenticare i dettagli della tua connessione", + "title": "Autentica nuovamente l'integrazione" + }, + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Porta", + "use_https": "Abilita HTTPS", + "username": "Nome utente" + }, + "description": "{error}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protocollo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/ja.json b/homeassistant/components/reolink/translations/ja.json new file mode 100644 index 00000000000..7f03bc6e886 --- /dev/null +++ b/homeassistant/components/reolink/translations/ja.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" + }, + "step": { + "reauth_confirm": { + "title": "\u7d71\u5408\u306e\u518d\u8a8d\u8a3c" + }, + "user": { + "description": "{error}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/lv.json b/homeassistant/components/reolink/translations/lv.json new file mode 100644 index 00000000000..120beefb9b2 --- /dev/null +++ b/homeassistant/components/reolink/translations/lv.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "step": { + "user": { + "description": "{error}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/nl.json b/homeassistant/components/reolink/translations/nl.json new file mode 100644 index 00000000000..e29f259788e --- /dev/null +++ b/homeassistant/components/reolink/translations/nl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd", + "reauth_successful": "Herauthenticatie geslaagd" + }, + "error": { + "api_error": "API-fout opgetreden", + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "reauth_confirm": { + "title": "Integratie herauthenticeren" + }, + "user": { + "data": { + "host": "Host", + "password": "Wachtwoord", + "port": "Poort", + "use_https": "HTTPS inschakelen", + "username": "Gebruikersnaam" + }, + "description": "{error}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protocol" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/no.json b/homeassistant/components/reolink/translations/no.json new file mode 100644 index 00000000000..e88b902f154 --- /dev/null +++ b/homeassistant/components/reolink/translations/no.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "reauth_successful": "Re-autentisering var vellykket" + }, + "error": { + "api_error": "API-feil oppstod", + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "not_admin": "Brukeren m\u00e5 v\u00e6re admin, brukeren '' {username} '' har autorisasjonsniv\u00e5 '' {userlevel} ''", + "unknown": "Uventet feil" + }, + "flow_title": "{short_mac} ( {ip_address} )", + "step": { + "reauth_confirm": { + "description": "Reolink-integrasjonen m\u00e5 autentisere tilkoblingsdetaljene dine p\u00e5 nytt", + "title": "Godkjenne integrering p\u00e5 nytt" + }, + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "port": "Port", + "use_https": "Aktiver HTTPS", + "username": "Brukernavn" + }, + "description": "{error}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protokoll" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/pl.json b/homeassistant/components/reolink/translations/pl.json new file mode 100644 index 00000000000..993077b75e6 --- /dev/null +++ b/homeassistant/components/reolink/translations/pl.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" + }, + "error": { + "api_error": "Wyst\u0105pi\u0142 b\u0142\u0105d API", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "not_admin": "U\u017cytkownik musi by\u0107 administratorem, u\u017cytkownik \u201e{username}\u201d ma poziom autoryzacji \u201e{userlevel}\u201d", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "reauth_confirm": { + "description": "Integracja Reolink wymaga ponownego uwierzytelnienia szczeg\u00f3\u0142\u00f3w po\u0142\u0105czenia", + "title": "Ponownie uwierzytelnij integracj\u0119" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", + "use_https": "W\u0142\u0105cz HTTPS", + "username": "Nazwa u\u017cytkownika" + }, + "description": "{error}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protok\u00f3\u0142" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/pt-BR.json b/homeassistant/components/reolink/translations/pt-BR.json new file mode 100644 index 00000000000..9750ff6689f --- /dev/null +++ b/homeassistant/components/reolink/translations/pt-BR.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida" + }, + "error": { + "api_error": "Ocorreu um erro de API", + "cannot_connect": "Falha ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "not_admin": "O usu\u00e1rio precisa ser administrador, o usu\u00e1rio ''{username}'' tem n\u00edvel de autoriza\u00e7\u00e3o ''{userlevel}''", + "unknown": "Erro inesperado" + }, + "flow_title": "{short_mac} ({ip_address})", + "step": { + "reauth_confirm": { + "description": "A integra\u00e7\u00e3o Reolink precisa autenticar novamente seus detalhes de conex\u00e3o", + "title": "Reautenticar Integra\u00e7\u00e3o" + }, + "user": { + "data": { + "host": "Nome do host", + "password": "Senha", + "port": "Porta", + "use_https": "Ativar HTTPS", + "username": "Usu\u00e1rio" + }, + "description": "{error}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protocolo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/pt.json b/homeassistant/components/reolink/translations/pt.json new file mode 100644 index 00000000000..c97470ef7b1 --- /dev/null +++ b/homeassistant/components/reolink/translations/pt.json @@ -0,0 +1,5 @@ +{ + "config": { + "flow_title": "{short_mac} ({ip_address})" + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/ru.json b/homeassistant/components/reolink/translations/ru.json new file mode 100644 index 00000000000..69a671509b4 --- /dev/null +++ b/homeassistant/components/reolink/translations/ru.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "error": { + "api_error": "\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 API.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f", + "not_admin": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c, \u0442\u043e\u0433\u0434\u0430 \u043a\u0430\u043a \u0443 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f ''{username}'' \u0443\u0440\u043e\u0432\u0435\u043d\u044c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 ''{userlevel}''.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "{short_mac} ({ip_address})", + "step": { + "reauth_confirm": { + "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 Reolink", + "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": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "use_https": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c HTTPS", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "{error}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/sk.json b/homeassistant/components/reolink/translations/sk.json new file mode 100644 index 00000000000..05c1688080c --- /dev/null +++ b/homeassistant/components/reolink/translations/sk.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9" + }, + "error": { + "api_error": "Vyskytla sa chyba API", + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "not_admin": "Pou\u017e\u00edvate\u013e mus\u00ed by\u0165 spr\u00e1vcom, pou\u017e\u00edvate\u013e ''{username}'' m\u00e1 \u00farove\u0148 autoriz\u00e1cie ''{userlevel}''", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "flow_title": "{short_mac} ({ip_address})", + "step": { + "reauth_confirm": { + "description": "Integr\u00e1cia Reolink potrebuje op\u00e4tovne overi\u0165 \u00fadaje o pripojen\u00ed", + "title": "Znova overi\u0165 integr\u00e1ciu" + }, + "user": { + "data": { + "host": "Hostite\u013e", + "password": "Heslo", + "port": "Port", + "use_https": "Povoli\u0165 HTTPS", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "{error}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protokol" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/sv.json b/homeassistant/components/reolink/translations/sv.json new file mode 100644 index 00000000000..efbd7e606c0 --- /dev/null +++ b/homeassistant/components/reolink/translations/sv.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "{error}" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/tr.json b/homeassistant/components/reolink/translations/tr.json new file mode 100644 index 00000000000..c65755993c3 --- /dev/null +++ b/homeassistant/components/reolink/translations/tr.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "api_error": "API hatas\u0131 olu\u015ftu", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "not_admin": "Kullan\u0131c\u0131n\u0131n y\u00f6netici olmas\u0131 gerekiyor, '' {username} '' kullan\u0131c\u0131s\u0131 '' {userlevel} '' yetki d\u00fczeyine sahip", + "unknown": "Beklenmeyen hata" + }, + "step": { + "reauth_confirm": { + "description": "Reolink entegrasyonunun ba\u011flant\u0131 ayr\u0131nt\u0131lar\u0131n\u0131z\u0131 yeniden do\u011frulamas\u0131 gerekiyor", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, + "user": { + "data": { + "host": "Sunucu", + "password": "Parola", + "port": "Port", + "use_https": "HTTPS'yi Etkinle\u015ftir", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "{error}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "Protokol" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/uk.json b/homeassistant/components/reolink/translations/uk.json new file mode 100644 index 00000000000..7674b878a0e --- /dev/null +++ b/homeassistant/components/reolink/translations/uk.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "api_error": "\u0421\u0442\u0430\u043b\u0430\u0441\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 API: {error}", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f", + "not_admin": "\u041a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447 \u043c\u0430\u0454 \u0431\u0443\u0442\u0438 \u0430\u0434\u043c\u0456\u043d\u0456\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c, \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447 ''{username}'' \u043c\u0430\u0454 \u0440\u0456\u0432\u0435\u043d\u044c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457 ''{userlevel}''", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430: {error}" + }, + "step": { + "reauth_confirm": { + "description": "\u041f\u043e\u0442\u0440\u0456\u0431\u043d\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Reolink", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "use_https": "\u0423\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c HTTPS", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "{error}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/reolink/translations/zh-Hant.json b/homeassistant/components/reolink/translations/zh-Hant.json new file mode 100644 index 00000000000..090aba9f570 --- /dev/null +++ b/homeassistant/components/reolink/translations/zh-Hant.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" + }, + "error": { + "api_error": "\u767c\u751f API \u932f\u8aa4", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "not_admin": "\u4f7f\u7528\u8005\u5fc5\u9808\u70ba\u7ba1\u7406\u54e1\u8eab\u4efd\u3002\u4f7f\u7528\u8005 ''{username}'' \u5177\u6709\u6388\u6b0a\u5c64\u7d1a ''{userlevel}''", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "{short_mac} ({ip_address})", + "step": { + "reauth_confirm": { + "description": "Reolink \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "use_https": "\u958b\u555f HTTPS", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "{error}" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "protocol": "\u901a\u8a0a\u5354\u5b9a" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/repairs/issue_handler.py b/homeassistant/components/repairs/issue_handler.py index 4bf7f466fd2..1dda2fb87a6 100644 --- a/homeassistant/components/repairs/issue_handler.py +++ b/homeassistant/components/repairs/issue_handler.py @@ -34,7 +34,7 @@ class ConfirmRepairFlow(RepairsFlow): ) -> data_entry_flow.FlowResult: """Handle the confirm step of a fix flow.""" if user_input is not None: - return self.async_create_entry(title="", data={}) + return self.async_create_entry(data={}) issue_registry = async_get_issue_registry(self.hass) description_placeholders = None diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index e703f0d09de..f5a0f0808da 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -6,7 +6,7 @@ from collections import defaultdict import logging import async_timeout -from rflink.protocol import create_rflink_connection +from rflink.protocol import ProtocolBase, create_rflink_connection from serial import SerialException import voluptuous as vol @@ -478,12 +478,16 @@ class RflinkCommand(RflinkDevice): # Keep repetition tasks to cancel if state is changed before repetitions # are sent - _repetition_task = None + _repetition_task: asyncio.Task[None] | None = None - _protocol = None + _protocol: ProtocolBase | None = None + + _wait_ack: bool | None = None @classmethod - def set_rflink_protocol(cls, protocol, wait_ack=None): + def set_rflink_protocol( + cls, protocol: ProtocolBase | None, wait_ack: bool | None = None + ) -> None: """Set the Rflink asyncio protocol as a class variable.""" cls._protocol = protocol if wait_ack is not None: diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index c32852fa448..de8a9fc6b8d 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -169,7 +169,7 @@ async def async_setup_internal(hass: HomeAssistant, entry: ConfigEntry) -> None: # Setup some per device config devices = _get_device_lookup(config[CONF_DEVICES]) - pt2262_devices: list[str] = [] + pt2262_devices: set[str] = set() device_registry = dr.async_get(hass) @@ -203,7 +203,7 @@ async def async_setup_internal(hass: HomeAssistant, entry: ConfigEntry) -> None: if event.device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: find_possible_pt2262_device(pt2262_devices, event.device.id_string) - pt2262_devices.append(event.device.id_string) + pt2262_devices.add(event.device.id_string) device_entry = device_registry.async_get_device( identifiers={(DOMAIN, *device_id)}, # type: ignore[arg-type] @@ -393,7 +393,7 @@ def get_device_data_bits( return data_bits -def find_possible_pt2262_device(device_ids: list[str], device_id: str) -> str | None: +def find_possible_pt2262_device(device_ids: set[str], device_id: str) -> str | None: """Look for the device which id matches the given device_id parameter.""" for dev_id in device_ids: if len(dev_id) == len(device_id): @@ -441,7 +441,7 @@ 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) + identifier = next((x for x in identifiers if x[0] == DOMAIN and len(x) == 4), None) if not identifier: return None # work around legacy identifier, being a multi tuple value diff --git a/homeassistant/components/ridwell/__init__.py b/homeassistant/components/ridwell/__init__.py index 5a9c19ed36f..116528f4ca8 100644 --- a/homeassistant/components/ridwell/__init__.py +++ b/homeassistant/components/ridwell/__init__.py @@ -1,88 +1,24 @@ """The Ridwell integration.""" from __future__ import annotations -import asyncio -from dataclasses import dataclass -from datetime import timedelta from typing import Any -from aioridwell import async_get_client -from aioridwell.errors import InvalidCredentialsError, RidwellError -from aioridwell.model import RidwellAccount, RidwellPickupEvent - from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.const import Platform 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.entity import EntityDescription -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers import entity_registry as er from .const import DOMAIN, LOGGER, SENSOR_TYPE_NEXT_PICKUP - -DEFAULT_UPDATE_INTERVAL = timedelta(hours=1) +from .coordinator import RidwellDataUpdateCoordinator PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.SWITCH] -@dataclass -class RidwellData: - """Define an object to be stored in `hass.data`.""" - - accounts: dict[str, RidwellAccount] - coordinator: DataUpdateCoordinator - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Ridwell from a config entry.""" - session = aiohttp_client.async_get_clientsession(hass) - - try: - client = await async_get_client( - entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session=session - ) - except InvalidCredentialsError as err: - raise ConfigEntryAuthFailed("Invalid username/password") from err - except RidwellError as err: - raise ConfigEntryNotReady(err) from err - - accounts = await client.async_get_accounts() - - async def async_update_data() -> dict[str, RidwellPickupEvent]: - """Get the latest pickup events.""" - data = {} - - async def async_get_pickups(account: RidwellAccount) -> None: - """Get the latest pickups for an account.""" - data[account.account_id] = await account.async_get_next_pickup_event() - - tasks = [async_get_pickups(account) for account in accounts.values()] - results = await asyncio.gather(*tasks, return_exceptions=True) - for result in results: - if isinstance(result, InvalidCredentialsError): - raise ConfigEntryAuthFailed("Invalid username/password") from result - if isinstance(result, RidwellError): - raise UpdateFailed(result) from result - - return data - - coordinator = DataUpdateCoordinator( - hass, - LOGGER, - name=entry.title, - update_interval=DEFAULT_UPDATE_INTERVAL, - update_method=async_update_data, - ) - - await coordinator.async_config_entry_first_refresh() - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = RidwellData( - accounts=accounts, coordinator=coordinator - ) + coordinator = RidwellDataUpdateCoordinator(hass, name=entry.title) + await coordinator.async_initialize() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -91,8 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) return unload_ok @@ -121,22 +56,3 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: LOGGER.info("Migration to version %s successful", version) return True - - -class RidwellEntity(CoordinatorEntity): - """Define a base Ridwell entity.""" - - _attr_has_entity_name = True - - def __init__( - self, - coordinator: DataUpdateCoordinator, - account: RidwellAccount, - description: EntityDescription, - ) -> None: - """Initialize the sensor.""" - super().__init__(coordinator) - - self._account = account - self._attr_unique_id = f"{account.account_id}_{description.key}" - self.entity_description = description diff --git a/homeassistant/components/ridwell/coordinator.py b/homeassistant/components/ridwell/coordinator.py new file mode 100644 index 00000000000..a3b83c70aae --- /dev/null +++ b/homeassistant/components/ridwell/coordinator.py @@ -0,0 +1,78 @@ +"""Define a Ridwell coordinator.""" +from __future__ import annotations + +import asyncio +from datetime import timedelta +from typing import cast + +from aioridwell.client import async_get_client +from aioridwell.errors import InvalidCredentialsError, RidwellError +from aioridwell.model import RidwellAccount, RidwellPickupEvent + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import LOGGER + +UPDATE_INTERVAL = timedelta(hours=1) + + +class RidwellDataUpdateCoordinator( + DataUpdateCoordinator[dict[str, RidwellPickupEvent]] +): + """Class to manage fetching data from single endpoint.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, *, name: str) -> None: + """Initialize global data updater.""" + # These will be filled in by async_initialize; we give them these defaults to + # avoid arduous typing checks down the line: + self.accounts: dict[str, RidwellAccount] = {} + self.dashboard_url = "" + self.user_id = "" + + super().__init__(hass, LOGGER, name=name, update_interval=UPDATE_INTERVAL) + + async def _async_update_data(self) -> dict[str, RidwellPickupEvent]: + """Fetch the latest data from the source.""" + data = {} + + async def async_get_pickups(account: RidwellAccount) -> None: + """Get the latest pickups for an account.""" + data[account.account_id] = await account.async_get_next_pickup_event() + + tasks = [async_get_pickups(account) for account in self.accounts.values()] + results = await asyncio.gather(*tasks, return_exceptions=True) + for result in results: + if isinstance(result, InvalidCredentialsError): + raise ConfigEntryAuthFailed("Invalid username/password") from result + if isinstance(result, RidwellError): + raise UpdateFailed(result) from result + + return data + + async def async_initialize(self) -> None: + """Initialize the coordinator.""" + session = aiohttp_client.async_get_clientsession(self.hass) + + try: + client = await async_get_client( + self.config_entry.data[CONF_USERNAME], + self.config_entry.data[CONF_PASSWORD], + session=session, + ) + except InvalidCredentialsError as err: + raise ConfigEntryAuthFailed("Invalid username/password") from err + except RidwellError as err: + raise ConfigEntryNotReady(err) from err + + self.accounts = await client.async_get_accounts() + await self.async_config_entry_first_refresh() + + self.dashboard_url = client.get_dashboard_url() + self.user_id = cast(str, client.user_id) diff --git a/homeassistant/components/ridwell/diagnostics.py b/homeassistant/components/ridwell/diagnostics.py index b4832770409..772efb87ac7 100644 --- a/homeassistant/components/ridwell/diagnostics.py +++ b/homeassistant/components/ridwell/diagnostics.py @@ -9,8 +9,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME from homeassistant.core import HomeAssistant -from . import RidwellData from .const import DOMAIN +from .coordinator import RidwellDataUpdateCoordinator CONF_TITLE = "title" @@ -27,14 +27,12 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - data: RidwellData = hass.data[DOMAIN][entry.entry_id] + coordinator: RidwellDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] return async_redact_data( { "entry": entry.as_dict(), - "data": [ - dataclasses.asdict(event) for event in data.coordinator.data.values() - ], + "data": [dataclasses.asdict(event) for event in coordinator.data.values()], }, TO_REDACT, ) diff --git a/homeassistant/components/ridwell/entity.py b/homeassistant/components/ridwell/entity.py new file mode 100644 index 00000000000..29dd68e2a81 --- /dev/null +++ b/homeassistant/components/ridwell/entity.py @@ -0,0 +1,40 @@ +"""Define a base Ridwell entity.""" +from aioridwell.model import RidwellAccount, RidwellPickupEvent + +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 +from .coordinator import RidwellDataUpdateCoordinator + + +class RidwellEntity(CoordinatorEntity[RidwellDataUpdateCoordinator]): + """Define a base Ridwell entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + coordinator: RidwellDataUpdateCoordinator, + account: RidwellAccount, + description: EntityDescription, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + + self._account = account + self._attr_device_info = DeviceInfo( + configuration_url=coordinator.dashboard_url, + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, coordinator.user_id)}, + manufacturer="Ridwell", + name="Ridwell", + ) + self._attr_unique_id = f"{account.account_id}_{description.key}" + self.entity_description = description + + @property + def next_pickup_event(self) -> RidwellPickupEvent: + """Get the next pickup event.""" + return self.coordinator.data[self._account.account_id] diff --git a/homeassistant/components/ridwell/manifest.json b/homeassistant/components/ridwell/manifest.json index 785457a57e0..b3dc5af39fa 100644 --- a/homeassistant/components/ridwell/manifest.json +++ b/homeassistant/components/ridwell/manifest.json @@ -3,7 +3,7 @@ "name": "Ridwell", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ridwell", - "requirements": ["aioridwell==2022.11.0"], + "requirements": ["aioridwell==2023.01.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling", "loggers": ["aioridwell"], diff --git a/homeassistant/components/ridwell/sensor.py b/homeassistant/components/ridwell/sensor.py index 9b7a4ab6954..05cee54ba9d 100644 --- a/homeassistant/components/ridwell/sensor.py +++ b/homeassistant/components/ridwell/sensor.py @@ -2,10 +2,10 @@ from __future__ import annotations from collections.abc import Mapping -from datetime import date, datetime +from datetime import date from typing import Any -from aioridwell.model import RidwellAccount, RidwellPickupEvent +from aioridwell.model import RidwellAccount from homeassistant.components.sensor import ( SensorDeviceClass, @@ -15,11 +15,10 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import RidwellData, RidwellEntity from .const import DOMAIN, SENSOR_TYPE_NEXT_PICKUP +from .coordinator import RidwellDataUpdateCoordinator +from .entity import RidwellEntity ATTR_CATEGORY = "category" ATTR_PICKUP_STATE = "pickup_state" @@ -37,13 +36,11 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Ridwell sensors based on a config entry.""" - data: RidwellData = hass.data[DOMAIN][entry.entry_id] + coordinator: RidwellDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - RidwellSensor(data.coordinator, account, SENSOR_DESCRIPTION) - for account in data.accounts.values() - ] + RidwellSensor(coordinator, account, SENSOR_DESCRIPTION) + for account in coordinator.accounts.values() ) @@ -52,7 +49,7 @@ class RidwellSensor(RidwellEntity, SensorEntity): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: RidwellDataUpdateCoordinator, account: RidwellAccount, description: SensorEntityDescription, ) -> None: @@ -64,14 +61,12 @@ class RidwellSensor(RidwellEntity, SensorEntity): @property def extra_state_attributes(self) -> Mapping[str, Any]: """Return entity specific state attributes.""" - event = self.coordinator.data[self._account.account_id] - attrs: dict[str, Any] = { ATTR_PICKUP_TYPES: {}, - ATTR_PICKUP_STATE: event.state.value, + ATTR_PICKUP_STATE: self.next_pickup_event.state.value, } - for pickup in event.pickups: + for pickup in self.next_pickup_event.pickups: if pickup.name not in attrs[ATTR_PICKUP_TYPES]: attrs[ATTR_PICKUP_TYPES][pickup.name] = { ATTR_CATEGORY: pickup.category.value, @@ -86,7 +81,6 @@ class RidwellSensor(RidwellEntity, SensorEntity): return attrs @property - def native_value(self) -> StateType | date | datetime: + def native_value(self) -> date: """Return the value reported by the sensor.""" - event: RidwellPickupEvent = self.coordinator.data[self._account.account_id] - return event.pickup_date + return self.next_pickup_event.pickup_date diff --git a/homeassistant/components/ridwell/switch.py b/homeassistant/components/ridwell/switch.py index d8e228be7db..f16bbaebab6 100644 --- a/homeassistant/components/ridwell/switch.py +++ b/homeassistant/components/ridwell/switch.py @@ -4,7 +4,7 @@ from __future__ import annotations from typing import Any from aioridwell.errors import RidwellError -from aioridwell.model import EventState, RidwellPickupEvent +from aioridwell.model import EventState from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.config_entries import ConfigEntry @@ -12,14 +12,15 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import RidwellData, RidwellEntity from .const import DOMAIN +from .coordinator import RidwellDataUpdateCoordinator +from .entity import RidwellEntity SWITCH_TYPE_OPT_IN = "opt_in" SWITCH_DESCRIPTION = SwitchEntityDescription( key=SWITCH_TYPE_OPT_IN, - name="Opt-In to Next Pickup", + name="Opt-in to next pickup", icon="mdi:calendar-check", ) @@ -28,13 +29,11 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up Ridwell sensors based on a config entry.""" - data: RidwellData = hass.data[DOMAIN][entry.entry_id] + coordinator: RidwellDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_entities( - [ - RidwellSwitch(data.coordinator, account, SWITCH_DESCRIPTION) - for account in data.accounts.values() - ] + RidwellSwitch(coordinator, account, SWITCH_DESCRIPTION) + for account in coordinator.accounts.values() ) @@ -44,15 +43,12 @@ class RidwellSwitch(RidwellEntity, SwitchEntity): @property def is_on(self) -> bool: """Return True if entity is on.""" - event: RidwellPickupEvent = self.coordinator.data[self._account.account_id] - return event.state == EventState.SCHEDULED + return self.next_pickup_event.state == EventState.SCHEDULED async def async_turn_off(self, **kwargs: Any) -> None: """Turn the switch off.""" - event: RidwellPickupEvent = self.coordinator.data[self._account.account_id] - try: - await event.async_opt_out() + await self.next_pickup_event.async_opt_out() except RidwellError as err: raise HomeAssistantError(f"Error while opting out: {err}") from err @@ -60,10 +56,8 @@ class RidwellSwitch(RidwellEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the switch on.""" - event: RidwellPickupEvent = self.coordinator.data[self._account.account_id] - try: - await event.async_opt_in() + await self.next_pickup_event.async_opt_in() except RidwellError as err: raise HomeAssistantError(f"Error while opting in: {err}") from err diff --git a/homeassistant/components/ridwell/translations/uk.json b/homeassistant/components/ridwell/translations/uk.json new file mode 100644 index 00000000000..f325a8cf2bc --- /dev/null +++ b/homeassistant/components/ridwell/translations/uk.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}:" + }, + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0441\u0432\u043e\u0454 \u0456\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c:" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index fc47ad7cbf0..e7463c6474e 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import datetime +from typing import Any from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, @@ -70,7 +71,7 @@ async def async_setup_entry( class RingBinarySensor(RingEntityMixin, BinarySensorEntity): """A binary sensor implementation for Ring device.""" - _active_alert = None + _active_alert: dict[str, Any] | None = None entity_description: RingBinarySensorEntityDescription def __init__( diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py index cca0c231d96..9425b2f98a4 100644 --- a/homeassistant/components/ring/config_flow.py +++ b/homeassistant/components/ring/config_flow.py @@ -1,11 +1,13 @@ """Config flow for Ring integration.""" import logging +from typing import Any from oauthlib.oauth2 import AccessDeniedError, MissingTokenError from ring_doorbell import Auth import voluptuous as vol -from homeassistant import config_entries, const, core, exceptions +from homeassistant import config_entries, core, exceptions +from homeassistant.const import __version__ as ha_version from . import DOMAIN @@ -15,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect.""" - auth = Auth(f"HomeAssistant/{const.__version__}") + auth = Auth(f"HomeAssistant/{ha_version}") try: token = await hass.async_add_executor_job( @@ -37,7 +39,7 @@ class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - user_pass = None + user_pass: dict[str, Any] = {} async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 10736c7b85c..c53f26a0e99 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Any from homeassistant.components.sensor import ( SensorDeviceClass, @@ -123,7 +124,7 @@ class HealthDataRingSensor(RingSensor): class HistoryRingSensor(RingSensor): """Ring sensor that relies on history data.""" - _latest_event = None + _latest_event: dict[str, Any] | None = None async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/homeassistant/components/ring/translations/lt.json b/homeassistant/components/ring/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/ring/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/translations/lv.json b/homeassistant/components/ring/translations/lv.json index 2c205bdd324..89513ca5a07 100644 --- a/homeassistant/components/ring/translations/lv.json +++ b/homeassistant/components/ring/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/risco/translations/nl.json b/homeassistant/components/risco/translations/nl.json index e16f43d1767..ed59ffe1197 100644 --- a/homeassistant/components/risco/translations/nl.json +++ b/homeassistant/components/risco/translations/nl.json @@ -22,6 +22,12 @@ "pin": "Pincode", "port": "Poort" } + }, + "user": { + "menu_options": { + "cloud": "Risco Cloud (aanbevolen)", + "local": "Lokaal Risco paneel (geavanceerd)" + } } } }, diff --git a/homeassistant/components/risco/translations/uk.json b/homeassistant/components/risco/translations/uk.json index 794fcfdbcda..306bb7a5ab5 100644 --- a/homeassistant/components/risco/translations/uk.json +++ b/homeassistant/components/risco/translations/uk.json @@ -7,6 +7,13 @@ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "cloud": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } } }, "options": { diff --git a/homeassistant/components/rituals_perfume_genie/translations/lv.json b/homeassistant/components/rituals_perfume_genie/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/rituals_perfume_genie/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/helpers.py b/homeassistant/components/roku/helpers.py index 6b3c02a5fab..22e9ee19b16 100644 --- a/homeassistant/components/roku/helpers.py +++ b/homeassistant/components/roku/helpers.py @@ -3,10 +3,9 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine from functools import wraps -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from rokuecp import RokuConnectionError, RokuConnectionTimeoutError, RokuError -from typing_extensions import Concatenate, ParamSpec from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/roku/translations/lv.json b/homeassistant/components/roku/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/roku/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/el.json b/homeassistant/components/roomba/translations/el.json index cb8683f2be2..e045581eca7 100644 --- a/homeassistant/components/roomba/translations/el.json +++ b/homeassistant/components/roomba/translations/el.json @@ -12,14 +12,14 @@ "flow_title": "{name} ({host})", "step": { "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.", + "description": "\u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae iRobot \u03b4\u03b5\u03bd \u03b5\u03ba\u03c4\u03b5\u03bb\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03b5 \u03ba\u03b1\u03bc\u03af\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \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 \u0391\u03c1\u03c7\u03b9\u03ba\u03ae \u03c3\u03b5\u03bb\u03af\u03b4\u03b1 \u03c3\u03c4\u03bf {name} \u03ad\u03c9\u03c2 \u03cc\u03c4\u03bf\u03c5 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \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 \u03bc\u03b5\u03c4\u03ac \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}", + "description": "\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae iRobot \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b1\u03bd\u03bf\u03b9\u03c7\u03c4\u03ae \u03c3\u03b5 \u03ba\u03b1\u03bc\u03af\u03b1 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03bd\u03ce \u03c0\u03c1\u03bf\u03c3\u03c0\u03b1\u03b8\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2. \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": { diff --git a/homeassistant/components/roomba/translations/hu.json b/homeassistant/components/roomba/translations/hu.json index e8597a0cd38..cc292245d6c 100644 --- a/homeassistant/components/roomba/translations/hu.json +++ b/homeassistant/components/roomba/translations/hu.json @@ -19,7 +19,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "A jelsz\u00f3t nem siker\u00fclt automatikusan lek\u00e9rdezni a k\u00e9sz\u00fcl\u00e9kr\u0151l. K\u00e9rj\u00fck, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy az iRobot alkalmaz\u00e1s nincs nyitva egyik eszk\u00f6z\u00f6n sem, mik\u00f6zben megpr\u00f3b\u00e1lja lek\u00e9rdezni a jelsz\u00f3t. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3ban le\u00edrt l\u00e9p\u00e9seket a k\u00f6vetkez\u0151 c\u00edmen: {auth_help_url}", + "description": "A jelsz\u00f3t nem siker\u00fclt automatikusan lek\u00e9rdezni a k\u00e9sz\u00fcl\u00e9kr\u0151l. K\u00e9rem, gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy az iRobot alkalmaz\u00e1s nincs nyitva egyik eszk\u00f6z\u00f6n sem, mik\u00f6zben megpr\u00f3b\u00e1lja lek\u00e9rdezni a jelsz\u00f3t. K\u00f6vesse a dokument\u00e1ci\u00f3ban le\u00edrt l\u00e9p\u00e9seket a k\u00f6vetkez\u0151 c\u00edmen: {auth_help_url}", "title": "Jelsz\u00f3 megad\u00e1sa" }, "manual": { diff --git a/homeassistant/components/roomba/translations/lv.json b/homeassistant/components/roomba/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/roomba/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/uk.json b/homeassistant/components/roomba/translations/uk.json index 722b1127051..107c48f771e 100644 --- a/homeassistant/components/roomba/translations/uk.json +++ b/homeassistant/components/roomba/translations/uk.json @@ -4,6 +4,15 @@ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" }, "step": { + "link": { + "title": "\u041e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u043f\u0430\u0440\u043e\u043b\u044c" + }, + "link_manual": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index 272c353601e..97faa1390f6 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -3,7 +3,7 @@ "name": "RoonLabs music player", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roon", - "requirements": ["roonapi==0.1.2"], + "requirements": ["roonapi==0.1.3"], "codeowners": ["@pavoni"], "iot_class": "local_push", "loggers": ["roonapi"] diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index ab811e0853b..09ecc3cec9f 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -180,13 +180,16 @@ class RoonDevice(MediaPlayerEntity): volume_data = player_data["volume"] volume_muted = volume_data["is_muted"] volume_step = convert(volume_data["step"], int, 0) + volume_max = volume_data["max"] + volume_min = volume_data["min"] + raw_level = convert(volume_data["value"], float, 0) - if volume_data["type"] == "db": - level = convert(volume_data["value"], float, 0.0) / 80 * 100 + 100 - else: - level = convert(volume_data["value"], float, 0.0) + volume_range = volume_max - volume_min + volume_percentage_factor = volume_range / 100 + level = (raw_level - volume_min) / volume_percentage_factor volume_level = convert(level, int, 0) / 100 + except KeyError: # catch KeyError pass @@ -329,7 +332,7 @@ class RoonDevice(MediaPlayerEntity): def set_volume_level(self, volume: float) -> None: """Send new volume_level to device.""" volume = int(volume * 100) - self._server.roonapi.change_volume(self.output_id, volume) + self._server.roonapi.set_volume_percent(self.output_id, volume) def mute_volume(self, mute=True): """Send mute/unmute to device.""" @@ -337,11 +340,11 @@ class RoonDevice(MediaPlayerEntity): def volume_up(self) -> None: """Send new volume_level to device.""" - self._server.roonapi.change_volume(self.output_id, 3, "relative") + self._server.roonapi.change_volume_percent(self.output_id, 3) def volume_down(self) -> None: """Send new volume_level to device.""" - self._server.roonapi.change_volume(self.output_id, -3, "relative") + self._server.roonapi.change_volume_percent(self.output_id, -3) def turn_on(self) -> None: """Turn on device (if supported).""" diff --git a/homeassistant/components/roon/translations/pl.json b/homeassistant/components/roon/translations/pl.json index 068153e2572..1d1e8853035 100644 --- a/homeassistant/components/roon/translations/pl.json +++ b/homeassistant/components/roon/translations/pl.json @@ -18,6 +18,12 @@ "link": { "description": "Musisz autoryzowa\u0107 Home Assistant w Roon. Po klikni\u0119ciu przycisku \"Zatwierd\u017a\", przejd\u017a do aplikacji Roon Core, otw\u00f3rz \"Ustawienia\" i w\u0142\u0105cz Home Assistant w karcie \"Rozszerzenia\" (Extensions).", "title": "Autoryzuj Home Assistant w Roon" + }, + "user": { + "few": "Pustych", + "many": "Pustych", + "one": "Pusty", + "other": "Puste" } } } diff --git a/homeassistant/components/rpi_power/translations/tr.json b/homeassistant/components/rpi_power/translations/tr.json index 5ad414a1be8..0c463362eb3 100644 --- a/homeassistant/components/rpi_power/translations/tr.json +++ b/homeassistant/components/rpi_power/translations/tr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } }, diff --git a/homeassistant/components/rss_feed_template/__init__.py b/homeassistant/components/rss_feed_template/__init__.py index 4dcbf7fe048..27a67fd6640 100644 --- a/homeassistant/components/rss_feed_template/__init__.py +++ b/homeassistant/components/rss_feed_template/__init__.py @@ -1,4 +1,6 @@ """Support to export sensor values via RSS feed.""" +from __future__ import annotations + from html import escape from aiohttp import web @@ -7,6 +9,7 @@ import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType CONTENT_TYPE_XML = "text/xml" @@ -43,12 +46,13 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: for (feeduri, feedconfig) in config[DOMAIN].items(): url = f"/api/rss_template/{feeduri}" - requires_auth = feedconfig.get("requires_api_password") + requires_auth: bool = feedconfig["requires_api_password"] + title: Template | None if (title := feedconfig.get("title")) is not None: title.hass = hass - items = feedconfig.get("items") + items: list[dict[str, Template]] = feedconfig["items"] for item in items: if "title" in item: item["title"].hass = hass @@ -64,20 +68,22 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: class RssView(HomeAssistantView): """Export states and other values as RSS.""" - requires_auth = True - url = None name = "rss_template" - _title = None - _items = None - def __init__(self, url, requires_auth, title, items): + def __init__( + self, + url: str, + requires_auth: bool, + title: Template | None, + items: list[dict[str, Template]], + ) -> None: """Initialize the rss view.""" self.url = url self.requires_auth = requires_auth self._title = title self._items = items - async def get(self, request, entity_id=None): + async def get(self, request: web.Request) -> web.Response: """Generate the RSS view XML.""" response = '\n\n' diff --git a/homeassistant/components/rtsp_to_webrtc/translations/nl.json b/homeassistant/components/rtsp_to_webrtc/translations/nl.json index 6454c48c22f..7395577b572 100644 --- a/homeassistant/components/rtsp_to_webrtc/translations/nl.json +++ b/homeassistant/components/rtsp_to_webrtc/translations/nl.json @@ -23,5 +23,14 @@ "title": "Configureer RTSPtoWebRTC" } } + }, + "options": { + "step": { + "init": { + "data": { + "stun_server": "Stun serveradres (host:poort)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/__init__.py b/homeassistant/components/ruuvi_gateway/__init__.py new file mode 100644 index 00000000000..59d37abbf7b --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/__init__.py @@ -0,0 +1,42 @@ +"""The Ruuvi Gateway integration.""" +from __future__ import annotations + +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_TOKEN +from homeassistant.core import HomeAssistant + +from .bluetooth import async_connect_scanner +from .const import DOMAIN, SCAN_INTERVAL +from .coordinator import RuuviGatewayUpdateCoordinator +from .models import RuuviGatewayRuntimeData + +_LOGGER = logging.getLogger(DOMAIN) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Ruuvi Gateway from a config entry.""" + coordinator = RuuviGatewayUpdateCoordinator( + hass, + logger=_LOGGER, + name=entry.title, + update_interval=SCAN_INTERVAL, + host=entry.data[CONF_HOST], + token=entry.data[CONF_TOKEN], + ) + scanner, unload_scanner = async_connect_scanner(hass, entry, coordinator) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = RuuviGatewayRuntimeData( + update_coordinator=coordinator, + scanner=scanner, + ) + entry.async_on_unload(unload_scanner) + 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, []): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/ruuvi_gateway/bluetooth.py b/homeassistant/components/ruuvi_gateway/bluetooth.py new file mode 100644 index 00000000000..f5748b8b4e9 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/bluetooth.py @@ -0,0 +1,103 @@ +"""Bluetooth support for Ruuvi Gateway.""" +from __future__ import annotations + +from collections.abc import Callable +import datetime +import logging + +from home_assistant_bluetooth import BluetoothServiceInfoBleak + +from homeassistant.components.bluetooth import ( + BaseHaRemoteScanner, + async_get_advertisement_callback, + async_register_scanner, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback + +from .const import OLD_ADVERTISEMENT_CUTOFF +from .coordinator import RuuviGatewayUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +class RuuviGatewayScanner(BaseHaRemoteScanner): + """Scanner for Ruuvi Gateway.""" + + def __init__( + self, + hass: HomeAssistant, + scanner_id: str, + name: str, + new_info_callback: Callable[[BluetoothServiceInfoBleak], None], + *, + coordinator: RuuviGatewayUpdateCoordinator, + ) -> None: + """Initialize the scanner, using the given update coordinator as data source.""" + super().__init__( + hass, + scanner_id, + name, + new_info_callback, + connector=None, + connectable=False, + ) + self.coordinator = coordinator + + @callback + def _async_handle_new_data(self) -> None: + now = datetime.datetime.now() + for tag_data in self.coordinator.data: + if now - tag_data.datetime > OLD_ADVERTISEMENT_CUTOFF: + # Don't process data that is older than 10 minutes + continue + anno = tag_data.parse_announcement() + self._async_on_advertisement( + address=tag_data.mac, + rssi=tag_data.rssi, + local_name=anno.local_name, + service_data=anno.service_data, + service_uuids=anno.service_uuids, + manufacturer_data=anno.manufacturer_data, + tx_power=anno.tx_power, + details={}, + ) + + @callback + def start_polling(self) -> CALLBACK_TYPE: + """Start polling; return a callback to stop polling.""" + return self.coordinator.async_add_listener(self._async_handle_new_data) + + +def async_connect_scanner( + hass: HomeAssistant, + entry: ConfigEntry, + coordinator: RuuviGatewayUpdateCoordinator, +) -> tuple[RuuviGatewayScanner, CALLBACK_TYPE]: + """Connect scanner and start polling.""" + assert entry.unique_id is not None + source = str(entry.unique_id) + _LOGGER.debug( + "%s [%s]: Connecting scanner", + entry.title, + source, + ) + scanner = RuuviGatewayScanner( + hass=hass, + scanner_id=source, + name=entry.title, + new_info_callback=async_get_advertisement_callback(hass), + coordinator=coordinator, + ) + unload_callbacks = [ + async_register_scanner(hass, scanner, connectable=False), + scanner.async_setup(), + scanner.start_polling(), + ] + + @callback + def _async_unload() -> None: + for unloader in unload_callbacks: + unloader() + + return (scanner, _async_unload) diff --git a/homeassistant/components/ruuvi_gateway/config_flow.py b/homeassistant/components/ruuvi_gateway/config_flow.py new file mode 100644 index 00000000000..178c55a53e4 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/config_flow.py @@ -0,0 +1,89 @@ +"""Config flow for Ruuvi Gateway integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import aioruuvigateway.api as gw_api +from aioruuvigateway.excs import CannotConnect, InvalidAuth + +from homeassistant import config_entries +from homeassistant.components import dhcp +from homeassistant.const import CONF_HOST, CONF_TOKEN +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.httpx_client import get_async_client + +from . import DOMAIN +from .schemata import CONFIG_SCHEMA, get_config_schema_with_default_host + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Ruuvi Gateway.""" + + VERSION = 1 + + def __init__(self) -> None: + """Initialize the config flow.""" + super().__init__() + self.config_schema = CONFIG_SCHEMA + + async def _async_validate( + self, + user_input: dict[str, Any], + ) -> tuple[FlowResult | None, dict[str, str]]: + """Validate configuration (either discovered or user input).""" + errors: dict[str, str] = {} + + try: + async with get_async_client(self.hass) as client: + resp = await gw_api.get_gateway_history_data( + client, + host=user_input[CONF_HOST], + bearer_token=user_input[CONF_TOKEN], + ) + await self.async_set_unique_id( + format_mac(resp.gw_mac), raise_on_progress=False + ) + self._abort_if_unique_id_configured( + updates={CONF_HOST: user_input[CONF_HOST]} + ) + info = {"title": f"Ruuvi Gateway {resp.gw_mac_suffix}"} + return ( + self.async_create_entry(title=info["title"], data=user_input), + errors, + ) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + return (None, errors) + + async def async_step_user( + self, + user_input: dict[str, Any] | None = None, + ) -> FlowResult: + """Handle requesting or validating user input.""" + if user_input is not None: + result, errors = await self._async_validate(user_input) + else: + result, errors = None, {} + if result is not None: + return result + return self.async_show_form( + step_id="user", + data_schema=self.config_schema, + errors=(errors or None), + ) + + async def async_step_dhcp(self, discovery_info: dhcp.DhcpServiceInfo) -> FlowResult: + """Prepare configuration for a DHCP discovered Ruuvi Gateway.""" + await self.async_set_unique_id(format_mac(discovery_info.macaddress)) + self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip}) + self.config_schema = get_config_schema_with_default_host(host=discovery_info.ip) + return await self.async_step_user() diff --git a/homeassistant/components/ruuvi_gateway/const.py b/homeassistant/components/ruuvi_gateway/const.py new file mode 100644 index 00000000000..609bad9a226 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/const.py @@ -0,0 +1,12 @@ +"""Constants for the Ruuvi Gateway integration.""" +from datetime import timedelta + +from homeassistant.components.bluetooth import ( + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, +) + +DOMAIN = "ruuvi_gateway" +SCAN_INTERVAL = timedelta(seconds=5) +OLD_ADVERTISEMENT_CUTOFF = timedelta( + seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS +) diff --git a/homeassistant/components/ruuvi_gateway/coordinator.py b/homeassistant/components/ruuvi_gateway/coordinator.py new file mode 100644 index 00000000000..38bc3b0e201 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/coordinator.py @@ -0,0 +1,49 @@ +"""Update coordinator for Ruuvi Gateway.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from aioruuvigateway.api import get_gateway_history_data +from aioruuvigateway.models import TagData + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + + +class RuuviGatewayUpdateCoordinator(DataUpdateCoordinator[list[TagData]]): + """Polls the gateway for data and returns a list of TagData objects that have changed since the last poll.""" + + def __init__( + self, + hass: HomeAssistant, + logger: logging.Logger, + *, + name: str, + update_interval: timedelta | None = None, + host: str, + token: str, + ) -> None: + """Initialize the coordinator using the given configuration (host, token).""" + super().__init__(hass, logger, name=name, update_interval=update_interval) + self.host = host + self.token = token + self.last_tag_datas: dict[str, TagData] = {} + + async def _async_update_data(self) -> list[TagData]: + changed_tag_datas: list[TagData] = [] + async with get_async_client(self.hass) as client: + data = await get_gateway_history_data( + client, + host=self.host, + bearer_token=self.token, + ) + for tag in data.tags: + if ( + tag.mac not in self.last_tag_datas + or self.last_tag_datas[tag.mac].data != tag.data + ): + changed_tag_datas.append(tag) + self.last_tag_datas[tag.mac] = tag + return changed_tag_datas diff --git a/homeassistant/components/ruuvi_gateway/manifest.json b/homeassistant/components/ruuvi_gateway/manifest.json new file mode 100644 index 00000000000..468d81fba20 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/manifest.json @@ -0,0 +1,15 @@ +{ + "domain": "ruuvi_gateway", + "name": "Ruuvi Gateway", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ruuvi_gateway", + "codeowners": ["@akx"], + "dependencies": ["bluetooth"], + "requirements": ["aioruuvigateway==0.0.2"], + "iot_class": "local_polling", + "dhcp": [ + { + "hostname": "ruuvigateway*" + } + ] +} diff --git a/homeassistant/components/ruuvi_gateway/models.py b/homeassistant/components/ruuvi_gateway/models.py new file mode 100644 index 00000000000..adb405f0bf8 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/models.py @@ -0,0 +1,15 @@ +"""Models for Ruuvi Gateway integration.""" +from __future__ import annotations + +import dataclasses + +from .bluetooth import RuuviGatewayScanner +from .coordinator import RuuviGatewayUpdateCoordinator + + +@dataclasses.dataclass(frozen=True) +class RuuviGatewayRuntimeData: + """Runtime data for Ruuvi Gateway integration.""" + + update_coordinator: RuuviGatewayUpdateCoordinator + scanner: RuuviGatewayScanner diff --git a/homeassistant/components/ruuvi_gateway/schemata.py b/homeassistant/components/ruuvi_gateway/schemata.py new file mode 100644 index 00000000000..eec86cd129f --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/schemata.py @@ -0,0 +1,18 @@ +"""Schemata for ruuvi_gateway.""" +from __future__ import annotations + +import voluptuous as vol + +from homeassistant.const import CONF_HOST, CONF_TOKEN + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_TOKEN): str, + } +) + + +def get_config_schema_with_default_host(host: str) -> vol.Schema: + """Return a config schema with a default host.""" + return CONFIG_SCHEMA.extend({vol.Required(CONF_HOST, default=host): str}) diff --git a/homeassistant/components/ruuvi_gateway/strings.json b/homeassistant/components/ruuvi_gateway/strings.json new file mode 100644 index 00000000000..10b149c9069 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Host (IP address or DNS name)", + "token": "Bearer token (configured during gateway setup)" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + } + } +} diff --git a/homeassistant/components/ruuvi_gateway/translations/bg.json b/homeassistant/components/ruuvi_gateway/translations/bg.json new file mode 100644 index 00000000000..7c26d1b45cd --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/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\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f", + "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 (IP \u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 DNS \u0438\u043c\u0435)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/ca.json b/homeassistant/components/ruuvi_gateway/translations/ca.json new file mode 100644 index 00000000000..7a173e20be1 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3 (adre\u00e7a IP o nom del DNS)", + "token": "Token del portador (configurat durant la configuraci\u00f3 de la passarel\u00b7la)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/cs.json b/homeassistant/components/ruuvi_gateway/translations/cs.json new file mode 100644 index 00000000000..e1bf8e7f45f --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/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/ruuvi_gateway/translations/de.json b/homeassistant/components/ruuvi_gateway/translations/de.json new file mode 100644 index 00000000000..db2794cbf66 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host (IP-Adresse oder DNS-Name)", + "token": "Bearer-Token (w\u00e4hrend der Gateway-Einrichtung konfiguriert)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/el.json b/homeassistant/components/ruuvi_gateway/translations/el.json new file mode 100644 index 00000000000..1ac264fa53e --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/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": { + "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2 (\u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03ae \u03cc\u03bd\u03bf\u03bc\u03b1 DNS)", + "token": "\u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c6\u03bf\u03c1\u03ad\u03b1 (\u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03b8\u03b7\u03ba\u03b5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c0\u03cd\u03bb\u03b7\u03c2)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/en-GB.json b/homeassistant/components/ruuvi_gateway/translations/en-GB.json new file mode 100644 index 00000000000..edbf085ea12 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/en-GB.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "token": "Bearer token (configured during gateway setup)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/en.json b/homeassistant/components/ruuvi_gateway/translations/en.json new file mode 100644 index 00000000000..519623e32ce --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host (IP address or DNS name)", + "token": "Bearer token (configured during gateway setup)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/es.json b/homeassistant/components/ruuvi_gateway/translations/es.json new file mode 100644 index 00000000000..194edd0c577 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host (direcci\u00f3n IP o nombre DNS)", + "token": "Token de portador (configurado durante la configuraci\u00f3n de la puerta de enlace)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/et.json b/homeassistant/components/ruuvi_gateway/translations/et.json new file mode 100644 index 00000000000..19246bb6007 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Konto on juba seadistatud" + }, + "error": { + "cannot_connect": "\u00dchhendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host (IP-aadress v\u00f5i DNS-nimi)", + "token": "Kandja tunnus (konfigureeritud l\u00fc\u00fcsi seadistamise ajal)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/fr.json b/homeassistant/components/ruuvi_gateway/translations/fr.json new file mode 100644 index 00000000000..0eb22e7d789 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te (adresse IP ou nom DNS)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/he.json b/homeassistant/components/ruuvi_gateway/translations/he.json new file mode 100644 index 00000000000..f0cfd532711 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/he.json @@ -0,0 +1,12 @@ +{ + "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", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/hu.json b/homeassistant/components/ruuvi_gateway/translations/hu.json new file mode 100644 index 00000000000..f3b7bcc4719 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/hu.json @@ -0,0 +1,20 @@ +{ + "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", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm (IP c\u00edm vagy hosztn\u00e9v)", + "token": "Bearer token (az \u00e1tj\u00e1r\u00f3 be\u00e1ll\u00edt\u00e1sa sor\u00e1n konfigur\u00e1lt)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/id.json b/homeassistant/components/ruuvi_gateway/translations/id.json new file mode 100644 index 00000000000..62376acd5ef --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Akun sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_auth": "Autentikasi tidak valid", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host (alamat IP atau nama DNS)", + "token": "Token pembawa (dikonfigurasi selama penyiapan gateway)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/it.json b/homeassistant/components/ruuvi_gateway/translations/it.json new file mode 100644 index 00000000000..297497864ea --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host (indirizzo IP o nome DNS)", + "token": "Token di connessione (configurato durante l'installazione del gateway)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/ja.json b/homeassistant/components/ruuvi_gateway/translations/ja.json new file mode 100644 index 00000000000..1df5b6944a1 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/ja.json @@ -0,0 +1,12 @@ +{ + "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", + "unknown": "\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/nl.json b/homeassistant/components/ruuvi_gateway/translations/nl.json new file mode 100644 index 00000000000..fa372723ce5 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/nl.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Account is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/no.json b/homeassistant/components/ruuvi_gateway/translations/no.json new file mode 100644 index 00000000000..93d05495576 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert (IP-adresse eller DNS-navn)", + "token": "B\u00e6rer-token (konfigureres under gateway-oppsett)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/pl.json b/homeassistant/components/ruuvi_gateway/translations/pl.json new file mode 100644 index 00000000000..d9bc1253579 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/pl.json @@ -0,0 +1,20 @@ +{ + "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", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Host (Adres IP lub nazwa DNS)", + "token": "Token okaziciela (skonfigurowany podczas konfiguracji bramki)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/pt-BR.json b/homeassistant/components/ruuvi_gateway/translations/pt-BR.json new file mode 100644 index 00000000000..436cef76863 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "A conta j\u00e1 est\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host (endere\u00e7o IP ou nome DNS)", + "token": "Token do portador (configurado durante a configura\u00e7\u00e3o do gateway)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/ru.json b/homeassistant/components/ruuvi_gateway/translations/ru.json new file mode 100644 index 00000000000..4b53bcad327 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/ru.json @@ -0,0 +1,20 @@ +{ + "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.", + "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 (IP-\u0430\u0434\u0440\u0435\u0441 \u0438\u043b\u0438 DNS-\u0438\u043c\u044f)", + "token": "\u0422\u043e\u043a\u0435\u043d \u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044f (\u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0448\u043b\u044e\u0437\u0430)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/sk.json b/homeassistant/components/ruuvi_gateway/translations/sk.json new file mode 100644 index 00000000000..a92196794ef --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/sk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je u\u017e nakonfigurovan\u00fd" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_auth": "Neplatn\u00e9 overenie", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e (IP adresa alebo n\u00e1zov DNS)", + "token": "Bearer token (konfigurovan\u00fd po\u010das nastavenia br\u00e1ny)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/tr.json b/homeassistant/components/ruuvi_gateway/translations/tr.json new file mode 100644 index 00000000000..541a46cac9b --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana bilgisayar (IP adresi veya DNS ad\u0131)", + "token": "Ta\u015f\u0131y\u0131c\u0131 anahtar (a\u011f ge\u00e7idi kurulumu s\u0131ras\u0131nda yap\u0131land\u0131r\u0131l\u0131r)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/uk.json b/homeassistant/components/ruuvi_gateway/translations/uk.json new file mode 100644 index 00000000000..de32e6568a7 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u041e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442 (IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u0430\u0431\u043e \u0456\u043c'\u044f DNS)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvi_gateway/translations/zh-Hant.json b/homeassistant/components/ruuvi_gateway/translations/zh-Hant.json new file mode 100644 index 00000000000..34a9e324b09 --- /dev/null +++ b/homeassistant/components/ruuvi_gateway/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "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", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef\uff08IP \u4f4d\u5740\u6216 DNS \u540d\u7a31\uff09", + "token": "Bearer \u5bc6\u9470\uff08\u65bc\u8a2d\u5b9a\u9598\u9053\u5668\u6642\u8a2d\u5b9a\uff09" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruuvitag_ble/manifest.json b/homeassistant/components/ruuvitag_ble/manifest.json index f7207f685c9..45bc0fba70f 100644 --- a/homeassistant/components/ruuvitag_ble/manifest.json +++ b/homeassistant/components/ruuvitag_ble/manifest.json @@ -14,7 +14,7 @@ } ], "requirements": ["ruuvitag-ble==0.1.1"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@akx"], "iot_class": "local_push" } diff --git a/homeassistant/components/ruuvitag_ble/sensor.py b/homeassistant/components/ruuvitag_ble/sensor.py index bd21e479abf..128edd42b19 100644 --- a/homeassistant/components/ruuvitag_ble/sensor.py +++ b/homeassistant/components/ruuvitag_ble/sensor.py @@ -1,8 +1,6 @@ """Support for RuuviTag sensors.""" from __future__ import annotations -from typing import Optional, Union - from sensor_state_data import ( DeviceKey, SensorDescription, @@ -77,7 +75,7 @@ SENSOR_DESCRIPTIONS = { ), (SSDSensorDeviceClass.COUNT, None): SensorEntityDescription( key="movement_counter", - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL_INCREASING, entity_registry_enabled_default=False, ), } @@ -143,9 +141,7 @@ async def async_setup_entry( class RuuvitagBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a Ruuvitag BLE sensor.""" diff --git a/homeassistant/components/rympro/__init__.py b/homeassistant/components/rympro/__init__.py new file mode 100644 index 00000000000..2e474be6a6a --- /dev/null +++ b/homeassistant/components/rympro/__init__.py @@ -0,0 +1,82 @@ +"""The Read Your Meter Pro integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from pyrympro import CannotConnectError, OperationError, RymPro, UnauthorizedError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TOKEN, 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, UpdateFailed + +from .const import DOMAIN + +SCAN_INTERVAL = 60 * 60 +PLATFORMS: list[Platform] = [Platform.SENSOR] +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Read Your Meter Pro from a config entry.""" + data = entry.data + rympro = RymPro(async_get_clientsession(hass)) + rympro.set_token(data[CONF_TOKEN]) + try: + await rympro.account_info() + except CannotConnectError as error: + raise ConfigEntryNotReady from error + except UnauthorizedError: + try: + token = await rympro.login(data[CONF_EMAIL], data[CONF_PASSWORD], "ha") + hass.config_entries.async_update_entry( + entry, + data={**data, CONF_TOKEN: token}, + ) + except UnauthorizedError as error: + raise ConfigEntryAuthFailed from error + + coordinator = RymProDataUpdateCoordinator(hass, rympro, SCAN_INTERVAL) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class RymProDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching RYM Pro data.""" + + def __init__(self, hass: HomeAssistant, rympro: RymPro, scan_interval: int) -> None: + """Initialize global RymPro data updater.""" + self.rympro = rympro + interval = timedelta(seconds=scan_interval) + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=interval, + ) + + async def _async_update_data(self): + """Fetch data from Rym Pro.""" + try: + return await self.rympro.last_read() + except UnauthorizedError: + await self.hass.config_entries.async_reload(self.config_entry.entry_id) + except (CannotConnectError, OperationError) as error: + raise UpdateFailed(error) from error diff --git a/homeassistant/components/rympro/config_flow.py b/homeassistant/components/rympro/config_flow.py new file mode 100644 index 00000000000..b954bb10c57 --- /dev/null +++ b/homeassistant/components/rympro/config_flow.py @@ -0,0 +1,100 @@ +"""Config flow for Read Your Meter Pro integration.""" +from __future__ import annotations + +from collections.abc import Mapping +import logging +from typing import Any + +from pyrympro import CannotConnectError, RymPro, UnauthorizedError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TOKEN, CONF_UNIQUE_ID +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 + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_EMAIL): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +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 STEP_USER_DATA_SCHEMA with values provided by the user. + """ + + rympro = RymPro(async_get_clientsession(hass)) + + token = await rympro.login(data[CONF_EMAIL], data[CONF_PASSWORD], "ha") + + info = await rympro.account_info() + + return {CONF_TOKEN: token, CONF_UNIQUE_ID: info["accountNumber"]} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Read Your Meter Pro.""" + + VERSION = 1 + + def __init__(self) -> None: + """Init the config flow.""" + self._reauth_entry: config_entries.ConfigEntry | None = None + + 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: + info = await validate_input(self.hass, user_input) + except CannotConnectError: + errors["base"] = "cannot_connect" + except UnauthorizedError: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + title = user_input[CONF_EMAIL] + data = {**user_input, **info} + + if not self._reauth_entry: + await self.async_set_unique_id(info[CONF_UNIQUE_ID]) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=title, data=data) + + self.hass.config_entries.async_update_entry( + self._reauth_entry, + title=title, + data=data, + unique_id=info[CONF_UNIQUE_ID], + ) + 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="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle configuration by re-auth.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_user() diff --git a/homeassistant/components/rympro/const.py b/homeassistant/components/rympro/const.py new file mode 100644 index 00000000000..ed7e2801a1b --- /dev/null +++ b/homeassistant/components/rympro/const.py @@ -0,0 +1,3 @@ +"""Constants for the Read Your Meter Pro integration.""" + +DOMAIN = "rympro" diff --git a/homeassistant/components/rympro/manifest.json b/homeassistant/components/rympro/manifest.json new file mode 100644 index 00000000000..9079a781e51 --- /dev/null +++ b/homeassistant/components/rympro/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "rympro", + "name": "Read Your Meter Pro", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/rympro", + "requirements": ["pyrympro==0.0.4"], + "codeowners": ["@OnFreund"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/rympro/sensor.py b/homeassistant/components/rympro/sensor.py new file mode 100644 index 00000000000..7360f930d30 --- /dev/null +++ b/homeassistant/components/rympro/sensor.py @@ -0,0 +1,70 @@ +"""Sensor for RymPro meters.""" +from __future__ import annotations + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfVolume +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import RymProDataUpdateCoordinator +from .const import DOMAIN + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up sensors for device.""" + coordinator: RymProDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + async_add_entities( + [ + RymProSensor(coordinator, meter_id, meter["read"], config_entry.entry_id) + for meter_id, meter in coordinator.data.items() + ] + ) + + +class RymProSensor(CoordinatorEntity[RymProDataUpdateCoordinator], SensorEntity): + """Sensor for RymPro meters.""" + + _attr_has_entity_name = True + _attr_name = "Last Read" + _attr_device_class = SensorDeviceClass.WATER + _attr_native_unit_of_measurement = UnitOfVolume.CUBIC_METERS + _attr_state_class = SensorStateClass.TOTAL_INCREASING + + def __init__( + self, + coordinator: RymProDataUpdateCoordinator, + meter_id: int, + last_read: int, + entry_id: str, + ) -> None: + """Initialize sensor.""" + super().__init__(coordinator) + self._meter_id = meter_id + self._entity_registry: er.EntityRegistry | None = None + unique_id = f"{entry_id}_{meter_id}" + self._attr_unique_id = f"{unique_id}_last_read" + self._attr_extra_state_attributes = {"meter_id": str(meter_id)} + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, unique_id)}, + manufacturer="Read Your Meter Pro", + name=f"Meter {meter_id}", + ) + self._attr_native_value = last_read + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._attr_native_value = self.coordinator.data[self._meter_id]["read"] + self.async_write_ha_state() diff --git a/homeassistant/components/rympro/strings.json b/homeassistant/components/rympro/strings.json new file mode 100644 index 00000000000..b6e7adc9631 --- /dev/null +++ b/homeassistant/components/rympro/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/rympro/translations/en.json b/homeassistant/components/rympro/translations/en.json new file mode 100644 index 00000000000..2a2c9252d1a --- /dev/null +++ b/homeassistant/components/rympro/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Password" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rympro/translations/et.json b/homeassistant/components/rympro/translations/et.json new file mode 100644 index 00000000000..6f51658b2e4 --- /dev/null +++ b/homeassistant/components/rympro/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "email": "E-posti aadress", + "password": "Salas\u00f5na" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index fe9a64f3d6b..fb3b4e46533 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -12,7 +12,6 @@ from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_NAME, - CONF_PATH, CONF_PORT, CONF_SENSORS, CONF_SSL, @@ -80,7 +79,6 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_API_KEY): str, vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, - vol.Optional(CONF_PATH): str, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_SENSORS): vol.All( diff --git a/homeassistant/components/sabnzbd/config_flow.py b/homeassistant/components/sabnzbd/config_flow.py index 7930363b2ac..073f4a08f76 100644 --- a/homeassistant/components/sabnzbd/config_flow.py +++ b/homeassistant/components/sabnzbd/config_flow.py @@ -11,7 +11,6 @@ from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_NAME, - CONF_PATH, CONF_PORT, CONF_SSL, CONF_URL, @@ -28,7 +27,6 @@ USER_SCHEMA = vol.Schema( vol.Required(CONF_API_KEY): str, vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, vol.Required(CONF_URL): str, - vol.Optional(CONF_PATH): str, } ) diff --git a/homeassistant/components/sabnzbd/sab.py b/homeassistant/components/sabnzbd/sab.py index ab3575c7092..af70e4b8afc 100644 --- a/homeassistant/components/sabnzbd/sab.py +++ b/homeassistant/components/sabnzbd/sab.py @@ -1,21 +1,19 @@ """Support for the Sabnzbd service.""" from pysabnzbd import SabnzbdApi, SabnzbdApiException -from homeassistant.const import CONF_API_KEY, CONF_PATH, CONF_URL +from homeassistant.const import CONF_API_KEY, CONF_URL from homeassistant.core import _LOGGER, HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession async def get_client(hass: HomeAssistant, data): """Get Sabnzbd client.""" - web_root = data.get(CONF_PATH) api_key = data[CONF_API_KEY] url = data[CONF_URL] sab_api = SabnzbdApi( url, api_key, - web_root=web_root, session=async_get_clientsession(hass, False), ) try: diff --git a/homeassistant/components/sabnzbd/strings.json b/homeassistant/components/sabnzbd/strings.json index 9de5e08230c..501e0d33faf 100644 --- a/homeassistant/components/sabnzbd/strings.json +++ b/homeassistant/components/sabnzbd/strings.json @@ -5,8 +5,7 @@ "data": { "api_key": "[%key:common::config_flow::data::api_key%]", "name": "[%key:common::config_flow::data::name%]", - "url": "[%key:common::config_flow::data::url%]", - "path": "[%key:common::config_flow::data::path%]" + "url": "[%key:common::config_flow::data::url%]" } } }, diff --git a/homeassistant/components/sabnzbd/translations/bg.json b/homeassistant/components/sabnzbd/translations/bg.json index a9d07e52b39..d3fcfb0789d 100644 --- a/homeassistant/components/sabnzbd/translations/bg.json +++ b/homeassistant/components/sabnzbd/translations/bg.json @@ -9,7 +9,6 @@ "data": { "api_key": "API \u043a\u043b\u044e\u0447", "name": "\u0418\u043c\u0435", - "path": "\u041f\u044a\u0442", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/ca.json b/homeassistant/components/sabnzbd/translations/ca.json index d1947bb05f5..6c980e23fa1 100644 --- a/homeassistant/components/sabnzbd/translations/ca.json +++ b/homeassistant/components/sabnzbd/translations/ca.json @@ -9,7 +9,6 @@ "data": { "api_key": "Clau API", "name": "Nom", - "path": "Ruta", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/de.json b/homeassistant/components/sabnzbd/translations/de.json index 9f128f2878f..aa646a2c365 100644 --- a/homeassistant/components/sabnzbd/translations/de.json +++ b/homeassistant/components/sabnzbd/translations/de.json @@ -9,7 +9,6 @@ "data": { "api_key": "API-Schl\u00fcssel", "name": "Name", - "path": "Pfad", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/el.json b/homeassistant/components/sabnzbd/translations/el.json index f3d461e3dee..0a7544de95c 100644 --- a/homeassistant/components/sabnzbd/translations/el.json +++ b/homeassistant/components/sabnzbd/translations/el.json @@ -9,7 +9,6 @@ "data": { "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API", "name": "\u038c\u03bd\u03bf\u03bc\u03b1", - "path": "\u0394\u03b9\u03b1\u03b4\u03c1\u03bf\u03bc\u03ae", "url": "\u0394\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL" } } diff --git a/homeassistant/components/sabnzbd/translations/en.json b/homeassistant/components/sabnzbd/translations/en.json index 4e857c42b64..c521f0a7275 100644 --- a/homeassistant/components/sabnzbd/translations/en.json +++ b/homeassistant/components/sabnzbd/translations/en.json @@ -9,7 +9,6 @@ "data": { "api_key": "API Key", "name": "Name", - "path": "Path", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/es.json b/homeassistant/components/sabnzbd/translations/es.json index c3e690941bb..c315823a027 100644 --- a/homeassistant/components/sabnzbd/translations/es.json +++ b/homeassistant/components/sabnzbd/translations/es.json @@ -9,7 +9,6 @@ "data": { "api_key": "Clave API", "name": "Nombre", - "path": "Ruta", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/et.json b/homeassistant/components/sabnzbd/translations/et.json index b940ab99569..cc85bb09087 100644 --- a/homeassistant/components/sabnzbd/translations/et.json +++ b/homeassistant/components/sabnzbd/translations/et.json @@ -9,7 +9,6 @@ "data": { "api_key": "API v\u00f5ti", "name": "Nimi", - "path": "Rada", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/fr.json b/homeassistant/components/sabnzbd/translations/fr.json index 9809ccf8a0b..795ac22a230 100644 --- a/homeassistant/components/sabnzbd/translations/fr.json +++ b/homeassistant/components/sabnzbd/translations/fr.json @@ -9,7 +9,6 @@ "data": { "api_key": "Cl\u00e9 d'API", "name": "Nom", - "path": "Chemin d'acc\u00e8s", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/he.json b/homeassistant/components/sabnzbd/translations/he.json index 1574a5e7719..28079d639a6 100644 --- a/homeassistant/components/sabnzbd/translations/he.json +++ b/homeassistant/components/sabnzbd/translations/he.json @@ -9,7 +9,6 @@ "data": { "api_key": "\u05de\u05e4\u05ea\u05d7 API", "name": "\u05e9\u05dd", - "path": "\u05e0\u05ea\u05d9\u05d1", "url": "\u05db\u05ea\u05d5\u05d1\u05ea \u05d0\u05ea\u05e8" } } diff --git a/homeassistant/components/sabnzbd/translations/hu.json b/homeassistant/components/sabnzbd/translations/hu.json index 0136c11fc88..accaa124bcc 100644 --- a/homeassistant/components/sabnzbd/translations/hu.json +++ b/homeassistant/components/sabnzbd/translations/hu.json @@ -9,7 +9,6 @@ "data": { "api_key": "API kulcs", "name": "Elnevez\u00e9s", - "path": "\u00datvonal", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/id.json b/homeassistant/components/sabnzbd/translations/id.json index caa7e73815e..1999b21c1d3 100644 --- a/homeassistant/components/sabnzbd/translations/id.json +++ b/homeassistant/components/sabnzbd/translations/id.json @@ -9,7 +9,6 @@ "data": { "api_key": "Kunci API", "name": "Nama", - "path": "Jalur", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/it.json b/homeassistant/components/sabnzbd/translations/it.json index 48f2dea100c..80830c2d69b 100644 --- a/homeassistant/components/sabnzbd/translations/it.json +++ b/homeassistant/components/sabnzbd/translations/it.json @@ -9,7 +9,6 @@ "data": { "api_key": "Chiave API", "name": "Nome", - "path": "Percorso", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/ja.json b/homeassistant/components/sabnzbd/translations/ja.json index 737bbfe4140..bb676ba66eb 100644 --- a/homeassistant/components/sabnzbd/translations/ja.json +++ b/homeassistant/components/sabnzbd/translations/ja.json @@ -9,7 +9,6 @@ "data": { "api_key": "API\u30ad\u30fc", "name": "\u540d\u524d", - "path": "\u30d1\u30b9", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/ko.json b/homeassistant/components/sabnzbd/translations/ko.json index e20f9375f25..a7aa04f0ce0 100644 --- a/homeassistant/components/sabnzbd/translations/ko.json +++ b/homeassistant/components/sabnzbd/translations/ko.json @@ -9,7 +9,6 @@ "data": { "api_key": "API \ud0a4", "name": "\uc774\ub984", - "path": "\uacbd\ub85c", "url": "URL \uc8fc\uc18c" } } diff --git a/homeassistant/components/sabnzbd/translations/nl.json b/homeassistant/components/sabnzbd/translations/nl.json index c737cd0d07e..1b4a4bd4868 100644 --- a/homeassistant/components/sabnzbd/translations/nl.json +++ b/homeassistant/components/sabnzbd/translations/nl.json @@ -9,7 +9,6 @@ "data": { "api_key": "API-sleutel", "name": "Naam", - "path": "Pad", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/no.json b/homeassistant/components/sabnzbd/translations/no.json index 4da8a925a29..ea6820301f4 100644 --- a/homeassistant/components/sabnzbd/translations/no.json +++ b/homeassistant/components/sabnzbd/translations/no.json @@ -9,7 +9,6 @@ "data": { "api_key": "API-n\u00f8kkel", "name": "Navn", - "path": "Sti", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/pl.json b/homeassistant/components/sabnzbd/translations/pl.json index b083df2b8dc..532fcd79406 100644 --- a/homeassistant/components/sabnzbd/translations/pl.json +++ b/homeassistant/components/sabnzbd/translations/pl.json @@ -9,7 +9,6 @@ "data": { "api_key": "Klucz API", "name": "Nazwa", - "path": "\u015acie\u017cka", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/pt-BR.json b/homeassistant/components/sabnzbd/translations/pt-BR.json index b9015b40b14..f2f5ae0c561 100644 --- a/homeassistant/components/sabnzbd/translations/pt-BR.json +++ b/homeassistant/components/sabnzbd/translations/pt-BR.json @@ -9,7 +9,6 @@ "data": { "api_key": "Chave da API", "name": "Nome", - "path": "Caminho", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/ru.json b/homeassistant/components/sabnzbd/translations/ru.json index 68e5f255f96..555a708726e 100644 --- a/homeassistant/components/sabnzbd/translations/ru.json +++ b/homeassistant/components/sabnzbd/translations/ru.json @@ -9,7 +9,6 @@ "data": { "api_key": "\u041a\u043b\u044e\u0447 API", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "path": "\u041f\u0443\u0442\u044c", "url": "URL-\u0430\u0434\u0440\u0435\u0441" } } diff --git a/homeassistant/components/sabnzbd/translations/sk.json b/homeassistant/components/sabnzbd/translations/sk.json index d87df07af6d..354454de6c9 100644 --- a/homeassistant/components/sabnzbd/translations/sk.json +++ b/homeassistant/components/sabnzbd/translations/sk.json @@ -9,7 +9,6 @@ "data": { "api_key": "API k\u013e\u00fa\u010d", "name": "N\u00e1zov", - "path": "Cesta", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/sv.json b/homeassistant/components/sabnzbd/translations/sv.json index d82e85f8c48..7fcaf09be47 100644 --- a/homeassistant/components/sabnzbd/translations/sv.json +++ b/homeassistant/components/sabnzbd/translations/sv.json @@ -9,7 +9,6 @@ "data": { "api_key": "API Nyckel", "name": "Namn", - "path": "S\u00f6kv\u00e4g", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/tr.json b/homeassistant/components/sabnzbd/translations/tr.json index f238072b6da..1f34b61e55d 100644 --- a/homeassistant/components/sabnzbd/translations/tr.json +++ b/homeassistant/components/sabnzbd/translations/tr.json @@ -9,7 +9,6 @@ "data": { "api_key": "API Anahtar\u0131", "name": "Ad", - "path": "Yol", "url": "URL" } } diff --git a/homeassistant/components/sabnzbd/translations/zh-Hant.json b/homeassistant/components/sabnzbd/translations/zh-Hant.json index 018952ba66c..b8b89745098 100644 --- a/homeassistant/components/sabnzbd/translations/zh-Hant.json +++ b/homeassistant/components/sabnzbd/translations/zh-Hant.json @@ -9,7 +9,6 @@ "data": { "api_key": "API \u91d1\u9470", "name": "\u540d\u7a31", - "path": "\u8def\u5f91", "url": "\u7db2\u5740" } } diff --git a/homeassistant/components/samsungtv/__init__.py b/homeassistant/components/samsungtv/__init__.py index 5edae371517..993100262e7 100644 --- a/homeassistant/components/samsungtv/__init__.py +++ b/homeassistant/components/samsungtv/__init__.py @@ -215,7 +215,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry.async_on_unload(entry.add_update_listener(debounced_reloader.async_call)) hass.data[DOMAIN][entry.entry_id] = bridge - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 20a4765957a..8d04efe8859 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -522,7 +522,7 @@ class SamsungTVWSBridge( return RESULT_AUTH_MISSING except (ConnectionFailure, OSError, AsyncioTimeoutError) as err: LOGGER.debug("Failing config: %s, %s error: %s", config, type(err), err) - # pylint: disable=useless-else-on-loop + # pylint: disable-next=useless-else-on-loop else: if result: return result diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index e1c326ba364..96dd8093cfc 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -8,7 +8,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.5.0", "wakeonlan==2.1.0", - "async-upnp-client==0.33.0" + "async-upnp-client==0.33.1" ], "ssdp": [ { diff --git a/homeassistant/components/samsungtv/translations/el.json b/homeassistant/components/samsungtv/translations/el.json index 4388073f70b..570050eff54 100644 --- a/homeassistant/components/samsungtv/translations/el.json +++ b/homeassistant/components/samsungtv/translations/el.json @@ -26,7 +26,7 @@ "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {device}; \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03b9 \u03c0\u03bf\u03c4\u03ad \u03c4\u03bf Home Assistant \u03c0\u03c1\u03b9\u03bd, \u03b8\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 \u03b6\u03b7\u03c4\u03ac \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7." }, "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." + "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} \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 \u03ae \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf PIN." }, "reauth_confirm_encrypted": { "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf PIN \u03c0\u03bf\u03c5 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7 {device}." diff --git a/homeassistant/components/samsungtv/translations/lv.json b/homeassistant/components/samsungtv/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/samsungtv/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/tr.json b/homeassistant/components/samsungtv/translations/tr.json index 172bd0e093e..bd75308f503 100644 --- a/homeassistant/components/samsungtv/translations/tr.json +++ b/homeassistant/components/samsungtv/translations/tr.json @@ -26,7 +26,7 @@ "description": "{device} kurulumunu yapmak istiyor musunuz? Home Assistant'\u0131 daha \u00f6nce hi\u00e7 ba\u011flamad\u0131ysan\u0131z, TV'nizde yetki isteyen bir a\u00e7\u0131l\u0131r pencere g\u00f6rmelisiniz." }, "reauth_confirm": { - "description": "G\u00f6nderdikten sonra, 30 saniye i\u00e7inde yetkilendirme isteyen {device} \u00fczerindeki a\u00e7\u0131l\u0131r pencereyi kabul edin veya PIN'i girin." + "description": "G\u00f6nderdikten sonra, {device} \u00fczerindeki yetkilendirme isteyen a\u00e7\u0131l\u0131r pencereyi 30 saniye i\u00e7inde kabul edin veya PIN'i girin." }, "reauth_confirm_encrypted": { "description": "L\u00fctfen {device} \u00fczerinde g\u00f6r\u00fcnt\u00fclenen PIN'i girin." diff --git a/homeassistant/components/scrape/config_flow.py b/homeassistant/components/scrape/config_flow.py index cbd0ed7d525..419dd04f606 100644 --- a/homeassistant/components/scrape/config_flow.py +++ b/homeassistant/components/scrape/config_flow.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Mapping -from typing import Any +from typing import Any, cast import uuid import voluptuous as vol @@ -172,7 +172,7 @@ async def get_edit_sensor_suggested_values( ) -> dict[str, Any]: """Return suggested values for sensor editing.""" idx: int = handler.flow_state["_idx"] - return handler.options[SENSOR_DOMAIN][idx] + return cast(dict[str, Any], handler.options[SENSOR_DOMAIN][idx]) async def validate_sensor_edit( @@ -284,4 +284,4 @@ class ScrapeConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title.""" - return options[CONF_RESOURCE] + return cast(str, options[CONF_RESOURCE]) diff --git a/homeassistant/components/scrape/translations/el.json b/homeassistant/components/scrape/translations/el.json index 6bb9e6b0345..c2f5968f5e0 100644 --- a/homeassistant/components/scrape/translations/el.json +++ b/homeassistant/components/scrape/translations/el.json @@ -112,7 +112,8 @@ "method": "\u039c\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2", "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", "resource": "\u03a0\u03cc\u03c1\u03bf\u03c2", - "timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf" + "timeout": "\u03a7\u03c1\u03bf\u03bd\u03b9\u03ba\u03cc \u03cc\u03c1\u03b9\u03bf", + "username": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7" }, "data_description": { "authentication": "\u03a4\u03cd\u03c0\u03bf\u03c2 \u03c4\u03bf\u03c5 \u03b5\u03bb\u03ad\u03b3\u03c7\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 HTTP. \u0395\u03af\u03c4\u03b5 \u03b2\u03b1\u03c3\u03b9\u03ba\u03cc\u03c2 \u03b5\u03af\u03c4\u03b5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2", diff --git a/homeassistant/components/scrape/translations/nl.json b/homeassistant/components/scrape/translations/nl.json index 78c43519015..b90d2134e2b 100644 --- a/homeassistant/components/scrape/translations/nl.json +++ b/homeassistant/components/scrape/translations/nl.json @@ -49,13 +49,31 @@ } } }, + "issues": { + "moved_yaml": { + "title": "De Scrape YAML-configuratie is verplaatst" + } + }, "options": { "step": { "add_sensor": { "data": { "attribute": "Attribuut", "device_class": "Apparaatklasse (device_class)", - "index": "Index" + "index": "Index", + "select": "Selecteer", + "state_class": "Statusklasse (state_class)", + "unit_of_measurement": "Eenheid", + "value_template": "Waardesjabloon (template)" + }, + "data_description": { + "attribute": "Bepaal de waarde van een attribuut op van de geselecteerde tag", + "device_class": "Type/klasse van de sensor voor het icoon in de frontend", + "index": "Bepaalt welke van de elementen worden teruggegeven door de CSS selector om te gebruiken", + "select": "Bepaalt de tag om naar te zoeken. Zie Beautifulsoup CSS selectors voor meer details", + "state_class": "De state_class van de sensor", + "unit_of_measurement": "Kies een temperatuurmeting of cre\u00eber er zelf een", + "value_template": "Definieert een template om de status van de sensor te bepalen" } }, "edit_sensor": { @@ -90,8 +108,10 @@ "authentication": "Authenticatie", "headers": "Headers", "method": "Methode", + "password": "Wachtwoord", "resource": "Bron", - "timeout": "Wachttijd verstreken" + "timeout": "Wachttijd verstreken", + "username": "Gebruikersnaam" }, "data_description": { "authentication": "Type van de HTTP-authenticatie. Ofwel basic of digest", diff --git a/homeassistant/components/scrape/translations/pl.json b/homeassistant/components/scrape/translations/pl.json index 46de7003777..b39e34ef2cc 100644 --- a/homeassistant/components/scrape/translations/pl.json +++ b/homeassistant/components/scrape/translations/pl.json @@ -114,7 +114,8 @@ "method": "Metoda", "password": "Has\u0142o", "resource": "Zas\u00f3b", - "timeout": "Limit czasu" + "timeout": "Limit czasu", + "username": "Nazwa u\u017cytkownika" }, "data_description": { "authentication": "Typ uwierzytelniania HTTP. Podstawowy lub digest.", diff --git a/homeassistant/components/scrape/translations/tr.json b/homeassistant/components/scrape/translations/tr.json index dec08746179..509595fed55 100644 --- a/homeassistant/components/scrape/translations/tr.json +++ b/homeassistant/components/scrape/translations/tr.json @@ -3,13 +3,39 @@ "abort": { "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, + "error": { + "resource_error": "Kalan veriler g\u00fcncellenemedi. Yap\u0131land\u0131rman\u0131z\u0131 do\u011frulay\u0131n" + }, "step": { + "sensor": { + "data": { + "attribute": "\u00d6znitelik", + "device_class": "Cihaz S\u0131n\u0131f\u0131", + "index": "Dizin", + "name": "Ad", + "select": "Se\u00e7", + "state_class": "Durum S\u0131n\u0131f\u0131", + "unit_of_measurement": "\u00d6l\u00e7\u00fc Birimi", + "value_template": "De\u011fer \u015eablonu" + }, + "data_description": { + "attribute": "Se\u00e7ilen etikette bir \u00f6zelli\u011fin de\u011ferini al\u0131n", + "device_class": "\u00d6nu\u00e7taki simgeyi ayarlamak i\u00e7in sens\u00f6r\u00fcn t\u00fcr\u00fc/s\u0131n\u0131f\u0131", + "index": "CSS se\u00e7ici taraf\u0131ndan d\u00f6nd\u00fcr\u00fclen \u00f6\u011felerden hangisinin kullan\u0131laca\u011f\u0131n\u0131 tan\u0131mlar", + "select": "Hangi etiketin aranaca\u011f\u0131n\u0131 tan\u0131mlar. Ayr\u0131nt\u0131lar i\u00e7in Beautifulsoup CSS se\u00e7icilerini kontrol edin", + "state_class": "Sens\u00f6r\u00fcn state_class", + "unit_of_measurement": "S\u0131cakl\u0131k \u00f6l\u00e7\u00fcm\u00fcn\u00fc se\u00e7in veya kendinizinkini olu\u015fturun", + "value_template": "Sens\u00f6r\u00fcn durumunu almak i\u00e7in bir \u015fablon tan\u0131mlar" + } + }, "user": { "data": { - "authentication": "Kimlik do\u011frulama", + "authentication": "Kimlik do\u011frulama y\u00f6ntemini se\u00e7in", "headers": "Ba\u015fl\u0131klar", + "method": "Y\u00f6ntem", "password": "Parola", "resource": "Kaynak", + "timeout": "Zaman a\u015f\u0131m\u0131", "username": "Kullan\u0131c\u0131 Ad\u0131", "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" }, @@ -17,6 +43,86 @@ "authentication": "HTTP kimlik do\u011frulamas\u0131n\u0131n t\u00fcr\u00fc. Temel veya basit", "headers": "Web iste\u011fi i\u00e7in kullan\u0131lacak ba\u015fl\u0131klar", "resource": "De\u011feri i\u00e7eren web sitesinin URL'si", + "timeout": "Web sitesine ba\u011flant\u0131 i\u00e7in zaman a\u015f\u0131m\u0131", + "verify_ssl": "\u00d6rne\u011fin, kendinden imzal\u0131ysa, SSL/TLS sertifikas\u0131n\u0131n do\u011frulanmas\u0131n\u0131 etkinle\u015ftirir/devre d\u0131\u015f\u0131 b\u0131rak\u0131r" + } + } + } + }, + "issues": { + "moved_yaml": { + "description": "YAML kullanarak Scrape'i yap\u0131land\u0131rma, entegrasyon anahtar\u0131na ta\u015f\u0131nd\u0131. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z 2 s\u00fcr\u00fcm daha \u00e7al\u0131\u015facak. \n\n YAML yap\u0131land\u0131rman\u0131z\u0131 belgelere g\u00f6re entegrasyon anahtar\u0131na ge\u00e7irin.", + "title": "Scrape YAML yap\u0131land\u0131rmas\u0131 ta\u015f\u0131nd\u0131" + } + }, + "options": { + "step": { + "add_sensor": { + "data": { + "attribute": "\u00d6znitelik", + "device_class": "Cihaz S\u0131n\u0131f\u0131", + "index": "Dizin", + "name": "Ad", + "select": "Se\u00e7", + "state_class": "Durum S\u0131n\u0131f\u0131", + "unit_of_measurement": "\u00d6l\u00e7\u00fc Birimi", + "value_template": "De\u011fer \u015eablonu" + }, + "data_description": { + "attribute": "Se\u00e7ilen etikette bir \u00f6zelli\u011fin de\u011ferini al\u0131n", + "device_class": "\u00d6nu\u00e7taki simgeyi ayarlamak i\u00e7in sens\u00f6r\u00fcn t\u00fcr\u00fc/s\u0131n\u0131f\u0131", + "index": "CSS se\u00e7ici taraf\u0131ndan d\u00f6nd\u00fcr\u00fclen \u00f6\u011felerden hangisinin kullan\u0131laca\u011f\u0131n\u0131 tan\u0131mlar", + "select": "Hangi etiketin aranaca\u011f\u0131n\u0131 tan\u0131mlar. Ayr\u0131nt\u0131lar i\u00e7in Beautifulsoup CSS se\u00e7icilerini kontrol edin", + "state_class": "Sens\u00f6r\u00fcn state_class", + "unit_of_measurement": "S\u0131cakl\u0131k \u00f6l\u00e7\u00fcm\u00fcn\u00fc se\u00e7in veya kendinizinkini olu\u015fturun", + "value_template": "Sens\u00f6r\u00fcn durumunu almak i\u00e7in bir \u015fablon tan\u0131mlar" + } + }, + "edit_sensor": { + "data": { + "attribute": "\u00d6znitelik", + "device_class": "Cihaz S\u0131n\u0131f\u0131", + "index": "Dizin", + "name": "Ad", + "select": "Se\u00e7", + "state_class": "Durum S\u0131n\u0131f\u0131", + "unit_of_measurement": "\u00d6l\u00e7\u00fc Birimi", + "value_template": "De\u011fer \u015eablonu" + }, + "data_description": { + "attribute": "Se\u00e7ilen etikette bir \u00f6zelli\u011fin de\u011ferini al\u0131n", + "device_class": "\u00d6nu\u00e7taki simgeyi ayarlamak i\u00e7in sens\u00f6r\u00fcn t\u00fcr\u00fc/s\u0131n\u0131f\u0131", + "index": "CSS se\u00e7ici taraf\u0131ndan d\u00f6nd\u00fcr\u00fclen \u00f6\u011felerden hangisinin kullan\u0131laca\u011f\u0131n\u0131 tan\u0131mlar", + "select": "Hangi etiketin aranaca\u011f\u0131n\u0131 tan\u0131mlar. Ayr\u0131nt\u0131lar i\u00e7in Beautifulsoup CSS se\u00e7icilerini kontrol edin", + "state_class": "Sens\u00f6r\u00fcn state_class", + "unit_of_measurement": "S\u0131cakl\u0131k \u00f6l\u00e7\u00fcm\u00fcn\u00fc se\u00e7in veya kendinizinkini olu\u015fturun", + "value_template": "Sens\u00f6r\u00fcn durumunu almak i\u00e7in bir \u015fablon tan\u0131mlar" + } + }, + "init": { + "menu_options": { + "add_sensor": "Sens\u00f6r ekle", + "remove_sensor": "Sens\u00f6r\u00fc kald\u0131r", + "resource": "Kayna\u011f\u0131 yap\u0131land\u0131r", + "select_edit_sensor": "Sens\u00f6r\u00fc yap\u0131land\u0131r" + } + }, + "resource": { + "data": { + "authentication": "Kimlik do\u011frulama y\u00f6ntemini se\u00e7in", + "headers": "Ba\u015fl\u0131klar", + "method": "Y\u00f6ntem", + "password": "Parola", + "resource": "Kaynak", + "timeout": "Zaman a\u015f\u0131m\u0131", + "username": "Kullan\u0131c\u0131 Ad\u0131", + "verify_ssl": "SSL sertifikas\u0131n\u0131 do\u011frulay\u0131n" + }, + "data_description": { + "authentication": "HTTP kimlik do\u011frulamas\u0131n\u0131n t\u00fcr\u00fc. Temel veya basit", + "headers": "Web iste\u011fi i\u00e7in kullan\u0131lacak ba\u015fl\u0131klar", + "resource": "De\u011feri i\u00e7eren web sitesinin URL'si", + "timeout": "Web sitesine ba\u011flant\u0131 i\u00e7in zaman a\u015f\u0131m\u0131", "verify_ssl": "\u00d6rne\u011fin, kendinden imzal\u0131ysa, SSL/TLS sertifikas\u0131n\u0131n do\u011frulanmas\u0131n\u0131 etkinle\u015ftirir/devre d\u0131\u015f\u0131 b\u0131rak\u0131r" } } diff --git a/homeassistant/components/scrape/translations/uk.json b/homeassistant/components/scrape/translations/uk.json new file mode 100644 index 00000000000..a983cd01d97 --- /dev/null +++ b/homeassistant/components/scrape/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "options": { + "step": { + "resource": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/screenlogic/__init__.py b/homeassistant/components/screenlogic/__init__.py index 41e0638c634..d698653a3fc 100644 --- a/homeassistant/components/screenlogic/__init__.py +++ b/homeassistant/components/screenlogic/__init__.py @@ -240,10 +240,12 @@ class ScreenlogicEntity(CoordinatorEntity[ScreenlogicDataUpdateCoordinator]): class ScreenLogicCircuitEntity(ScreenlogicEntity): """ScreenLogic circuit entity.""" + _attr_has_entity_name = True + @property def name(self): """Get the name of the switch.""" - return f"{self.gateway_name} {self.circuit['name']}" + return self.circuit["name"] @property def is_on(self) -> bool: diff --git a/homeassistant/components/screenlogic/binary_sensor.py b/homeassistant/components/screenlogic/binary_sensor.py index 73596983cec..08035e25a87 100644 --- a/homeassistant/components/screenlogic/binary_sensor.py +++ b/homeassistant/components/screenlogic/binary_sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ScreenlogicEntity @@ -14,6 +15,13 @@ from .const import DOMAIN SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = {DEVICE_TYPE.ALARM: BinarySensorDeviceClass.PROBLEM} +SUPPORTED_CONFIG_BINARY_SENSORS = ( + "freeze_mode", + "pool_delay", + "spa_delay", + "cleaner_delay", +) + async def async_setup_entry( hass: HomeAssistant, @@ -27,6 +35,14 @@ async def async_setup_entry( # Generic binary sensor entities.append(ScreenLogicBinarySensor(coordinator, "chem_alarm")) + entities.extend( + [ + ScreenlogicConfigBinarySensor(coordinator, cfg_sensor) + for cfg_sensor in coordinator.data[SL_DATA.KEY_CONFIG] + if cfg_sensor in SUPPORTED_CONFIG_BINARY_SENSORS + ] + ) + if ( coordinator.data[SL_DATA.KEY_CONFIG]["equipment_flags"] & EQUIPMENT.FLAG_INTELLICHEM @@ -38,6 +54,7 @@ async def async_setup_entry( for chem_alarm in coordinator.data[SL_DATA.KEY_CHEMISTRY][ SL_DATA.KEY_ALERTS ] + if chem_alarm != "_raw" ] ) @@ -48,6 +65,7 @@ async def async_setup_entry( for chem_notif in coordinator.data[SL_DATA.KEY_CHEMISTRY][ SL_DATA.KEY_NOTIFICATIONS ] + if chem_notif != "_raw" ] ) @@ -64,10 +82,13 @@ async def async_setup_entry( class ScreenLogicBinarySensor(ScreenlogicEntity, BinarySensorEntity): """Representation of the basic ScreenLogic binary sensor entity.""" + _attr_has_entity_name = True + _attr_entity_category = EntityCategory.DIAGNOSTIC + @property def name(self): """Return the sensor name.""" - return f"{self.gateway_name} {self.sensor['name']}" + return self.sensor["name"] @property def device_class(self): @@ -115,3 +136,12 @@ class ScreenlogicSCGBinarySensor(ScreenLogicBinarySensor): def sensor(self): """Shortcut to access the sensor data.""" return self.coordinator.data[SL_DATA.KEY_SCG][self._data_key] + + +class ScreenlogicConfigBinarySensor(ScreenLogicBinarySensor): + """Representation of a ScreenLogic config data binary sensor entity.""" + + @property + def sensor(self): + """Shortcut to access the sensor data.""" + return self.coordinator.data[SL_DATA.KEY_CONFIG][self._data_key] diff --git a/homeassistant/components/screenlogic/climate.py b/homeassistant/components/screenlogic/climate.py index 588d6d9a581..093d96a285e 100644 --- a/homeassistant/components/screenlogic/climate.py +++ b/homeassistant/components/screenlogic/climate.py @@ -51,6 +51,8 @@ async def async_setup_entry( class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity): """Represents a ScreenLogic climate entity.""" + _attr_has_entity_name = True + _attr_hvac_modes = SUPPORTED_MODES _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE @@ -71,8 +73,7 @@ class ScreenLogicClimate(ScreenlogicEntity, ClimateEntity, RestoreEntity): @property def name(self) -> str: """Name of the heater.""" - ent_name = self.body["heat_status"]["name"] - return f"{self.gateway_name} {ent_name}" + return self.body["heat_status"]["name"] @property def min_temp(self) -> float: diff --git a/homeassistant/components/screenlogic/const.py b/homeassistant/components/screenlogic/const.py index bfa9d09cab8..e4a5ea82186 100644 --- a/homeassistant/components/screenlogic/const.py +++ b/homeassistant/components/screenlogic/const.py @@ -13,4 +13,13 @@ SUPPORTED_COLOR_MODES = { slugify(name): num for num, name in COLOR_MODE.NAME_FOR_NUM.items() } -LIGHT_CIRCUIT_FUNCTIONS = {CIRCUIT_FUNCTION.INTELLIBRITE, CIRCUIT_FUNCTION.LIGHT} +LIGHT_CIRCUIT_FUNCTIONS = { + CIRCUIT_FUNCTION.COLOR_WHEEL, + CIRCUIT_FUNCTION.DIMMER, + CIRCUIT_FUNCTION.INTELLIBRITE, + CIRCUIT_FUNCTION.LIGHT, + CIRCUIT_FUNCTION.MAGICSTREAM, + CIRCUIT_FUNCTION.PHOTONGEN, + CIRCUIT_FUNCTION.SAL_LIGHT, + CIRCUIT_FUNCTION.SAM_LIGHT, +} diff --git a/homeassistant/components/screenlogic/diagnostics.py b/homeassistant/components/screenlogic/diagnostics.py index 33041597b75..6de916d9514 100644 --- a/homeassistant/components/screenlogic/diagnostics.py +++ b/homeassistant/components/screenlogic/diagnostics.py @@ -1,5 +1,7 @@ """Diagnostics for Screenlogic.""" +from typing import Any + from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -9,7 +11,7 @@ from .const import DOMAIN async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" coordinator: ScreenlogicDataUpdateCoordinator = hass.data[DOMAIN][ config_entry.entry_id @@ -18,4 +20,5 @@ async def async_get_config_entry_diagnostics( return { "config_entry": config_entry.as_dict(), "data": coordinator.data, + "debug": coordinator.gateway.get_debug(), } diff --git a/homeassistant/components/screenlogic/manifest.json b/homeassistant/components/screenlogic/manifest.json index 4998b7b507f..94f7078b706 100644 --- a/homeassistant/components/screenlogic/manifest.json +++ b/homeassistant/components/screenlogic/manifest.json @@ -3,7 +3,7 @@ "name": "Pentair ScreenLogic", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/screenlogic", - "requirements": ["screenlogicpy==0.5.4"], + "requirements": ["screenlogicpy==0.6.4"], "codeowners": ["@dieselrabbit", "@bdraco"], "dhcp": [ { "registered_devices": true }, diff --git a/homeassistant/components/screenlogic/number.py b/homeassistant/components/screenlogic/number.py index 75dee907cc1..74a7811b590 100644 --- a/homeassistant/components/screenlogic/number.py +++ b/homeassistant/components/screenlogic/number.py @@ -6,6 +6,7 @@ from screenlogicpy.const import BODY_TYPE, DATA as SL_DATA, EQUIPMENT, SCG from homeassistant.components.number import NumberEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ScreenlogicEntity @@ -42,13 +43,16 @@ async def async_setup_entry( class ScreenLogicNumber(ScreenlogicEntity, NumberEntity): """Class to represent a ScreenLogic Number.""" + _attr_has_entity_name = True + def __init__(self, coordinator, data_key, enabled=True): """Initialize of the entity.""" super().__init__(coordinator, data_key, enabled) self._body_type = SUPPORTED_SCG_NUMBERS.index(self._data_key) self._attr_native_max_value = SCG.LIMIT_FOR_BODY[self._body_type] - self._attr_name = f"{self.gateway_name} {self.sensor['name']}" + self._attr_name = self.sensor["name"] self._attr_native_unit_of_measurement = self.sensor["unit"] + self._attr_entity_category = EntityCategory.CONFIG @property def native_value(self) -> float: diff --git a/homeassistant/components/screenlogic/sensor.py b/homeassistant/components/screenlogic/sensor.py index beab664f448..3f488db98eb 100644 --- a/homeassistant/components/screenlogic/sensor.py +++ b/homeassistant/components/screenlogic/sensor.py @@ -4,6 +4,7 @@ from screenlogicpy.const import ( DATA as SL_DATA, DEVICE_TYPE, EQUIPMENT, + UNIT, ) from homeassistant.components.sensor import ( @@ -12,7 +13,17 @@ from homeassistant.components.sensor import ( SensorStateClass, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, + PERCENTAGE, + REVOLUTIONS_PER_MINUTE, + UnitOfElectricPotential, + UnitOfPower, + UnitOfTemperature, + UnitOfTime, +) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import ScreenlogicEntity @@ -56,8 +67,23 @@ SUPPORTED_SCG_SENSORS = ( SUPPORTED_PUMP_SENSORS = ("currentWatts", "currentRPM", "currentGPM") SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS = { - DEVICE_TYPE.TEMPERATURE: SensorDeviceClass.TEMPERATURE, + DEVICE_TYPE.DURATION: SensorDeviceClass.DURATION, DEVICE_TYPE.ENERGY: SensorDeviceClass.POWER, + DEVICE_TYPE.POWER: SensorDeviceClass.POWER, + DEVICE_TYPE.TEMPERATURE: SensorDeviceClass.TEMPERATURE, + DEVICE_TYPE.VOLUME: SensorDeviceClass.VOLUME, +} + +SL_UNIT_TO_HA_UNIT = { + UNIT.CELSIUS: UnitOfTemperature.CELSIUS, + UNIT.FAHRENHEIT: UnitOfTemperature.FAHRENHEIT, + UNIT.MILLIVOLT: UnitOfElectricPotential.MILLIVOLT, + UNIT.WATT: UnitOfPower.WATT, + UNIT.HOUR: UnitOfTime.HOURS, + UNIT.SECOND: UnitOfTime.SECONDS, + UNIT.REVOLUTIONS_PER_MINUTE: REVOLUTIONS_PER_MINUTE, + UNIT.PARTS_PER_MILLION: CONCENTRATION_PARTS_PER_MILLION, + UNIT.PERCENT: PERCENTAGE, } @@ -129,15 +155,18 @@ async def async_setup_entry( class ScreenLogicSensor(ScreenlogicEntity, SensorEntity): """Representation of the basic ScreenLogic sensor entity.""" + _attr_has_entity_name = True + @property def name(self): """Name of the sensor.""" - return f"{self.gateway_name} {self.sensor['name']}" + return self.sensor["name"] @property def native_unit_of_measurement(self): """Return the unit of measurement.""" - return self.sensor.get("unit") + sl_unit = self.sensor.get("unit") + return SL_UNIT_TO_HA_UNIT.get(sl_unit, sl_unit) @property def device_class(self): @@ -145,6 +174,13 @@ class ScreenLogicSensor(ScreenlogicEntity, SensorEntity): device_type = self.sensor.get("device_type") return SL_DEVICE_TYPE_TO_HA_DEVICE_CLASS.get(device_type) + @property + def entity_category(self): + """Entity Category of the sensor.""" + return ( + None if self._data_key == "air_temperature" else EntityCategory.DIAGNOSTIC + ) + @property def state_class(self): """Return the state class of the sensor.""" diff --git a/homeassistant/components/screenlogic/translations/lv.json b/homeassistant/components/screenlogic/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/screenlogic/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/season/translations/fr.json b/homeassistant/components/season/translations/fr.json index a3bf66a400e..09f93133862 100644 --- a/homeassistant/components/season/translations/fr.json +++ b/homeassistant/components/season/translations/fr.json @@ -10,5 +10,17 @@ } } } + }, + "entity": { + "sensor": { + "season": { + "state": { + "autumn": "Automne", + "spring": "Printemps", + "summer": "\u00c9t\u00e9", + "winter": "Hiver" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/nl.json b/homeassistant/components/season/translations/nl.json index 63067c8a814..611bb91366a 100644 --- a/homeassistant/components/season/translations/nl.json +++ b/homeassistant/components/season/translations/nl.json @@ -10,5 +10,22 @@ } } } + }, + "entity": { + "sensor": { + "season": { + "state": { + "autumn": "Herfst", + "spring": "Lente", + "summer": "Zomer", + "winter": "Winter" + } + } + } + }, + "issues": { + "removed_yaml": { + "title": "De Season YAML-configuratie is verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/season/translations/tr.json b/homeassistant/components/season/translations/tr.json index d214692147a..958ba3a4984 100644 --- a/homeassistant/components/season/translations/tr.json +++ b/homeassistant/components/season/translations/tr.json @@ -11,6 +11,18 @@ } } }, + "entity": { + "sensor": { + "season": { + "state": { + "autumn": "Sonbahar", + "spring": "\u0130lkbahar", + "summer": "Yaz", + "winter": "K\u0131\u015f" + } + } + } + }, "issues": { "removed_yaml": { "description": "Season'\u0131 YAML kullanarak yap\u0131land\u0131rma kald\u0131r\u0131ld\u0131. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z Home Assistant taraf\u0131ndan kullan\u0131lm\u0131yor. \n\n YAML yap\u0131land\u0131rmas\u0131n\u0131 configuration.yaml dosyan\u0131zdan kald\u0131r\u0131n ve bu sorunu gidermek i\u00e7in Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", diff --git a/homeassistant/components/season/translations/uk.json b/homeassistant/components/season/translations/uk.json new file mode 100644 index 00000000000..cdc66c4f8bb --- /dev/null +++ b/homeassistant/components/season/translations/uk.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u043e\u0441\u043b\u0443\u0433\u0430 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0430" + }, + "step": { + "user": { + "data": { + "type": "\u0412\u0438\u0434 \u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u043e\u0440\u0438 \u0440\u043e\u043a\u0443" + } + } + } + }, + "entity": { + "sensor": { + "season": { + "state": { + "autumn": "\u041e\u0441\u0456\u043d\u044c", + "spring": "\u0412\u0435\u0441\u043d\u0430", + "summer": "\u041b\u0456\u0442\u043e", + "winter": "\u0417\u0438\u043c\u0430" + } + } + } + }, + "issues": { + "removed_yaml": { + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u0435\u0437\u043e\u043d\u0443 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e YAML \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043e.\n\n\u0412\u0430\u0448\u0430 \u043d\u0430\u044f\u0432\u043d\u0430 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f YAML \u043d\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f Home Assistant.\n\n\u0412\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e YAML \u0456\u0437 \u0444\u0430\u0439\u043b\u0443 configuration.yaml \u0456 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c Home Assistant, \u0449\u043e\u0431 \u0440\u043e\u0437\u0432\u2019\u044f\u0437\u0430\u0442\u0438 \u0446\u044e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443.", + "title": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e YAML \u0441\u0435\u0437\u043e\u043d\u0443 \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/select/translations/ru.json b/homeassistant/components/select/translations/ru.json index 5bbdd279b43..5efcac577e0 100644 --- a/homeassistant/components/select/translations/ru.json +++ b/homeassistant/components/select/translations/ru.json @@ -10,5 +10,5 @@ "current_option_changed": "{entity_name} \u043c\u0435\u043d\u044f\u0435\u0442 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0432\u044b\u0431\u043e\u0440\u0430" } }, - "title": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c" + "title": "\u0412\u044b\u0431\u043e\u0440" } \ No newline at end of file diff --git a/homeassistant/components/sense/translations/lt.json b/homeassistant/components/sense/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/sense/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/lv.json b/homeassistant/components/sense/translations/lv.json index 85a6742da50..107e19a1437 100644 --- a/homeassistant/components/sense/translations/lv.json +++ b/homeassistant/components/sense/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "error": { "unknown": "Neparedz\u0113ta k\u013c\u016bda" }, diff --git a/homeassistant/components/sense/translations/uk.json b/homeassistant/components/sense/translations/uk.json index 8eac9c9d4ab..1c7a4b535ad 100644 --- a/homeassistant/components/sense/translations/uk.json +++ b/homeassistant/components/sense/translations/uk.json @@ -9,6 +9,11 @@ "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { + "reauth_validate": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + }, "user": { "data": { "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438", diff --git a/homeassistant/components/senseme/translations/lv.json b/homeassistant/components/senseme/translations/lv.json index 35d9add569f..5a1f64f08fe 100644 --- a/homeassistant/components/senseme/translations/lv.json +++ b/homeassistant/components/senseme/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/senseme/translations/tr.json b/homeassistant/components/senseme/translations/tr.json index 87c43a08326..3d5d87691d4 100644 --- a/homeassistant/components/senseme/translations/tr.json +++ b/homeassistant/components/senseme/translations/tr.json @@ -11,7 +11,7 @@ "flow_title": "{name} - {model} ({host})", "step": { "discovery_confirm": { - "description": "{name} - {model} ( {host} ) kurulumunu yapmak istiyor musunuz?" + "description": "{name} - {model} ( {host} ) kurmak istiyor musunuz?" }, "manual": { "data": { diff --git a/homeassistant/components/senseme/translations/uk.json b/homeassistant/components/senseme/translations/uk.json new file mode 100644 index 00000000000..8decd180ea2 --- /dev/null +++ b/homeassistant/components/senseme/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index 583d25112b4..9afdff43ef0 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -308,11 +308,13 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): if "fanLevel" not in self.device_data.active_features: raise HomeAssistantError("Current mode doesn't support setting Fanlevel") + transformation = self.device_data.fan_modes_translated await self.async_send_api_call( key=AC_STATE_TO_DATA["fanLevel"], value=fan_mode, name="fanLevel", assumed_state=False, + transformation=transformation, ) async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: @@ -347,11 +349,13 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): if "swing" not in self.device_data.active_features: raise HomeAssistantError("Current mode doesn't support setting Swing") + transformation = self.device_data.swing_modes_translated await self.async_send_api_call( key=AC_STATE_TO_DATA["swing"], value=swing_mode, name="swing", assumed_state=False, + transformation=transformation, ) async def async_turn_on(self) -> None: @@ -502,8 +506,11 @@ class SensiboClimate(SensiboDeviceBaseEntity, ClimateEntity): value: Any, name: str, assumed_state: bool = False, + transformation: dict | None = None, ) -> bool: """Make service call to api.""" + if transformation: + value = transformation[value] result = await self._client.async_set_ac_state_property( self._device_id, name, diff --git a/homeassistant/components/sensibo/entity.py b/homeassistant/components/sensibo/entity.py index 56a7c820739..8b46e3e7941 100644 --- a/homeassistant/components/sensibo/entity.py +++ b/homeassistant/components/sensibo/entity.py @@ -2,11 +2,10 @@ from __future__ import annotations from collections.abc import Callable, Coroutine -from typing import TYPE_CHECKING, Any, TypeVar +from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar import async_timeout from pysensibo.model import MotionSensor, SensiboDevice -from typing_extensions import Concatenate, ParamSpec from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index e685e80718f..40e53dd8c79 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,7 +2,7 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": ["pysensibo==1.0.22"], + "requirements": ["pysensibo==1.0.25"], "config_flow": true, "codeowners": ["@andrey-git", "@gjohansson-ST"], "iot_class": "cloud_polling", diff --git a/homeassistant/components/sensibo/select.py b/homeassistant/components/sensibo/select.py index b69de8d0763..29ebdc89261 100644 --- a/homeassistant/components/sensibo/select.py +++ b/homeassistant/components/sensibo/select.py @@ -27,6 +27,7 @@ class SensiboSelectDescriptionMixin: data_key: str value_fn: Callable[[SensiboDevice], str | None] options_fn: Callable[[SensiboDevice], list[str] | None] + transformation: Callable[[SensiboDevice], dict | None] @dataclass @@ -44,6 +45,8 @@ DEVICE_SELECT_TYPES = ( icon="mdi:air-conditioner", value_fn=lambda data: data.horizontal_swing_mode, options_fn=lambda data: data.horizontal_swing_modes, + translation_key="horizontalswing", + transformation=lambda data: data.horizontal_swing_modes_translated, ), SensiboSelectEntityDescription( key="light", @@ -52,6 +55,8 @@ DEVICE_SELECT_TYPES = ( icon="mdi:flashlight", value_fn=lambda data: data.light_mode, options_fn=lambda data: data.light_modes, + translation_key="light", + transformation=lambda data: data.light_modes_translated, ), ) @@ -116,6 +121,10 @@ class SensiboSelect(SensiboDeviceBaseEntity, SelectEntity): @async_handle_api_call async def async_send_api_call(self, key: str, value: Any) -> bool: """Make service call to api.""" + transformation = self.entity_description.transformation(self.device_data) + if TYPE_CHECKING: + assert transformation is not None + data = { "name": self.entity_description.key, "value": value, @@ -125,7 +134,7 @@ class SensiboSelect(SensiboDeviceBaseEntity, SelectEntity): result = await self._client.async_set_ac_state_property( self._device_id, data["name"], - data["value"], + transformation[data["value"]], data["ac_states"], data["assumed_state"], ) diff --git a/homeassistant/components/sensibo/sensor.py b/homeassistant/components/sensibo/sensor.py index 348c5986bd2..8048eece338 100644 --- a/homeassistant/components/sensibo/sensor.py +++ b/homeassistant/components/sensibo/sensor.py @@ -352,8 +352,6 @@ class SensiboDeviceSensor(SensiboDeviceBaseEntity, SensorEntity): def native_value(self) -> StateType | datetime: """Return value of sensor.""" state = self.entity_description.value_fn(self.device_data) - if isinstance(state, str): - return state.lower() return state @property diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json index 21830ff913c..fb3559de91a 100644 --- a/homeassistant/components/sensibo/strings.json +++ b/homeassistant/components/sensibo/strings.json @@ -45,6 +45,28 @@ "humidity": "Humidity" } } + }, + "select": { + "horizontalswing": { + "state": { + "stopped": "Stopped", + "fixedleft": "Fixed left", + "fixedcenterleft": "Fixed center left", + "fixedcenter": "Fixed center", + "fixedcenterright": "Fixed center right", + "fixedright": "Fixed right", + "fixedleftright": "Fixed left right", + "rangecenter": "Range center", + "rangefull": "Range full" + } + }, + "light": { + "state": { + "on": "On", + "dim": "Dim", + "off": "Off" + } + } } } } diff --git a/homeassistant/components/sensibo/translations/bg.json b/homeassistant/components/sensibo/translations/bg.json index 50b9fabe5c2..d0c8f2fc810 100644 --- a/homeassistant/components/sensibo/translations/bg.json +++ b/homeassistant/components/sensibo/translations/bg.json @@ -28,6 +28,14 @@ } }, "entity": { + "select": { + "light": { + "state": { + "off": "\u0418\u0437\u043a\u043b.", + "on": "\u0412\u043a\u043b." + } + } + }, "sensor": { "smart_type": { "state": { diff --git a/homeassistant/components/sensibo/translations/ca.json b/homeassistant/components/sensibo/translations/ca.json index d1a30f07ffe..47349e575fb 100644 --- a/homeassistant/components/sensibo/translations/ca.json +++ b/homeassistant/components/sensibo/translations/ca.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Fix al centre", + "fixedcenterleft": "Fix al centre i a l'esquerra", + "fixedcenterright": "Fix al centre i a la dreta", + "fixedleft": "Fix a l'esquerra", + "fixedleftright": "Fix a l'esquerra i a la dreta", + "fixedright": "Fix a la dreta", + "rangecenter": "Rang central", + "rangefull": "Rang complet", + "stopped": "Aturat" + } + }, + "light": { + "state": { + "dim": "Atenuat", + "off": "OFF", + "on": "ON" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/de.json b/homeassistant/components/sensibo/translations/de.json index 35fb78b7949..a9fedfc27d0 100644 --- a/homeassistant/components/sensibo/translations/de.json +++ b/homeassistant/components/sensibo/translations/de.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Mittig fixiert", + "fixedcenterleft": "Mitte links fixiert", + "fixedcenterright": "Mitte rechts fixiert", + "fixedleft": "Links fixiert", + "fixedleftright": "Links rechts fixiert", + "fixedright": "Rechts fixiert", + "rangecenter": "Bereich Mitte", + "rangefull": "Bereich voll", + "stopped": "Angehalten" + } + }, + "light": { + "state": { + "dim": "Abgeblendet", + "off": "Aus", + "on": "An" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/el.json b/homeassistant/components/sensibo/translations/el.json index 022cf65ea6d..0ec04761c16 100644 --- a/homeassistant/components/sensibo/translations/el.json +++ b/homeassistant/components/sensibo/translations/el.json @@ -17,7 +17,7 @@ "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" }, "data_description": { - "api_key": "\u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03bd\u03ad\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af api." + "api_key": "\u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af api \u03c3\u03b1\u03c2" } }, "user": { @@ -25,12 +25,34 @@ "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" }, "data_description": { - "api_key": "\u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af api \u03c3\u03b1\u03c2." + "api_key": "\u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af api \u03c3\u03b1\u03c2" } } } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "\u03a3\u03c4\u03b1\u03b8\u03b5\u03c1\u03cc \u03ba\u03ad\u03bd\u03c4\u03c1\u03bf", + "fixedcenterleft": "\u03a3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ac \u03ba\u03b5\u03bd\u03c4\u03c1\u03bf\u03b1\u03c1\u03b9\u03c3\u03c4\u03b5\u03c1\u03ac", + "fixedcenterright": "\u03a3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ac \u03ba\u03b5\u03bd\u03c4\u03c1\u03bf\u03b4\u03b5\u03be\u03b9\u03ac", + "fixedleft": "\u03a3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ac \u03b1\u03c1\u03b9\u03c3\u03c4\u03b5\u03c1\u03ac", + "fixedleftright": "\u03a3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ac \u03b1\u03c1\u03b9\u03c3\u03c4\u03b5\u03c1\u03ac \u03b4\u03b5\u03be\u03b9\u03ac", + "fixedright": "\u03a3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ac \u03b4\u03b5\u03be\u03b9\u03ac", + "rangecenter": "\u039a\u03ad\u03bd\u03c4\u03c1\u03bf \u03b5\u03bc\u03b2\u03ad\u03bb\u03b5\u03b9\u03b1\u03c2", + "rangefull": "\u03a0\u03bb\u03ae\u03c1\u03b7\u03c2 \u03b5\u03bc\u03b2\u03ad\u03bb\u03b5\u03b9\u03b1", + "stopped": "\u03a3\u03c4\u03b1\u03bc\u03ac\u03c4\u03b7\u03c3\u03b5" + } + }, + "light": { + "state": { + "dim": "\u0391\u03bc\u03c5\u03b4\u03c1\u03cc", + "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc", + "on": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/en.json b/homeassistant/components/sensibo/translations/en.json index 14a399f63a5..323a186f86b 100644 --- a/homeassistant/components/sensibo/translations/en.json +++ b/homeassistant/components/sensibo/translations/en.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Fixed center", + "fixedcenterleft": "Fixed center left", + "fixedcenterright": "Fixed center right", + "fixedleft": "Fixed left", + "fixedleftright": "Fixed left right", + "fixedright": "Fixed right", + "rangecenter": "Range center", + "rangefull": "Range full", + "stopped": "Stopped" + } + }, + "light": { + "state": { + "dim": "Dim", + "off": "Off", + "on": "On" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/es.json b/homeassistant/components/sensibo/translations/es.json index b295bc24418..177edbfc002 100644 --- a/homeassistant/components/sensibo/translations/es.json +++ b/homeassistant/components/sensibo/translations/es.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Fijado al centro", + "fixedcenterleft": "Fijado al centro izquierda", + "fixedcenterright": "Fijado al centro derecha", + "fixedleft": "Fijado a la izquierda", + "fixedleftright": "Fijado a izquierda y derecha", + "fixedright": "Fijado a la derecha", + "rangecenter": "Rango al centro", + "rangefull": "Rango completo", + "stopped": "Detenido" + } + }, + "light": { + "state": { + "dim": "Atenuada", + "off": "Apagada", + "on": "Encendida" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/et.json b/homeassistant/components/sensibo/translations/et.json index 01feb3b925c..b5c6e4e279e 100644 --- a/homeassistant/components/sensibo/translations/et.json +++ b/homeassistant/components/sensibo/translations/et.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Keskele", + "fixedcenterleft": "Keskele ja vasakule", + "fixedcenterright": "Keskele ja paremale", + "fixedleft": "Vasakule", + "fixedleftright": "Vasakule ja paremale", + "fixedright": "Paremale", + "rangecenter": "Keskmine osa", + "rangefull": "T\u00e4isulatus", + "stopped": "Peatatud" + } + }, + "light": { + "state": { + "dim": "H\u00e4mar", + "off": "V\u00e4ljas", + "on": "Sees" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/fr.json b/homeassistant/components/sensibo/translations/fr.json index 3209ba2f2c8..02b5957bc6f 100644 --- a/homeassistant/components/sensibo/translations/fr.json +++ b/homeassistant/components/sensibo/translations/fr.json @@ -29,5 +29,15 @@ } } } + }, + "entity": { + "sensor": { + "smart_type": { + "state": { + "humidity": "Humidit\u00e9", + "temperature": "Temp\u00e9rature" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/hu.json b/homeassistant/components/sensibo/translations/hu.json index 27ab4c103bd..3829fb48e2c 100644 --- a/homeassistant/components/sensibo/translations/hu.json +++ b/homeassistant/components/sensibo/translations/hu.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Csak k\u00f6z\u00e9pre", + "fixedcenterleft": "Csak balra \u00e9s k\u00f6z\u00e9pre", + "fixedcenterright": "Csak k\u00f6z\u00e9pre \u00e9s jobbra", + "fixedleft": "Csak balra", + "fixedleftright": "Csak jobbra \u00e9s balra", + "fixedright": "Csak jobbra", + "rangecenter": "Tartom\u00e1ny k\u00f6zepe", + "rangefull": "Teljes tartom\u00e1ny", + "stopped": "Meg\u00e1ll\u00edtva" + } + }, + "light": { + "state": { + "dim": "Halv\u00e1ny", + "off": "Ki", + "on": "Be" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/id.json b/homeassistant/components/sensibo/translations/id.json index 5368bc42d31..3e2e6c1290e 100644 --- a/homeassistant/components/sensibo/translations/id.json +++ b/homeassistant/components/sensibo/translations/id.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Tetap tengah", + "fixedcenterleft": "Tetap kiri tengah", + "fixedcenterright": "Tetap kanan tengah", + "fixedleft": "Tetap kiri", + "fixedleftright": "Tetap kiri kanan", + "fixedright": "Tetap kanan", + "rangecenter": "Kisaran tengah", + "rangefull": "Kisaran penuh", + "stopped": "Terhenti" + } + }, + "light": { + "state": { + "dim": "Redup", + "off": "Mati", + "on": "Nyala" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/it.json b/homeassistant/components/sensibo/translations/it.json index 7676d85b5bd..d43a624b309 100644 --- a/homeassistant/components/sensibo/translations/it.json +++ b/homeassistant/components/sensibo/translations/it.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Fisso al centro", + "fixedcenterleft": "Fisso al centro sinistra", + "fixedcenterright": "Fisso al centro destra", + "fixedleft": "Fisso a sinistra", + "fixedleftright": "Fisso a sinistra destra", + "fixedright": "Fisso a destra", + "rangecenter": "Intervallo centrale", + "rangefull": "Intervallo completo", + "stopped": "Fermata" + } + }, + "light": { + "state": { + "dim": "Attenuata", + "off": "Spenta", + "on": "Accesa" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/ja.json b/homeassistant/components/sensibo/translations/ja.json index a537c9a5bf1..769766b7f56 100644 --- a/homeassistant/components/sensibo/translations/ja.json +++ b/homeassistant/components/sensibo/translations/ja.json @@ -31,6 +31,15 @@ } }, "entity": { + "select": { + "light": { + "state": { + "dim": "\u8584\u6697\u3044", + "off": "\u30aa\u30d5", + "on": "\u30aa\u30f3" + } + } + }, "sensor": { "smart_type": { "state": { diff --git a/homeassistant/components/sensibo/translations/nl.json b/homeassistant/components/sensibo/translations/nl.json index 9d8d0cd9871..a7eb4747cb9 100644 --- a/homeassistant/components/sensibo/translations/nl.json +++ b/homeassistant/components/sensibo/translations/nl.json @@ -31,6 +31,14 @@ } }, "entity": { + "select": { + "light": { + "state": { + "off": "Uit", + "on": "Aan" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/no.json b/homeassistant/components/sensibo/translations/no.json index 1adea0f007d..980a781f51d 100644 --- a/homeassistant/components/sensibo/translations/no.json +++ b/homeassistant/components/sensibo/translations/no.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Fast senter", + "fixedcenterleft": "Fast midt venstre", + "fixedcenterright": "Fast midt h\u00f8yre", + "fixedleft": "Fast venstre", + "fixedleftright": "Fast venstre h\u00f8yre", + "fixedright": "Rettet til h\u00f8yre", + "rangecenter": "Rekkeviddesenter", + "rangefull": "Rekkevidde fullt", + "stopped": "Stoppet" + } + }, + "light": { + "state": { + "dim": "Dim", + "off": "Av", + "on": "P\u00e5" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/pl.json b/homeassistant/components/sensibo/translations/pl.json index c52fb671aad..f5e182719b4 100644 --- a/homeassistant/components/sensibo/translations/pl.json +++ b/homeassistant/components/sensibo/translations/pl.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "po \u015brodku", + "fixedcenterleft": "po \u015brodku w lewo", + "fixedcenterright": "po \u015brodku w prawo", + "fixedleft": "w lewo", + "fixedleftright": "w prawo i w lewo", + "fixedright": "w prawo", + "rangecenter": "zasi\u0119g \u015brodkowy", + "rangefull": "pe\u0142ny zasi\u0119g", + "stopped": "zatrzymane" + } + }, + "light": { + "state": { + "dim": "ciemne", + "off": "wy\u0142.", + "on": "w\u0142." + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/pt-BR.json b/homeassistant/components/sensibo/translations/pt-BR.json index 506417c1da6..8294d7e4492 100644 --- a/homeassistant/components/sensibo/translations/pt-BR.json +++ b/homeassistant/components/sensibo/translations/pt-BR.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Fixo centro", + "fixedcenterleft": "Fixo centro esquerda", + "fixedcenterright": "Fixo centro direita", + "fixedleft": "Fixo esquerda", + "fixedleftright": "Fixo esquerda direita", + "fixedright": "Fixo direita", + "rangecenter": "Centro do alcance", + "rangefull": "Alcance completo", + "stopped": "Parado" + } + }, + "light": { + "state": { + "dim": "Dim", + "off": "Desligado", + "on": "Ligado" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/ru.json b/homeassistant/components/sensibo/translations/ru.json index 90da8687e85..3d26ccebe3f 100644 --- a/homeassistant/components/sensibo/translations/ru.json +++ b/homeassistant/components/sensibo/translations/ru.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "\u0424\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0446\u0435\u043d\u0442\u0440", + "fixedcenterleft": "\u0424\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0446\u0435\u043d\u0442\u0440 \u0441\u043b\u0435\u0432\u0430", + "fixedcenterright": "\u0424\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0446\u0435\u043d\u0442\u0440 \u0441\u043f\u0440\u0430\u0432\u0430", + "fixedleft": "\u0424\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043b\u0435\u0432\u044b\u0439", + "fixedleftright": "\u0424\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043b\u0435\u0432\u044b\u0439 \u043f\u0440\u0430\u0432\u044b\u0439", + "fixedright": "\u0424\u0438\u043a\u0441\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043f\u0440\u0430\u0432\u044b\u0439", + "rangecenter": "\u0426\u0435\u043d\u0442\u0440 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d\u0430", + "rangefull": "\u041f\u043e\u043b\u043d\u044b\u0439 \u0434\u0438\u0430\u043f\u0430\u0437\u043e\u043d", + "stopped": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e" + } + }, + "light": { + "state": { + "dim": "\u0422\u0443\u0441\u043a\u043b\u043e", + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/sensor.sv.json b/homeassistant/components/sensibo/translations/sensor.sv.json index b07d40e18fd..2b871b82b3e 100644 --- a/homeassistant/components/sensibo/translations/sensor.sv.json +++ b/homeassistant/components/sensibo/translations/sensor.sv.json @@ -3,6 +3,11 @@ "sensibo__sensitivity": { "n": "Normal", "s": "K\u00e4nslighet" + }, + "sensibo__smart_type": { + "feelslike": "K\u00e4nns som", + "humidity": "Luftfuktighet", + "temperature": "Temperatur" } } } \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/sk.json b/homeassistant/components/sensibo/translations/sk.json index e70216e0784..44a34c01748 100644 --- a/homeassistant/components/sensibo/translations/sk.json +++ b/homeassistant/components/sensibo/translations/sk.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Pevn\u00fd stred", + "fixedcenterleft": "Pevn\u00fd stred v\u013eavo", + "fixedcenterright": "Pevn\u00fd stred vpravo", + "fixedleft": "Pevn\u00e9 v\u013eavo", + "fixedleftright": "Pevn\u00e9 v\u013eavo vpravo", + "fixedright": "Pevn\u00e9 vpravo", + "rangecenter": "Stred rozsahu", + "rangefull": "Rozsah pln\u00fd", + "stopped": "Zastaven\u00e9" + } + }, + "light": { + "state": { + "dim": "Dim", + "off": "Vypnut\u00e9", + "on": "Zapnut\u00e9" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensibo/translations/tr.json b/homeassistant/components/sensibo/translations/tr.json index fc2abb330e4..4dcf6582443 100644 --- a/homeassistant/components/sensibo/translations/tr.json +++ b/homeassistant/components/sensibo/translations/tr.json @@ -29,5 +29,44 @@ } } } + }, + "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "Orta d\u00fczeltildi", + "fixedcenterleft": "Orta sol d\u00fczeltildi", + "fixedcenterright": "Orta sa\u011f d\u00fczeltildi", + "fixedleft": "Sol d\u00fczeltildi", + "fixedleftright": "Sa\u011f sol d\u00fczeltildi", + "fixedright": "Sa\u011f d\u00fczeltildi", + "rangecenter": "Menzil merkezi", + "rangefull": "Menzil dolu", + "stopped": "Durduruldu" + } + }, + "light": { + "state": { + "dim": "Dim", + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k" + } + } + }, + "sensor": { + "sensitivity": { + "state": { + "n": "Normal", + "s": "Duyarl\u0131" + } + }, + "smart_type": { + "state": { + "feelslike": "Hissedilen", + "humidity": "Nem", + "temperature": "S\u0131cakl\u0131k" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/uk.json b/homeassistant/components/sensibo/translations/uk.json new file mode 100644 index 00000000000..b37f57d197b --- /dev/null +++ b/homeassistant/components/sensibo/translations/uk.json @@ -0,0 +1,12 @@ +{ + "entity": { + "select": { + "light": { + "state": { + "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", + "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensibo/translations/zh-Hant.json b/homeassistant/components/sensibo/translations/zh-Hant.json index 3c144345f3d..e39b23237b2 100644 --- a/homeassistant/components/sensibo/translations/zh-Hant.json +++ b/homeassistant/components/sensibo/translations/zh-Hant.json @@ -31,6 +31,28 @@ } }, "entity": { + "select": { + "horizontalswing": { + "state": { + "fixedcenter": "\u56fa\u5b9a\u4e2d", + "fixedcenterleft": "\u56fa\u5b9a\u4e2d\u5de6", + "fixedcenterright": "\u56fa\u5b9a\u4e2d\u53f3", + "fixedleft": "\u56fa\u5b9a\u5de6", + "fixedleftright": "\u56fa\u5b9a\u5de6\u53f3", + "fixedright": "\u56fa\u5b9a\u53f3", + "rangecenter": "\u7bc4\u570d\u4e2d", + "rangefull": "\u5168\u7bc4\u570d", + "stopped": "\u505c\u6b62" + } + }, + "light": { + "state": { + "dim": "\u5fae\u5149", + "off": "\u95dc\u9589", + "on": "\u958b\u555f" + } + } + }, "sensor": { "sensitivity": { "state": { diff --git a/homeassistant/components/sensirion_ble/manifest.json b/homeassistant/components/sensirion_ble/manifest.json index a3011639d3e..da95b5828c9 100644 --- a/homeassistant/components/sensirion_ble/manifest.json +++ b/homeassistant/components/sensirion_ble/manifest.json @@ -14,7 +14,7 @@ } ], "requirements": ["sensirion-ble==0.0.1"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@akx"], "iot_class": "local_push" } diff --git a/homeassistant/components/sensirion_ble/sensor.py b/homeassistant/components/sensirion_ble/sensor.py index 2af5808fa56..3d288f92d12 100644 --- a/homeassistant/components/sensirion_ble/sensor.py +++ b/homeassistant/components/sensirion_ble/sensor.py @@ -1,8 +1,6 @@ """Support for Sensirion sensors.""" from __future__ import annotations -from typing import Optional, Union - from sensor_state_data import ( DeviceKey, SensorDescription, @@ -123,9 +121,7 @@ async def async_setup_entry( class SensirionBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a Sensirion BLE sensor.""" diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 5a96036f22b..35ffc1c3d2a 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -8,16 +8,14 @@ from dataclasses import dataclass from datetime import date, datetime, timedelta, timezone from decimal import Decimal, InvalidOperation as DecimalInvalidOperation import logging -from math import floor, log10 +from math import ceil, floor, log10 +import re from typing import Any, Final, cast, final -import voluptuous as vol - -from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( # noqa: F401, pylint: disable=[hass-deprecated-import] - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_MILLION, + +# pylint: disable=[hass-deprecated-import] +from homeassistant.const import ( # noqa: F401 CONF_UNIT_OF_MEASUREMENT, DEVICE_CLASS_AQI, DEVICE_CLASS_BATTERY, @@ -47,34 +45,11 @@ from homeassistant.const import ( # noqa: F401, pylint: disable=[hass-deprecate DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, DEVICE_CLASS_VOLTAGE, - LIGHT_LUX, - PERCENTAGE, - POWER_VOLT_AMPERE_REACTIVE, - SIGNAL_STRENGTH_DECIBELS, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - UnitOfApparentPower, - UnitOfDataRate, - UnitOfElectricCurrent, - UnitOfElectricPotential, - UnitOfEnergy, - UnitOfFrequency, - UnitOfInformation, - UnitOfIrradiance, - UnitOfLength, - UnitOfMass, - UnitOfPower, - UnitOfPrecipitationDepth, - UnitOfPressure, - UnitOfSoundPressure, - UnitOfSpeed, UnitOfTemperature, - UnitOfTime, - UnitOfVolume, - UnitOfVolumetricFlux, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.config_validation import ( # noqa: F401 +from homeassistant.helpers.config_validation import ( PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) @@ -82,477 +57,53 @@ from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity -from homeassistant.helpers.typing import ConfigType, StateType +from homeassistant.helpers.typing import UNDEFINED, ConfigType, StateType, UndefinedType from homeassistant.util import dt as dt_util -from homeassistant.util.unit_conversion import ( - BaseUnitConverter, - DataRateConverter, - DistanceConverter, - ElectricCurrentConverter, - ElectricPotentialConverter, - InformationConverter, - MassConverter, - PressureConverter, - SpeedConverter, - TemperatureConverter, - VolumeConverter, -) -from .const import CONF_STATE_CLASS # noqa: F401 +from .const import ( # noqa: F401 + ATTR_LAST_RESET, + ATTR_OPTIONS, + ATTR_STATE_CLASS, + CONF_STATE_CLASS, + DEVICE_CLASS_STATE_CLASSES, + DEVICE_CLASS_UNITS, + DEVICE_CLASSES, + DEVICE_CLASSES_SCHEMA, + DOMAIN, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL, + STATE_CLASS_TOTAL_INCREASING, + STATE_CLASSES, + STATE_CLASSES_SCHEMA, + UNIT_CONVERTERS, + SensorDeviceClass, + SensorStateClass, +) +from .websocket_api import async_setup as async_setup_ws_api _LOGGER: Final = logging.getLogger(__name__) -ATTR_LAST_RESET: Final = "last_reset" -ATTR_STATE_CLASS: Final = "state_class" -ATTR_OPTIONS: Final = "options" - -DOMAIN: Final = "sensor" - ENTITY_ID_FORMAT: Final = DOMAIN + ".{}" +NEGATIVE_ZERO_PATTERN = re.compile(r"^-(0\.?0*)$") + SCAN_INTERVAL: Final = timedelta(seconds=30) - -class SensorDeviceClass(StrEnum): - """Device class for sensors.""" - - # Non-numerical device classes - DATE = "date" - """Date. - - Unit of measurement: `None` - - ISO8601 format: https://en.wikipedia.org/wiki/ISO_8601 - """ - - DURATION = "duration" - """Fixed duration. - - Unit of measurement: `d`, `h`, `min`, `s` - """ - - ENUM = "enum" - """Enumeration. - - Provides a fixed list of options the state of the sensor can be in. - - Unit of measurement: `None` - """ - - TIMESTAMP = "timestamp" - """Timestamp. - - Unit of measurement: `None` - - ISO8601 format: https://en.wikipedia.org/wiki/ISO_8601 - """ - - # Numerical device classes, these should be aligned with NumberDeviceClass - APPARENT_POWER = "apparent_power" - """Apparent power. - - Unit of measurement: `VA` - """ - - AQI = "aqi" - """Air Quality Index. - - Unit of measurement: `None` - """ - - ATMOSPHERIC_PRESSURE = "atmospheric_pressure" - """Atmospheric pressure. - - Unit of measurement: `UnitOfPressure` units - """ - - BATTERY = "battery" - """Percentage of battery that is left. - - Unit of measurement: `%` - """ - - CO = "carbon_monoxide" - """Carbon Monoxide gas concentration. - - Unit of measurement: `ppm` (parts per million) - """ - - CO2 = "carbon_dioxide" - """Carbon Dioxide gas concentration. - - Unit of measurement: `ppm` (parts per million) - """ - - CURRENT = "current" - """Current. - - Unit of measurement: `A`, `mA` - """ - - DATA_RATE = "data_rate" - """Data rate. - - Unit of measurement: UnitOfDataRate - """ - - DATA_SIZE = "data_size" - """Data size. - - Unit of measurement: UnitOfInformation - """ - - DISTANCE = "distance" - """Generic distance. - - Unit of measurement: `LENGTH_*` units - - SI /metric: `mm`, `cm`, `m`, `km` - - USCS / imperial: `in`, `ft`, `yd`, `mi` - """ - - ENERGY = "energy" - """Energy. - - Unit of measurement: `Wh`, `kWh`, `MWh`, `GJ` - """ - - FREQUENCY = "frequency" - """Frequency. - - Unit of measurement: `Hz`, `kHz`, `MHz`, `GHz` - """ - - GAS = "gas" - """Gas. - - Unit of measurement: - - SI / metric: `m³` - - USCS / imperial: `ft³`, `CCF` - """ - - HUMIDITY = "humidity" - """Relative humidity. - - Unit of measurement: `%` - """ - - ILLUMINANCE = "illuminance" - """Illuminance. - - Unit of measurement: `lx` - """ - - IRRADIANCE = "irradiance" - """Irradiance. - - Unit of measurement: - - SI / metric: `W/m²` - - USCS / imperial: `BTU/(h⋅ft²)` - """ - - MOISTURE = "moisture" - """Moisture. - - Unit of measurement: `%` - """ - - MONETARY = "monetary" - """Amount of money. - - Unit of measurement: ISO4217 currency code - - See https://en.wikipedia.org/wiki/ISO_4217#Active_codes for active codes - """ - - NITROGEN_DIOXIDE = "nitrogen_dioxide" - """Amount of NO2. - - Unit of measurement: `µg/m³` - """ - - NITROGEN_MONOXIDE = "nitrogen_monoxide" - """Amount of NO. - - Unit of measurement: `µg/m³` - """ - - NITROUS_OXIDE = "nitrous_oxide" - """Amount of N2O. - - Unit of measurement: `µg/m³` - """ - - OZONE = "ozone" - """Amount of O3. - - Unit of measurement: `µg/m³` - """ - - PM1 = "pm1" - """Particulate matter <= 0.1 μm. - - Unit of measurement: `µg/m³` - """ - - PM10 = "pm10" - """Particulate matter <= 10 μm. - - Unit of measurement: `µg/m³` - """ - - PM25 = "pm25" - """Particulate matter <= 2.5 μm. - - Unit of measurement: `µg/m³` - """ - - POWER_FACTOR = "power_factor" - """Power factor. - - Unit of measurement: `%`, `None` - """ - - POWER = "power" - """Power. - - Unit of measurement: `W`, `kW` - """ - - PRECIPITATION = "precipitation" - """Precipitation. - - Unit of measurement: UnitOfPrecipitationDepth - - SI / metric: `cm`, `mm` - - USCS / imperial: `in` - """ - - PRECIPITATION_INTENSITY = "precipitation_intensity" - """Precipitation intensity. - - Unit of measurement: UnitOfVolumetricFlux - - SI /metric: `mm/d`, `mm/h` - - USCS / imperial: `in/d`, `in/h` - """ - - PRESSURE = "pressure" - """Pressure. - - Unit of measurement: - - `mbar`, `cbar`, `bar` - - `Pa`, `hPa`, `kPa` - - `inHg` - - `psi` - """ - - REACTIVE_POWER = "reactive_power" - """Reactive power. - - Unit of measurement: `var` - """ - - SIGNAL_STRENGTH = "signal_strength" - """Signal strength. - - Unit of measurement: `dB`, `dBm` - """ - - SOUND_PRESSURE = "sound_pressure" - """Sound pressure. - - Unit of measurement: `dB`, `dBA` - """ - - SPEED = "speed" - """Generic speed. - - Unit of measurement: `SPEED_*` units or `UnitOfVolumetricFlux` - - SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h` - - USCS / imperial: `in/d`, `in/h`, `ft/s`, `mph` - - Nautical: `kn` - """ - - SULPHUR_DIOXIDE = "sulphur_dioxide" - """Amount of SO2. - - Unit of measurement: `µg/m³` - """ - - TEMPERATURE = "temperature" - """Temperature. - - Unit of measurement: `°C`, `°F` - """ - - VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" - """Amount of VOC. - - Unit of measurement: `µg/m³` - """ - - VOLTAGE = "voltage" - """Voltage. - - Unit of measurement: `V`, `mV` - """ - - VOLUME = "volume" - """Generic volume. - - Unit of measurement: `VOLUME_*` units - - SI / metric: `mL`, `L`, `m³` - - USCS / imperial: `ft³`, `CCF`, `fl. oz.`, `gal` (warning: volumes expressed in - USCS/imperial units are currently assumed to be US volumes) - """ - - WATER = "water" - """Water. - - Unit of measurement: - - SI / metric: `m³`, `L` - - USCS / imperial: `ft³`, `CCF`, `gal` (warning: volumes expressed in - USCS/imperial units are currently assumed to be US volumes) - """ - - WEIGHT = "weight" - """Generic weight, represents a measurement of an object's mass. - - Weight is used instead of mass to fit with every day language. - - Unit of measurement: `MASS_*` units - - SI / metric: `µg`, `mg`, `g`, `kg` - - USCS / imperial: `oz`, `lb` - """ - - WIND_SPEED = "wind_speed" - """Wind speed. - - Unit of measurement: `SPEED_*` units - - SI /metric: `m/s`, `km/h` - - USCS / imperial: `ft/s`, `mph` - - Nautical: `kn` - """ - - -DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(SensorDeviceClass)) - -# DEVICE_CLASSES is deprecated as of 2021.12 -# use the SensorDeviceClass enum instead. -DEVICE_CLASSES: Final[list[str]] = [cls.value for cls in SensorDeviceClass] - - -class SensorStateClass(StrEnum): - """State class for sensors.""" - - MEASUREMENT = "measurement" - """The state represents a measurement in present time.""" - - TOTAL = "total" - """The state represents a total amount. - - For example: net energy consumption""" - - TOTAL_INCREASING = "total_increasing" - """The state represents a monotonically increasing total. - - For example: an amount of consumed gas""" - - -STATE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(SensorStateClass)) - - -# STATE_CLASS* is deprecated as of 2021.12 -# use the SensorStateClass enum instead. -STATE_CLASS_MEASUREMENT: Final = "measurement" -STATE_CLASS_TOTAL: Final = "total" -STATE_CLASS_TOTAL_INCREASING: Final = "total_increasing" -STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] - -# Note: this needs to be aligned with frontend: OVERRIDE_SENSOR_UNITS in -# `entity-registry-settings.ts` -UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = { - SensorDeviceClass.DATA_RATE: DataRateConverter, - SensorDeviceClass.DATA_SIZE: InformationConverter, - SensorDeviceClass.DISTANCE: DistanceConverter, - SensorDeviceClass.CURRENT: ElectricCurrentConverter, - SensorDeviceClass.GAS: VolumeConverter, - SensorDeviceClass.PRECIPITATION: DistanceConverter, - SensorDeviceClass.PRESSURE: PressureConverter, - SensorDeviceClass.SPEED: SpeedConverter, - SensorDeviceClass.TEMPERATURE: TemperatureConverter, - SensorDeviceClass.VOLTAGE: ElectricPotentialConverter, - SensorDeviceClass.VOLUME: VolumeConverter, - SensorDeviceClass.WATER: VolumeConverter, - SensorDeviceClass.WEIGHT: MassConverter, - SensorDeviceClass.WIND_SPEED: SpeedConverter, -} - -DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { - SensorDeviceClass.APPARENT_POWER: set(UnitOfApparentPower), - SensorDeviceClass.AQI: {None}, - SensorDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure), - SensorDeviceClass.BATTERY: {PERCENTAGE}, - SensorDeviceClass.CO: {CONCENTRATION_PARTS_PER_MILLION}, - SensorDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION}, - SensorDeviceClass.CURRENT: set(UnitOfElectricCurrent), - SensorDeviceClass.DATA_RATE: set(UnitOfDataRate), - SensorDeviceClass.DATA_SIZE: set(UnitOfInformation), - SensorDeviceClass.DISTANCE: set(UnitOfLength), - SensorDeviceClass.DURATION: { - UnitOfTime.DAYS, - UnitOfTime.HOURS, - UnitOfTime.MINUTES, - UnitOfTime.SECONDS, - }, - SensorDeviceClass.ENERGY: set(UnitOfEnergy), - SensorDeviceClass.FREQUENCY: set(UnitOfFrequency), - SensorDeviceClass.GAS: { - UnitOfVolume.CENTUM_CUBIC_FEET, - UnitOfVolume.CUBIC_FEET, - UnitOfVolume.CUBIC_METERS, - }, - SensorDeviceClass.HUMIDITY: {PERCENTAGE}, - SensorDeviceClass.ILLUMINANCE: {LIGHT_LUX}, - SensorDeviceClass.IRRADIANCE: set(UnitOfIrradiance), - SensorDeviceClass.MOISTURE: {PERCENTAGE}, - SensorDeviceClass.NITROGEN_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - SensorDeviceClass.NITROGEN_MONOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - SensorDeviceClass.NITROUS_OXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - SensorDeviceClass.OZONE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - SensorDeviceClass.PM1: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - SensorDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - SensorDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - SensorDeviceClass.POWER_FACTOR: {PERCENTAGE, None}, - SensorDeviceClass.POWER: {UnitOfPower.WATT, UnitOfPower.KILO_WATT}, - SensorDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth), - SensorDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux), - SensorDeviceClass.PRESSURE: set(UnitOfPressure), - SensorDeviceClass.REACTIVE_POWER: {POWER_VOLT_AMPERE_REACTIVE}, - SensorDeviceClass.SIGNAL_STRENGTH: { - SIGNAL_STRENGTH_DECIBELS, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - }, - SensorDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure), - SensorDeviceClass.SPEED: set(UnitOfSpeed).union(set(UnitOfVolumetricFlux)), - SensorDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, - SensorDeviceClass.TEMPERATURE: { - UnitOfTemperature.CELSIUS, - UnitOfTemperature.FAHRENHEIT, - }, - SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: { - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - }, - SensorDeviceClass.VOLTAGE: set(UnitOfElectricPotential), - SensorDeviceClass.VOLUME: set(UnitOfVolume), - SensorDeviceClass.WATER: { - UnitOfVolume.CENTUM_CUBIC_FEET, - UnitOfVolume.CUBIC_FEET, - UnitOfVolume.CUBIC_METERS, - UnitOfVolume.GALLONS, - UnitOfVolume.LITERS, - }, - SensorDeviceClass.WEIGHT: set(UnitOfMass), - SensorDeviceClass.WIND_SPEED: set(UnitOfSpeed), -} +__all__ = [ + "ATTR_LAST_RESET", + "ATTR_OPTIONS", + "ATTR_STATE_CLASS", + "CONF_STATE_CLASS", + "DOMAIN", + "PLATFORM_SCHEMA_BASE", + "PLATFORM_SCHEMA", + "RestoreSensor", + "SensorDeviceClass", + "SensorEntity", + "SensorEntityDescription", + "SensorExtraStoredData", + "SensorStateClass", +] # mypy: disallow-any-generics @@ -563,6 +114,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) + async_setup_ws_api(hass) await component.async_setup(config) return True @@ -584,11 +136,12 @@ class SensorEntityDescription(EntityDescription): """A class that describes sensor entities.""" device_class: SensorDeviceClass | None = None - suggested_unit_of_measurement: str | None = None last_reset: datetime | None = None + native_precision: int | None = None native_unit_of_measurement: str | None = None - state_class: SensorStateClass | str | None = None options: list[str] | None = None + state_class: SensorStateClass | str | None = None + suggested_unit_of_measurement: str | None = None unit_of_measurement: None = None # Type override, use native_unit_of_measurement @@ -598,6 +151,7 @@ class SensorEntity(Entity): entity_description: SensorEntityDescription _attr_device_class: SensorDeviceClass | None _attr_last_reset: datetime | None + _attr_native_precision: int | None _attr_native_unit_of_measurement: str | None _attr_native_value: StateType | date | datetime | Decimal = None _attr_options: list[str] | None @@ -607,9 +161,12 @@ class SensorEntity(Entity): _attr_unit_of_measurement: None = ( None # Subclasses of SensorEntity should not set this ) + _invalid_numeric_value_reported = False + _invalid_state_class_reported = False _invalid_unit_of_measurement_reported = False _last_reset_reported = False - _sensor_option_unit_of_measurement: str | None = None + _sensor_option_precision: int | None = None + _sensor_option_unit_of_measurement: str | None | UndefinedType = UNDEFINED @callback def add_to_platform_start( @@ -786,6 +343,60 @@ class SensorEntity(Entity): """Return the value reported by the sensor.""" return self._attr_native_value + @property + def native_precision(self) -> int | None: + """Return the number of digits after the decimal point for the sensor's state. + + If native_precision is None, no rounding is done unless the sensor is subject + to unit conversion. + + The display precision is influenced by unit conversion, a sensor which has + native_unit_of_measurement 'Wh' and is converted to 'kWh' will have its + native_precision increased by 3. + """ + if hasattr(self, "_attr_native_precision"): + return self._attr_native_precision + if hasattr(self, "entity_description"): + return self.entity_description.native_precision + return None + + @final + @property + def precision(self) -> int | None: + """Return the number of digits after the decimal point for the sensor's state. + + This is the precision after unit conversion. + """ + # Highest priority, for registered entities: precision set by user + if self._sensor_option_precision is not None: + return self._sensor_option_precision + + # Second priority, native precision + if (precision := self.native_precision) is None: + return None + + device_class = self.device_class + native_unit_of_measurement = self.native_unit_of_measurement + unit_of_measurement = self.unit_of_measurement + + if ( + native_unit_of_measurement != unit_of_measurement + and device_class in UNIT_CONVERTERS + ): + converter = UNIT_CONVERTERS[device_class] + + # Scale the precision when converting to a larger or smaller unit + # For example 1.1 Wh should be rendered as 0.0011 kWh, not 0.0 kWh + ratio_log = log10( + converter.get_unit_ratio( + native_unit_of_measurement, unit_of_measurement + ) + ) + ratio_log = floor(ratio_log) if ratio_log > 0 else ceil(ratio_log) + precision = max(0, precision + ratio_log) + + return precision + @property def native_unit_of_measurement(self) -> str | None: """Return the unit of measurement of the sensor, if any.""" @@ -806,12 +417,12 @@ class SensorEntity(Entity): For sensors without a `unique_id`, this takes precedence over legacy temperature conversion rules only. - For sensors with a `unique_id`, this is applied only if the unit is not set by the user, - and takes precedence over automatic device-class conversion rules. + For sensors with a `unique_id`, this is applied only if the unit is not set by + the user, and takes precedence over automatic device-class conversion rules. Note: - suggested_unit_of_measurement is stored in the entity registry the first time - the entity is seen, and then never updated. + suggested_unit_of_measurement is stored in the entity registry the first + time the entity is seen, and then never updated. """ if hasattr(self, "_attr_suggested_unit_of_measurement"): return self._attr_suggested_unit_of_measurement @@ -823,9 +434,9 @@ class SensorEntity(Entity): @property def unit_of_measurement(self) -> str | None: """Return the unit of measurement of the entity, after unit conversion.""" - # Highest priority, for registered entities: unit set by user, with fallback to unit suggested - # by integration or secondary fallback to unit conversion rules - if self._sensor_option_unit_of_measurement: + # Highest priority, for registered entities: unit set by user,with fallback to + # unit suggested by integration or secondary fallback to unit conversion rules + if self._sensor_option_unit_of_measurement is not UNDEFINED: return self._sensor_option_unit_of_measurement # Second priority, for non registered entities: unit suggested by integration @@ -837,7 +448,7 @@ class SensorEntity(Entity): native_unit_of_measurement = self.native_unit_of_measurement if ( - self.device_class == DEVICE_CLASS_TEMPERATURE + self.device_class == SensorDeviceClass.TEMPERATURE and native_unit_of_measurement in {UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT} ): @@ -848,15 +459,65 @@ class SensorEntity(Entity): @final @property - def state(self) -> Any: + def state(self) -> Any: # noqa: C901 """Return the state of the sensor and perform unit conversions, if needed.""" native_unit_of_measurement = self.native_unit_of_measurement unit_of_measurement = self.unit_of_measurement value = self.native_value - device_class = self.device_class + device_class: SensorDeviceClass | None = None + with suppress(ValueError): + # For the sake of validation, we can ignore custom device classes + # (customization and legacy style translations) + device_class = SensorDeviceClass(str(self.device_class)) + state_class = self.state_class + + # Sensors with device classes indicating a non-numeric value + # should not have a unit of measurement + if ( + device_class + in { + SensorDeviceClass.DATE, + SensorDeviceClass.ENUM, + SensorDeviceClass.TIMESTAMP, + } + and unit_of_measurement + ): + raise ValueError( + f"Sensor {self.entity_id} has a unit of measurement and thus " + "indicating it has a numeric value; however, it has the " + f"non-numeric device class: {device_class}" + ) + + # Validate state class for sensors with a device class + if ( + state_class + and not self._invalid_state_class_reported + and device_class + and (classes := DEVICE_CLASS_STATE_CLASSES.get(device_class)) is not None + and state_class not in classes + ): + self._invalid_state_class_reported = True + report_issue = self._suggest_report_issue() + + # This should raise in Home Assistant Core 2023.6 + _LOGGER.warning( + "Entity %s (%s) is using state class '%s' which " + "is impossible considering device class ('%s') it is using; " + "Please update your configuration if your entity is manually " + "configured, otherwise %s", + self.entity_id, + type(self), + state_class, + device_class, + report_issue, + ) + + # Checks below only apply if there is a value + if value is None: + return None # Received a datetime - if value is not None and device_class == DEVICE_CLASS_TIMESTAMP: + if device_class == SensorDeviceClass.TIMESTAMP: try: # We cast the value, to avoid using isinstance, but satisfy # typechecking. The errors are guarded in this try. @@ -878,7 +539,7 @@ class SensorEntity(Entity): ) from err # Received a date value - if value is not None and device_class == DEVICE_CLASS_DATE: + if device_class == SensorDeviceClass.DATE: try: # We cast the value, to avoid using isinstance, but satisfy # typechecking. The errors are guarded in this try. @@ -890,31 +551,8 @@ class SensorEntity(Entity): f"but provides state {value}:{type(value)} resulting in '{err}'" ) from err - # Sensors with device classes indicating a non-numeric value - # should not have a state class or unit of measurement - if device_class in { - SensorDeviceClass.DATE, - SensorDeviceClass.ENUM, - SensorDeviceClass.TIMESTAMP, - }: - if self.state_class: - raise ValueError( - f"Sensor {self.entity_id} has a state class and thus indicating " - "it has a numeric value; however, it has the non-numeric " - f"device class: {device_class}" - ) - - if unit_of_measurement: - raise ValueError( - f"Sensor {self.entity_id} has a unit of measurement and thus " - "indicating it has a numeric value; however, it has the " - f"non-numeric device class: {device_class}" - ) - # Enum checks - if value is not None and ( - device_class == SensorDeviceClass.ENUM or self.options is not None - ): + if device_class == SensorDeviceClass.ENUM or self.options is not None: if device_class != SensorDeviceClass.ENUM: reason = "is missing the enum device class" if device_class is not None: @@ -928,47 +566,103 @@ class SensorEntity(Entity): f"Sensor {self.entity_id} provides state value '{value}', " "which is not in the list of options provided" ) + return value + + precision = self.precision + + # If the sensor has neither a device class, a state class, a unit of measurement + # nor a precision then there are no further checks or conversions + if ( + not device_class + and not state_class + and not unit_of_measurement + and precision is None + ): + return value + + # From here on a numerical value is expected + numerical_value: int | float | Decimal + if not isinstance(value, (int, float, Decimal)): + try: + if isinstance(value, str) and "." not in value: + numerical_value = int(value) + else: + numerical_value = float(value) # type:ignore[arg-type] + except (TypeError, ValueError) as err: + # Raise if precision is not None, for other cases log a warning + if precision is not None: + raise ValueError( + f"Sensor {self.entity_id} has device class {device_class}, " + f"state class {state_class} unit {unit_of_measurement} and " + f"precision {precision} thus indicating it has a numeric value;" + f" however, it has the non-numeric value: {value} " + f"({type(value)})" + ) from err + # This should raise in Home Assistant Core 2023.4 + if not self._invalid_numeric_value_reported: + self._invalid_numeric_value_reported = True + report_issue = self._suggest_report_issue() + _LOGGER.warning( + "Sensor %s has device class %s, state class %s and unit %s " + "thus indicating it has a numeric value; however, it has the " + "non-numeric value: %s (%s); Please update your configuration " + "if your entity is manually configured, otherwise %s", + self.entity_id, + device_class, + state_class, + unit_of_measurement, + value, + type(value), + report_issue, + ) + return value + else: + numerical_value = value if ( - value is not None - and native_unit_of_measurement != unit_of_measurement + native_unit_of_measurement != unit_of_measurement and device_class in UNIT_CONVERTERS ): - assert unit_of_measurement - assert native_unit_of_measurement + # Unit conversion needed converter = UNIT_CONVERTERS[device_class] - value_s = str(value) - prec = len(value_s) - value_s.index(".") - 1 if "." in value_s else 0 - - # Scale the precision when converting to a larger unit - # For example 1.1 Wh should be rendered as 0.0011 kWh, not 0.0 kWh - ratio_log = max( - 0, - log10( - converter.get_unit_ratio( - native_unit_of_measurement, unit_of_measurement - ) - ), - ) - prec = prec + floor(ratio_log) - - # Suppress ValueError (Could not convert sensor_value to float) - with suppress(ValueError): - value_f = float(value) # type: ignore[arg-type] - value_f_new = converter.convert( - value_f, - native_unit_of_measurement, - unit_of_measurement, + if precision is None: + # Deduce the precision by finding the decimal point, if any + value_s = str(value) + precision = ( + len(value_s) - value_s.index(".") - 1 if "." in value_s else 0 ) - # Round to the wanted precision - value = round(value_f_new) if prec == 0 else round(value_f_new, prec) + # Scale the precision when converting to a larger unit + # For example 1.1 Wh should be rendered as 0.0011 kWh, not 0.0 kWh + ratio_log = max( + 0, + log10( + converter.get_unit_ratio( + native_unit_of_measurement, unit_of_measurement + ) + ), + ) + precision = precision + floor(ratio_log) + + converted_numerical_value = converter.convert( + float(numerical_value), + native_unit_of_measurement, + unit_of_measurement, + ) + value = f"{converted_numerical_value:.{precision}f}" + # This can be replaced with adding the z option when we drop support for + # Python 3.10 + value = NEGATIVE_ZERO_PATTERN.sub(r"\1", value) + elif precision is not None: + value = f"{numerical_value:.{precision}f}" + # This can be replaced with adding the z option when we drop support for + # Python 3.10 + value = NEGATIVE_ZERO_PATTERN.sub(r"\1", value) # Validate unit of measurement used for sensors with a device class if ( not self._invalid_unit_of_measurement_reported - and value is not None and device_class and (units := DEVICE_CLASS_UNITS.get(device_class)) is not None and native_unit_of_measurement not in units @@ -981,6 +675,7 @@ class SensorEntity(Entity): ( "Entity %s (%s) is using native unit of measurement '%s' which " "is not a valid unit for the device class ('%s') it is using; " + "expected one of %s; " "Please update your configuration if your entity is manually " "configured, otherwise %s" ), @@ -988,6 +683,7 @@ class SensorEntity(Entity): type(self), native_unit_of_measurement, device_class, + [str(unit) if unit else "no unit of measurement" for unit in units], report_issue, ) @@ -1004,28 +700,41 @@ class SensorEntity(Entity): return super().__repr__() - def _custom_unit_or_none(self, primary_key: str, secondary_key: str) -> str | None: - """Return a custom unit, or None if it's not compatible with the native unit.""" + def _custom_precision_or_none(self) -> int | None: + """Return a custom precisions or None if not set.""" + assert self.registry_entry + if (sensor_options := self.registry_entry.options.get(DOMAIN)) and ( + precision := sensor_options.get("precision") + ) is not None: + return int(precision) + return None + + def _custom_unit_or_undef( + self, primary_key: str, secondary_key: str + ) -> str | None | UndefinedType: + """Return a custom unit, or UNDEFINED if not compatible with the native unit.""" assert self.registry_entry if ( (sensor_options := self.registry_entry.options.get(primary_key)) - and (custom_unit := sensor_options.get(secondary_key)) + and secondary_key in sensor_options and (device_class := self.device_class) in UNIT_CONVERTERS and self.native_unit_of_measurement in UNIT_CONVERTERS[device_class].VALID_UNITS - and custom_unit in UNIT_CONVERTERS[device_class].VALID_UNITS + and (custom_unit := sensor_options[secondary_key]) + in UNIT_CONVERTERS[device_class].VALID_UNITS ): return cast(str, custom_unit) - return None + return UNDEFINED @callback def async_registry_entry_updated(self) -> None: """Run when the entity registry entry has been updated.""" - self._sensor_option_unit_of_measurement = self._custom_unit_or_none( + self._sensor_option_precision = self._custom_precision_or_none() + self._sensor_option_unit_of_measurement = self._custom_unit_or_undef( DOMAIN, CONF_UNIT_OF_MEASUREMENT ) - if not self._sensor_option_unit_of_measurement: - self._sensor_option_unit_of_measurement = self._custom_unit_or_none( + if self._sensor_option_unit_of_measurement is UNDEFINED: + self._sensor_option_unit_of_measurement = self._custom_unit_or_undef( f"{DOMAIN}.private", "suggested_unit_of_measurement" ) diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index 54d683242ea..c8402a28ffe 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -1,4 +1,560 @@ """Constants for sensor.""" +from __future__ import annotations + from typing import Final +import voluptuous as vol + +from homeassistant.backports.enum import StrEnum +from homeassistant.const import ( + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, + LIGHT_LUX, + PERCENTAGE, + POWER_VOLT_AMPERE_REACTIVE, + SIGNAL_STRENGTH_DECIBELS, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + UnitOfApparentPower, + UnitOfDataRate, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfFrequency, + UnitOfInformation, + UnitOfIrradiance, + UnitOfLength, + UnitOfMass, + UnitOfPower, + UnitOfPrecipitationDepth, + UnitOfPressure, + UnitOfSoundPressure, + UnitOfSpeed, + UnitOfTemperature, + UnitOfTime, + UnitOfVolume, + UnitOfVolumetricFlux, +) +from homeassistant.util.unit_conversion import ( + BaseUnitConverter, + DataRateConverter, + DistanceConverter, + ElectricCurrentConverter, + ElectricPotentialConverter, + EnergyConverter, + InformationConverter, + MassConverter, + PressureConverter, + SpeedConverter, + TemperatureConverter, + UnitlessRatioConverter, + VolumeConverter, +) + +DOMAIN: Final = "sensor" + CONF_STATE_CLASS: Final = "state_class" + +ATTR_LAST_RESET: Final = "last_reset" +ATTR_STATE_CLASS: Final = "state_class" +ATTR_OPTIONS: Final = "options" + + +class SensorDeviceClass(StrEnum): + """Device class for sensors.""" + + # Non-numerical device classes + DATE = "date" + """Date. + + Unit of measurement: `None` + + ISO8601 format: https://en.wikipedia.org/wiki/ISO_8601 + """ + + DURATION = "duration" + """Fixed duration. + + Unit of measurement: `d`, `h`, `min`, `s` + """ + + ENUM = "enum" + """Enumeration. + + Provides a fixed list of options the state of the sensor can be in. + + Unit of measurement: `None` + """ + + TIMESTAMP = "timestamp" + """Timestamp. + + Unit of measurement: `None` + + ISO8601 format: https://en.wikipedia.org/wiki/ISO_8601 + """ + + # Numerical device classes, these should be aligned with NumberDeviceClass + APPARENT_POWER = "apparent_power" + """Apparent power. + + Unit of measurement: `VA` + """ + + AQI = "aqi" + """Air Quality Index. + + Unit of measurement: `None` + """ + + ATMOSPHERIC_PRESSURE = "atmospheric_pressure" + """Atmospheric pressure. + + Unit of measurement: `UnitOfPressure` units + """ + + BATTERY = "battery" + """Percentage of battery that is left. + + Unit of measurement: `%` + """ + + CO = "carbon_monoxide" + """Carbon Monoxide gas concentration. + + Unit of measurement: `ppm` (parts per million) + """ + + CO2 = "carbon_dioxide" + """Carbon Dioxide gas concentration. + + Unit of measurement: `ppm` (parts per million) + """ + + CURRENT = "current" + """Current. + + Unit of measurement: `A`, `mA` + """ + + DATA_RATE = "data_rate" + """Data rate. + + Unit of measurement: UnitOfDataRate + """ + + DATA_SIZE = "data_size" + """Data size. + + Unit of measurement: UnitOfInformation + """ + + DISTANCE = "distance" + """Generic distance. + + Unit of measurement: `LENGTH_*` units + - SI /metric: `mm`, `cm`, `m`, `km` + - USCS / imperial: `in`, `ft`, `yd`, `mi` + """ + + ENERGY = "energy" + """Energy. + + Unit of measurement: `Wh`, `kWh`, `MWh`, `MJ`, `GJ` + """ + + FREQUENCY = "frequency" + """Frequency. + + Unit of measurement: `Hz`, `kHz`, `MHz`, `GHz` + """ + + GAS = "gas" + """Gas. + + Unit of measurement: + - SI / metric: `m³` + - USCS / imperial: `ft³`, `CCF` + """ + + HUMIDITY = "humidity" + """Relative humidity. + + Unit of measurement: `%` + """ + + ILLUMINANCE = "illuminance" + """Illuminance. + + Unit of measurement: `lx` + """ + + IRRADIANCE = "irradiance" + """Irradiance. + + Unit of measurement: + - SI / metric: `W/m²` + - USCS / imperial: `BTU/(h⋅ft²)` + """ + + MOISTURE = "moisture" + """Moisture. + + Unit of measurement: `%` + """ + + MONETARY = "monetary" + """Amount of money. + + Unit of measurement: ISO4217 currency code + + See https://en.wikipedia.org/wiki/ISO_4217#Active_codes for active codes + """ + + NITROGEN_DIOXIDE = "nitrogen_dioxide" + """Amount of NO2. + + Unit of measurement: `µg/m³` + """ + + NITROGEN_MONOXIDE = "nitrogen_monoxide" + """Amount of NO. + + Unit of measurement: `µg/m³` + """ + + NITROUS_OXIDE = "nitrous_oxide" + """Amount of N2O. + + Unit of measurement: `µg/m³` + """ + + OZONE = "ozone" + """Amount of O3. + + Unit of measurement: `µg/m³` + """ + + PM1 = "pm1" + """Particulate matter <= 0.1 μm. + + Unit of measurement: `µg/m³` + """ + + PM10 = "pm10" + """Particulate matter <= 10 μm. + + Unit of measurement: `µg/m³` + """ + + PM25 = "pm25" + """Particulate matter <= 2.5 μm. + + Unit of measurement: `µg/m³` + """ + + POWER_FACTOR = "power_factor" + """Power factor. + + Unit of measurement: `%`, `None` + """ + + POWER = "power" + """Power. + + Unit of measurement: `W`, `kW` + """ + + PRECIPITATION = "precipitation" + """Accumulated precipitation. + + Unit of measurement: UnitOfPrecipitationDepth + - SI / metric: `cm`, `mm` + - USCS / imperial: `in` + """ + + PRECIPITATION_INTENSITY = "precipitation_intensity" + """Precipitation intensity. + + Unit of measurement: UnitOfVolumetricFlux + - SI /metric: `mm/d`, `mm/h` + - USCS / imperial: `in/d`, `in/h` + """ + + PRESSURE = "pressure" + """Pressure. + + Unit of measurement: + - `mbar`, `cbar`, `bar` + - `Pa`, `hPa`, `kPa` + - `inHg` + - `psi` + """ + + REACTIVE_POWER = "reactive_power" + """Reactive power. + + Unit of measurement: `var` + """ + + SIGNAL_STRENGTH = "signal_strength" + """Signal strength. + + Unit of measurement: `dB`, `dBm` + """ + + SOUND_PRESSURE = "sound_pressure" + """Sound pressure. + + Unit of measurement: `dB`, `dBA` + """ + + SPEED = "speed" + """Generic speed. + + Unit of measurement: `SPEED_*` units or `UnitOfVolumetricFlux` + - SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h` + - USCS / imperial: `in/d`, `in/h`, `ft/s`, `mph` + - Nautical: `kn` + """ + + SULPHUR_DIOXIDE = "sulphur_dioxide" + """Amount of SO2. + + Unit of measurement: `µg/m³` + """ + + TEMPERATURE = "temperature" + """Temperature. + + Unit of measurement: `°C`, `°F`, `K` + """ + + VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" + """Amount of VOC. + + Unit of measurement: `µg/m³` + """ + + VOLTAGE = "voltage" + """Voltage. + + Unit of measurement: `V`, `mV` + """ + + VOLUME = "volume" + """Generic volume. + + Unit of measurement: `VOLUME_*` units + - SI / metric: `mL`, `L`, `m³` + - USCS / imperial: `ft³`, `CCF`, `fl. oz.`, `gal` (warning: volumes expressed in + USCS/imperial units are currently assumed to be US volumes) + """ + + WATER = "water" + """Water. + + Unit of measurement: + - SI / metric: `m³`, `L` + - USCS / imperial: `ft³`, `CCF`, `gal` (warning: volumes expressed in + USCS/imperial units are currently assumed to be US volumes) + """ + + WEIGHT = "weight" + """Generic weight, represents a measurement of an object's mass. + + Weight is used instead of mass to fit with every day language. + + Unit of measurement: `MASS_*` units + - SI / metric: `µg`, `mg`, `g`, `kg` + - USCS / imperial: `oz`, `lb` + """ + + WIND_SPEED = "wind_speed" + """Wind speed. + + Unit of measurement: `SPEED_*` units + - SI /metric: `m/s`, `km/h` + - USCS / imperial: `ft/s`, `mph` + - Nautical: `kn` + """ + + +DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(SensorDeviceClass)) + +# DEVICE_CLASSES is deprecated as of 2021.12 +# use the SensorDeviceClass enum instead. +DEVICE_CLASSES: Final[list[str]] = [cls.value for cls in SensorDeviceClass] + + +class SensorStateClass(StrEnum): + """State class for sensors.""" + + MEASUREMENT = "measurement" + """The state represents a measurement in present time.""" + + TOTAL = "total" + """The state represents a total amount. + + For example: net energy consumption""" + + TOTAL_INCREASING = "total_increasing" + """The state represents a monotonically increasing total. + + For example: an amount of consumed gas""" + + +STATE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(SensorStateClass)) + + +# STATE_CLASS* is deprecated as of 2021.12 +# use the SensorStateClass enum instead. +STATE_CLASS_MEASUREMENT: Final = "measurement" +STATE_CLASS_TOTAL: Final = "total" +STATE_CLASS_TOTAL_INCREASING: Final = "total_increasing" +STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass] + +# Note: this needs to be aligned with frontend: OVERRIDE_SENSOR_UNITS in +# `entity-registry-settings.ts` +UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = { + SensorDeviceClass.ATMOSPHERIC_PRESSURE: PressureConverter, + SensorDeviceClass.CURRENT: ElectricCurrentConverter, + SensorDeviceClass.DATA_RATE: DataRateConverter, + SensorDeviceClass.DATA_SIZE: InformationConverter, + SensorDeviceClass.DISTANCE: DistanceConverter, + SensorDeviceClass.ENERGY: EnergyConverter, + SensorDeviceClass.GAS: VolumeConverter, + SensorDeviceClass.POWER_FACTOR: UnitlessRatioConverter, + SensorDeviceClass.PRECIPITATION: DistanceConverter, + SensorDeviceClass.PRECIPITATION_INTENSITY: SpeedConverter, + SensorDeviceClass.PRESSURE: PressureConverter, + SensorDeviceClass.SPEED: SpeedConverter, + SensorDeviceClass.TEMPERATURE: TemperatureConverter, + SensorDeviceClass.VOLTAGE: ElectricPotentialConverter, + SensorDeviceClass.VOLUME: VolumeConverter, + SensorDeviceClass.WATER: VolumeConverter, + SensorDeviceClass.WEIGHT: MassConverter, + SensorDeviceClass.WIND_SPEED: SpeedConverter, +} + +DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { + SensorDeviceClass.APPARENT_POWER: set(UnitOfApparentPower), + SensorDeviceClass.AQI: {None}, + SensorDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure), + SensorDeviceClass.BATTERY: {PERCENTAGE}, + SensorDeviceClass.CO: {CONCENTRATION_PARTS_PER_MILLION}, + SensorDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION}, + SensorDeviceClass.CURRENT: set(UnitOfElectricCurrent), + SensorDeviceClass.DATA_RATE: set(UnitOfDataRate), + SensorDeviceClass.DATA_SIZE: set(UnitOfInformation), + SensorDeviceClass.DISTANCE: set(UnitOfLength), + SensorDeviceClass.DURATION: { + UnitOfTime.DAYS, + UnitOfTime.HOURS, + UnitOfTime.MINUTES, + UnitOfTime.SECONDS, + }, + SensorDeviceClass.ENERGY: set(UnitOfEnergy), + SensorDeviceClass.FREQUENCY: set(UnitOfFrequency), + SensorDeviceClass.GAS: { + UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, + UnitOfVolume.CUBIC_METERS, + }, + SensorDeviceClass.HUMIDITY: {PERCENTAGE}, + SensorDeviceClass.ILLUMINANCE: {LIGHT_LUX}, + SensorDeviceClass.IRRADIANCE: set(UnitOfIrradiance), + SensorDeviceClass.MOISTURE: {PERCENTAGE}, + SensorDeviceClass.NITROGEN_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + SensorDeviceClass.NITROGEN_MONOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + SensorDeviceClass.NITROUS_OXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + SensorDeviceClass.OZONE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + SensorDeviceClass.PM1: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + SensorDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + SensorDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + SensorDeviceClass.POWER_FACTOR: {PERCENTAGE, None}, + SensorDeviceClass.POWER: {UnitOfPower.WATT, UnitOfPower.KILO_WATT}, + SensorDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth), + SensorDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux), + SensorDeviceClass.PRESSURE: set(UnitOfPressure), + SensorDeviceClass.REACTIVE_POWER: {POWER_VOLT_AMPERE_REACTIVE}, + SensorDeviceClass.SIGNAL_STRENGTH: { + SIGNAL_STRENGTH_DECIBELS, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + }, + SensorDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure), + SensorDeviceClass.SPEED: set(UnitOfSpeed).union(set(UnitOfVolumetricFlux)), + SensorDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}, + SensorDeviceClass.TEMPERATURE: set(UnitOfTemperature), + SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: { + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + }, + SensorDeviceClass.VOLTAGE: set(UnitOfElectricPotential), + SensorDeviceClass.VOLUME: set(UnitOfVolume), + SensorDeviceClass.WATER: { + UnitOfVolume.CENTUM_CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, + UnitOfVolume.CUBIC_METERS, + UnitOfVolume.GALLONS, + UnitOfVolume.LITERS, + }, + SensorDeviceClass.WEIGHT: set(UnitOfMass), + SensorDeviceClass.WIND_SPEED: set(UnitOfSpeed), +} + +DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass | None]] = { + SensorDeviceClass.APPARENT_POWER: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.AQI: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.ATMOSPHERIC_PRESSURE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.BATTERY: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.CO: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.CO2: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.CURRENT: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.DATA_RATE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.DATA_SIZE: set(SensorStateClass), + SensorDeviceClass.DATE: set(), + SensorDeviceClass.DISTANCE: set(SensorStateClass), + SensorDeviceClass.DURATION: set(SensorStateClass), + SensorDeviceClass.ENERGY: { + SensorStateClass.TOTAL, + SensorStateClass.TOTAL_INCREASING, + }, + SensorDeviceClass.ENUM: set(), + SensorDeviceClass.FREQUENCY: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.GAS: {SensorStateClass.TOTAL, SensorStateClass.TOTAL_INCREASING}, + SensorDeviceClass.HUMIDITY: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.ILLUMINANCE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.IRRADIANCE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.MOISTURE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.MONETARY: {SensorStateClass.TOTAL}, + SensorDeviceClass.NITROGEN_DIOXIDE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.NITROGEN_MONOXIDE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.NITROUS_OXIDE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.OZONE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.PM1: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.PM10: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.PM25: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.POWER_FACTOR: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.POWER: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.PRECIPITATION: set(SensorStateClass), + SensorDeviceClass.PRECIPITATION_INTENSITY: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.PRESSURE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.REACTIVE_POWER: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.SIGNAL_STRENGTH: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.SOUND_PRESSURE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.SPEED: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.SULPHUR_DIOXIDE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.TEMPERATURE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.TIMESTAMP: set(), + SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.VOLTAGE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.VOLUME: { + SensorStateClass.TOTAL, + SensorStateClass.TOTAL_INCREASING, + }, + SensorDeviceClass.WATER: { + SensorStateClass.TOTAL, + SensorStateClass.TOTAL_INCREASING, + }, + SensorDeviceClass.WEIGHT: {SensorStateClass.TOTAL}, + SensorDeviceClass.WIND_SPEED: {SensorStateClass.MEASUREMENT}, +} diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 412ea3d4d5d..656e9fb00f0 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -183,10 +183,7 @@ def _normalize_states( # We have seen this sensor before, use the unit from metadata statistics_unit = old_metadata["unit_of_measurement"] - if ( - not statistics_unit - or statistics_unit not in statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER - ): + if statistics_unit not in statistics.STATISTIC_UNIT_TO_UNIT_CONVERTER: # The unit used by this sensor doesn't support unit conversion all_units = _get_units(fstates) @@ -507,9 +504,19 @@ def _compile_statistics( # noqa: C901 # Make calculations stat: StatisticData = {"start": start} if "max" in wanted_statistics[entity_id]: - stat["max"] = max(*itertools.islice(zip(*fstates), 1)) # type: ignore[typeddict-item] + stat["max"] = max( + *itertools.islice( + zip(*fstates), # type: ignore[typeddict-item] + 1, + ) + ) if "min" in wanted_statistics[entity_id]: - stat["min"] = min(*itertools.islice(zip(*fstates), 1)) # type: ignore[typeddict-item] + stat["min"] = min( + *itertools.islice( + zip(*fstates), # type: ignore[typeddict-item] + 1, + ) + ) if "mean" in wanted_statistics[entity_id]: stat["mean"] = _time_weighted_average(fstates, start, end) @@ -519,7 +526,8 @@ def _compile_statistics( # noqa: C901 new_state = old_state = None _sum = 0.0 if entity_id in last_stats: - # We have compiled history for this sensor before, use that as a starting point + # We have compiled history for this sensor before, + # use that as a starting point. last_reset = old_last_reset = last_stats[entity_id][0]["last_reset"] if old_last_reset is not None: last_reset = old_last_reset = old_last_reset.isoformat() @@ -710,7 +718,8 @@ def validate_statistics( ) elif state_unit not in converter.VALID_UNITS: # The state unit can't be converted to the unit in metadata - valid_units = ", ".join(sorted(converter.VALID_UNITS)) + valid_units = (unit or "" for unit in converter.VALID_UNITS) + valid_units_str = ", ".join(sorted(valid_units)) validation_result[entity_id].append( statistics.ValidationIssue( "units_changed", @@ -718,7 +727,7 @@ def validate_statistics( "statistic_id": entity_id, "state_unit": state_unit, "metadata_unit": metadata_unit, - "supported_unit": valid_units, + "supported_unit": valid_units_str, }, ) ) diff --git a/homeassistant/components/sensor/translations/ca.json b/homeassistant/components/sensor/translations/ca.json index 8240871d6b3..fbf6f72a837 100644 --- a/homeassistant/components/sensor/translations/ca.json +++ b/homeassistant/components/sensor/translations/ca.json @@ -48,7 +48,6 @@ "carbon_monoxide": "Canvia la concentraci\u00f3 de mon\u00f2xid de carboni de {entity_name}", "current": "Canvia la intensitat de {entity_name}", "data_rate": "Canvia la taxa de dades de {entity_name}", - "data_size": "Canvia la mida de dades de {entity_name}", "distance": "Canvia la dist\u00e0ncia de {entity_name}", "energy": "Canvia l'energia de {entity_name}", "frequency": "Canvia la freq\u00fc\u00e8ncia de {entity_name}", diff --git a/homeassistant/components/sensor/translations/de.json b/homeassistant/components/sensor/translations/de.json index 2e2065570c1..c7edb76dbc1 100644 --- a/homeassistant/components/sensor/translations/de.json +++ b/homeassistant/components/sensor/translations/de.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} Kohlenstoffmonoxid-Konzentrations\u00e4nderung", "current": "{entity_name} Stromver\u00e4nderung", "data_rate": "\u00c4nderung der Datenrate von {entity_name}", - "data_size": "\u00c4nderung der Datengr\u00f6\u00dfe von {entity_name}", "distance": "Abstand zu {entity_name} \u00e4ndert sich", "energy": "{entity_name} Energie\u00e4nderungen", "frequency": "{entity_name} Frequenz\u00e4nderungen", diff --git a/homeassistant/components/sensor/translations/el.json b/homeassistant/components/sensor/translations/el.json index cf4e854f3fb..d9b95712a61 100644 --- a/homeassistant/components/sensor/translations/el.json +++ b/homeassistant/components/sensor/translations/el.json @@ -48,7 +48,6 @@ "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", "data_rate": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ad\u03c2 \u03c1\u03c5\u03b8\u03bc\u03bf\u03cd \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd {entity_name}", - "data_size": "\u0391\u03bb\u03bb\u03ac\u03b6\u03b5\u03b9 \u03c4\u03bf \u03bc\u03ad\u03b3\u03b5\u03b8\u03bf\u03c2 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03c9\u03bd {entity_name}", "distance": "\u0391\u03bb\u03bb\u03b1\u03b3\u03ae \u03b1\u03c0\u03cc\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 {entity_name}", "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}", diff --git a/homeassistant/components/sensor/translations/en.json b/homeassistant/components/sensor/translations/en.json index 18ca324a557..ab43a16769a 100644 --- a/homeassistant/components/sensor/translations/en.json +++ b/homeassistant/components/sensor/translations/en.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} carbon monoxide concentration changes", "current": "{entity_name} current changes", "data_rate": "{entity_name} data rate changes", - "data_size": "{entity_name} data size changes", "distance": "{entity_name} distance changes", "energy": "{entity_name} energy changes", "frequency": "{entity_name} frequency changes", diff --git a/homeassistant/components/sensor/translations/es.json b/homeassistant/components/sensor/translations/es.json index a7cfb9bdfa9..b4e36a9720b 100644 --- a/homeassistant/components/sensor/translations/es.json +++ b/homeassistant/components/sensor/translations/es.json @@ -48,7 +48,6 @@ "carbon_monoxide": "La concentraci\u00f3n de mon\u00f3xido de carbono de {entity_name} cambia", "current": "La intensidad de corriente de {entity_name} cambia", "data_rate": "La velocidad de datos de {entity_name} cambia", - "data_size": "El tama\u00f1o de los datos de {entity_name} cambia", "distance": "La distancia de {entity_name} cambia", "energy": "La energ\u00eda de {entity_name} cambia", "frequency": "La frecuencia de {entity_name} cambia", diff --git a/homeassistant/components/sensor/translations/et.json b/homeassistant/components/sensor/translations/et.json index 497fb3ee613..4ee4969ae0b 100644 --- a/homeassistant/components/sensor/translations/et.json +++ b/homeassistant/components/sensor/translations/et.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} vingugaasi tase muutus", "current": "{entity_name} voolutugevus muutub", "data_rate": "{entity_name} andmeedastuskiirus muutub", - "data_size": "{entity_name} andmemahu muutused", "distance": "{entity_name} kaugus muutub", "energy": "{entity_name} v\u00f5imsus muutub", "frequency": "{entity_name} sagedus muutub", diff --git a/homeassistant/components/sensor/translations/he.json b/homeassistant/components/sensor/translations/he.json index a5e6f6b1d9a..1d3f0acadfb 100644 --- a/homeassistant/components/sensor/translations/he.json +++ b/homeassistant/components/sensor/translations/he.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e8\u05d9\u05db\u05d5\u05d6 \u05d7\u05d3 \u05ea\u05d7\u05de\u05d5\u05e6\u05ea \u05d4\u05e4\u05d7\u05de\u05df", "current": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05e0\u05d5\u05db\u05d7\u05d9\u05d9\u05dd", "data_rate": "\u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05e7\u05e6\u05d1 \u05d4\u05e0\u05ea\u05d5\u05e0\u05d9\u05dd {entity_name}", - "data_size": "\u05e9\u05d9\u05e0\u05d5\u05d9\u05d9\u05dd \u05d1\u05d2\u05d5\u05d3\u05dc \u05d4\u05e0\u05ea\u05d5\u05e0\u05d9\u05dd {entity_name}", "distance": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05de\u05e8\u05d7\u05e7", "energy": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05d0\u05e0\u05e8\u05d2\u05d9\u05d4", "frequency": "{entity_name} \u05e9\u05d9\u05e0\u05d5\u05d9\u05d9 \u05ea\u05d3\u05e8\u05d9\u05dd", diff --git a/homeassistant/components/sensor/translations/hu.json b/homeassistant/components/sensor/translations/hu.json index c39e3708d35..a052f7884f5 100644 --- a/homeassistant/components/sensor/translations/hu.json +++ b/homeassistant/components/sensor/translations/hu.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} sz\u00e9n-monoxid koncentr\u00e1ci\u00f3ja megv\u00e1ltozik", "current": "{entity_name} aktu\u00e1lis v\u00e1ltoz\u00e1sai", "data_rate": "{entity_name} adat\u00e1tviteli sebess\u00e9g v\u00e1ltoz\u00e1sa", - "data_size": "{entity_name} adatmennyis\u00e9g v\u00e1ltoz\u00e1s", "distance": "{entity_name} t\u00e1vols\u00e1g v\u00e1ltoz\u00e1s", "energy": "{entity_name} energiav\u00e1ltoz\u00e1sa", "frequency": "{entity_name} gyakoris\u00e1gi v\u00e1ltoz\u00e1sok", diff --git a/homeassistant/components/sensor/translations/id.json b/homeassistant/components/sensor/translations/id.json index c231ce3b331..22f1685515f 100644 --- a/homeassistant/components/sensor/translations/id.json +++ b/homeassistant/components/sensor/translations/id.json @@ -48,7 +48,6 @@ "carbon_monoxide": "Perubahan konsentrasi karbonmonoksida {entity_name}", "current": "Perubahan arus {entity_name}", "data_rate": "Perubahan laju data {entity_name}", - "data_size": "Perubahan ukuran data {entity_name}", "distance": "Perubahan jarak {entity_name}", "energy": "Perubahan energi {entity_name}", "frequency": "Perubahan frekuensi {entity_name}", diff --git a/homeassistant/components/sensor/translations/it.json b/homeassistant/components/sensor/translations/it.json index 39ac47650b5..d91b4270b3d 100644 --- a/homeassistant/components/sensor/translations/it.json +++ b/homeassistant/components/sensor/translations/it.json @@ -48,7 +48,6 @@ "carbon_monoxide": "Variazioni nella concentrazione di monossido di carbonio di {entity_name}", "current": "Variazioni di corrente di {entity_name}", "data_rate": "{entity_name} modifiche alla velocit\u00e0 dei dati", - "data_size": "{entity_name} cambia la dimensione dei dati", "distance": "Variazioni di distanza di {entity_name}", "energy": "Variazioni di energia di {entity_name}", "frequency": "{entity_name} cambiamenti di frequenza", diff --git a/homeassistant/components/sensor/translations/lt.json b/homeassistant/components/sensor/translations/lt.json index f9197c923ba..f87e64b9bca 100644 --- a/homeassistant/components/sensor/translations/lt.json +++ b/homeassistant/components/sensor/translations/lt.json @@ -2,6 +2,9 @@ "device_automation": { "condition_type": { "is_illuminance": "Dabartinis {entity_name} ap\u0161vietimas" + }, + "trigger_type": { + "illuminance": "{entity_name} ap\u0161vietimas pakito" } }, "state": { diff --git a/homeassistant/components/sensor/translations/nl.json b/homeassistant/components/sensor/translations/nl.json index aaf71635015..1c91f6c379e 100644 --- a/homeassistant/components/sensor/translations/nl.json +++ b/homeassistant/components/sensor/translations/nl.json @@ -6,12 +6,15 @@ "is_carbon_dioxide": "Huidig niveau {entity_name} kooldioxideconcentratie", "is_carbon_monoxide": "Huidig niveau {entity_name} koolmonoxideconcentratie", "is_current": "Huidige {entity_name} stroom", + "is_data_rate": "Huidige {entity_name} datasnelheid", + "is_data_size": "Huidige {entity_name} datagrootte", "is_distance": "Huidig afstand van {entity_name}", "is_energy": "Huidige {entity_name} energie", "is_frequency": "Huidige {entity_name} frequentie", "is_gas": "Huidig {entity_name} gas", "is_humidity": "Huidige {entity_name} vochtigheidsgraad", "is_illuminance": "Huidige {entity_name} verlichtingssterkte", + "is_irradiance": "Huidige {entity_name} instraling", "is_moisture": "Huidige vochtigheid van {entity_name}", "is_nitrogen_dioxide": "Huidige {entity_name} stikstofdioxideconcentratie", "is_nitrogen_monoxide": "Huidige {entity_name} stikstofmonoxideconcentratie", @@ -25,6 +28,7 @@ "is_pressure": "Huidige {entity_name} druk", "is_reactive_power": "Huidig {entity_name} blindvermogen", "is_signal_strength": "Huidige {entity_name} signaalsterkte", + "is_sound_pressure": "Huidig {entity_name} geluidsdruk", "is_speed": "Huidige snelheid van {entity_name}", "is_sulphur_dioxide": "Huidige {entity_name} zwaveldioxideconcentratie", "is_temperature": "Huidige {entity_name} temperatuur", @@ -40,12 +44,14 @@ "carbon_dioxide": "{entity_name} kooldioxideconcentratie gewijzigd", "carbon_monoxide": "{entity_name} koolmonoxideconcentratie gewijzigd", "current": "{entity_name} huidige wijzigingen", + "data_rate": "{entity_name} datasnelheid wijzigingen", "distance": "Afstand van {entity_name} veranderd", "energy": "{entity_name} energieveranderingen", "frequency": "{entity_name} frequentie verandert", "gas": "{entity_name} gas verandert", "humidity": "{entity_name} vochtigheidsgraad gewijzigd", "illuminance": "{entity_name} verlichtingssterkte gewijzigd", + "irradiance": "{entity_name} instalingswijzigingen", "moisture": "Vochtigheid van {entity_name} veranderd", "nitrogen_dioxide": "{entity_name} stikstofdioxideconcentratieverandering", "nitrogen_monoxide": "{entity_name} stikstofmonoxideconcentratieverandering", @@ -59,6 +65,7 @@ "pressure": "{entity_name} druk gewijzigd", "reactive_power": "{entity_name} blindvermogen veranderingen", "signal_strength": "{entity_name} signaalsterkte gewijzigd", + "sound_pressure": "{entity_name} geluidsdrukveranderingen", "speed": "Snelheid van {entity_name} veranderd", "sulphur_dioxide": "{entity_name} zwaveldioxideconcentratieveranderingen", "temperature": "{entity_name} temperatuur gewijzigd", diff --git a/homeassistant/components/sensor/translations/no.json b/homeassistant/components/sensor/translations/no.json index ca1cc72e86a..dcd64fdca0c 100644 --- a/homeassistant/components/sensor/translations/no.json +++ b/homeassistant/components/sensor/translations/no.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} endringer i konsentrasjonen av karbonmonoksid", "current": "{entity_name} gjeldende endringer", "data_rate": "Datahastighetendringer {entity_name}", - "data_size": "Datast\u00f8rrelsen {entity_name} endres", "distance": "{entity_name} avstandsendringer", "energy": "{entity_name} effektendringer", "frequency": "{entity_name} frekvensendringer", diff --git a/homeassistant/components/sensor/translations/pl.json b/homeassistant/components/sensor/translations/pl.json index d0ccf56410d..cd304d8ebae 100644 --- a/homeassistant/components/sensor/translations/pl.json +++ b/homeassistant/components/sensor/translations/pl.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} wykryje zmian\u0119 st\u0119\u017cenia tlenku w\u0119gla", "current": "zmieni si\u0119 nat\u0119\u017cenie pr\u0105du w {entity_name}", "data_rate": "zmieni si\u0119 rozmiar danych {entity_name}", - "data_size": "zmieni si\u0119 rozmiar danych {entity_name}", "distance": "zmieni si\u0119 odleg\u0142o\u015b\u0107 {entity_name}", "energy": "zmieni si\u0119 energia {entity_name}", "frequency": "zmieni si\u0119 cz\u0119stotliwo\u015b\u0107 w {entity_name}", diff --git a/homeassistant/components/sensor/translations/pt-BR.json b/homeassistant/components/sensor/translations/pt-BR.json index 50fa4194d64..d17cb2da225 100644 --- a/homeassistant/components/sensor/translations/pt-BR.json +++ b/homeassistant/components/sensor/translations/pt-BR.json @@ -48,7 +48,6 @@ "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}", "data_rate": "Altera\u00e7\u00f5es na taxa de dados de {entity_name}", - "data_size": "{entity_name} altera\u00e7\u00f5es no tamanho dos dados", "distance": "Mudan\u00e7as da dist\u00e2ncia de {entity_name}", "energy": "Mudan\u00e7as na energia de {entity_name}", "frequency": "Altera\u00e7\u00f5es de frequ\u00eancia de {entity_name}", diff --git a/homeassistant/components/sensor/translations/ru.json b/homeassistant/components/sensor/translations/ru.json index e7833d31f4a..d703aae12fc 100644 --- a/homeassistant/components/sensor/translations/ru.json +++ b/homeassistant/components/sensor/translations/ru.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "current": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0441\u0438\u043b\u044b \u0442\u043e\u043a\u0430", "data_rate": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", - "data_size": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", "distance": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0440\u0430\u0441\u0441\u0442\u043e\u044f\u043d\u0438\u0435", "energy": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", "frequency": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", diff --git a/homeassistant/components/sensor/translations/sk.json b/homeassistant/components/sensor/translations/sk.json index 6eb25658a88..2f85bd9169d 100644 --- a/homeassistant/components/sensor/translations/sk.json +++ b/homeassistant/components/sensor/translations/sk.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} sa men\u00ed koncentr\u00e1cia oxidu uho\u013enat\u00e9ho", "current": "{entity_name} pr\u00fad sa zmen\u00ed", "data_rate": "Pri zmene r\u00fdchlosti prenosu d\u00e1t {entity_name}", - "data_size": "Pri zmene ve\u013ekosti \u00fadajov {entity_name}", "distance": "Pri zmene vzdialenosti {entity_name}", "energy": "Pri zmene energie {entity_name}", "frequency": "Pri zmene frekvencie {entity_name}", diff --git a/homeassistant/components/sensor/translations/tr.json b/homeassistant/components/sensor/translations/tr.json index 33b43342c81..40bf8b596f0 100644 --- a/homeassistant/components/sensor/translations/tr.json +++ b/homeassistant/components/sensor/translations/tr.json @@ -2,16 +2,20 @@ "device_automation": { "condition_type": { "is_apparent_power": "Mevcut {entity_name} g\u00f6r\u00fcn\u00fcr g\u00fc\u00e7", + "is_atmospheric_pressure": "Mevcut {entity_name} atmosferik bas\u0131n\u00e7", "is_battery_level": "Mevcut {entity_name} pil seviyesi", "is_carbon_dioxide": "Mevcut {entity_name} karbondioksit konsantrasyon seviyesi", "is_carbon_monoxide": "Mevcut {entity_name} karbon monoksit konsantrasyon seviyesi", "is_current": "Mevcut {entity_name} ak\u0131m\u0131", + "is_data_rate": "Ge\u00e7erli {entity_name} veri h\u0131z\u0131", + "is_data_size": "Ge\u00e7erli {entity_name} veri boyutu", "is_distance": "Mevcut {entity_name} mesafesi", "is_energy": "Mevcut {entity_name} enerjisi", "is_frequency": "Ge\u00e7erli {entity_name} frekans\u0131", "is_gas": "Mevcut {entity_name} gaz\u0131", "is_humidity": "Ge\u00e7erli {entity_name} nem oran\u0131", "is_illuminance": "Mevcut {entity_name} ayd\u0131nlatma d\u00fczeyi", + "is_irradiance": "Mevcut {entity_name} \u0131\u015f\u0131n\u0131m\u0131", "is_moisture": "Mevcut {entity_name} nemi", "is_nitrogen_dioxide": "Mevcut {entity_name} nitrojen dioksit konsantrasyon seviyesi", "is_nitrogen_monoxide": "Mevcut {entity_name} nitrojen monoksit konsantrasyon seviyesi", @@ -25,6 +29,7 @@ "is_pressure": "Ge\u00e7erli {entity_name} bas\u0131nc\u0131", "is_reactive_power": "Mevcut {entity_name} reaktif g\u00fc\u00e7", "is_signal_strength": "Mevcut {entity_name} sinyal g\u00fcc\u00fc", + "is_sound_pressure": "Mevcut {entity_name} ses bas\u0131nc\u0131", "is_speed": "Mevcut {entity_name} h\u0131z\u0131", "is_sulphur_dioxide": "Mevcut {entity_name} k\u00fck\u00fcrt dioksit konsantrasyon seviyesi", "is_temperature": "Mevcut {entity_name} s\u0131cakl\u0131\u011f\u0131", @@ -37,16 +42,19 @@ }, "trigger_type": { "apparent_power": "{entity_name} g\u00f6r\u00fcn\u00fcr g\u00fc\u00e7 de\u011fi\u015fiklikleri", + "atmospheric_pressure": "{entity_name} atmosferik bas\u0131n\u00e7 de\u011fi\u015fiklikleri", "battery_level": "{entity_name} pil seviyesi de\u011fi\u015fiklikleri", "carbon_dioxide": "{entity_name} karbondioksit konsantrasyonu de\u011fi\u015fiklikleri", "carbon_monoxide": "{entity_name} karbon monoksit konsantrasyonu de\u011fi\u015fiklikleri", "current": "{entity_name} ak\u0131m de\u011fi\u015fiklikleri", + "data_rate": "{entity_name} veri h\u0131z\u0131 de\u011fi\u015fiklikleri", "distance": "{entity_name} mesafe de\u011fi\u015fiklikleri", "energy": "{entity_name} enerji de\u011fi\u015fiklikleri", "frequency": "{entity_name} frekans de\u011fi\u015fiklikleri", "gas": "{entity_name} gaz de\u011fi\u015fiklikleri", "humidity": "{entity_name} nem de\u011fi\u015fiklikleri", "illuminance": "{entity_name} ayd\u0131nlatma de\u011fi\u015fiklikleri", + "irradiance": "{entity_name} \u0131\u015f\u0131n\u0131m de\u011fi\u015fiklikleri", "moisture": "{entity_name} nem de\u011fi\u015fimleri", "nitrogen_dioxide": "{entity_name} nitrojen dioksit konsantrasyonu de\u011fi\u015fiklikleri", "nitrogen_monoxide": "{entity_name} nitrojen monoksit konsantrasyonu de\u011fi\u015fiklikleri", @@ -60,6 +68,7 @@ "pressure": "{entity_name} bas\u0131n\u00e7 de\u011fi\u015fiklikleri", "reactive_power": "{entity_name} reaktif g\u00fc\u00e7 de\u011fi\u015fiklikleri", "signal_strength": "{entity_name} sinyal g\u00fcc\u00fc de\u011fi\u015fiklikleri", + "sound_pressure": "{entity_name} ses bas\u0131nc\u0131 de\u011fi\u015fiklikleri", "speed": "{entity_name} h\u0131z de\u011fi\u015fiklikleri", "sulphur_dioxide": "{entity_name} k\u00fck\u00fcrt dioksit konsantrasyonu de\u011fi\u015fiklikleri", "temperature": "{entity_name} s\u0131cakl\u0131k de\u011fi\u015fiklikleri", diff --git a/homeassistant/components/sensor/translations/zh-Hant.json b/homeassistant/components/sensor/translations/zh-Hant.json index ecd82e9f2b6..22cfde99137 100644 --- a/homeassistant/components/sensor/translations/zh-Hant.json +++ b/homeassistant/components/sensor/translations/zh-Hant.json @@ -48,7 +48,6 @@ "carbon_monoxide": "{entity_name} \u4e00\u6c27\u5316\u78b3\u6fc3\u5ea6\u8b8a\u5316", "current": "\u76ee\u524d{entity_name}\u96fb\u6d41\u8b8a\u66f4", "data_rate": "{entity_name}\u8cc7\u6599\u50b3\u8f38\u7387\u8b8a\u66f4", - "data_size": "{entity_name} \u8cc7\u6599\u5927\u5c0f\u8b8a\u66f4", "distance": "{entity_name}\u8ddd\u96e2\u8b8a\u66f4", "energy": "\u76ee\u524d{entity_name}\u96fb\u529b\u8b8a\u66f4", "frequency": "{entity_name}\u983b\u7387\u8b8a\u66f4", diff --git a/homeassistant/components/sensor/websocket_api.py b/homeassistant/components/sensor/websocket_api.py new file mode 100644 index 00000000000..10699b8c1c6 --- /dev/null +++ b/homeassistant/components/sensor/websocket_api.py @@ -0,0 +1,35 @@ +"""The sensor websocket API.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.components import websocket_api +from homeassistant.core import HomeAssistant, callback + +from .const import DEVICE_CLASS_UNITS, UNIT_CONVERTERS + + +@callback +def async_setup(hass: HomeAssistant) -> None: + """Set up the sensor websocket API.""" + websocket_api.async_register_command(hass, ws_device_class_units) + + +@callback +@websocket_api.websocket_command( + { + vol.Required("type"): "sensor/device_class_convertible_units", + vol.Required("device_class"): str, + } +) +def ws_device_class_units( + hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any] +) -> None: + """Return supported units for a device class.""" + device_class = msg["device_class"] + convertible_units = set() + if device_class in UNIT_CONVERTERS and device_class in DEVICE_CLASS_UNITS: + convertible_units = DEVICE_CLASS_UNITS[device_class] + connection.send_result(msg["id"], {"units": convertible_units}) diff --git a/homeassistant/components/sensorpro/manifest.json b/homeassistant/components/sensorpro/manifest.json index ba9b8bcf164..07499609133 100644 --- a/homeassistant/components/sensorpro/manifest.json +++ b/homeassistant/components/sensorpro/manifest.json @@ -16,7 +16,7 @@ } ], "requirements": ["sensorpro-ble==0.5.1"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" } diff --git a/homeassistant/components/sensorpro/sensor.py b/homeassistant/components/sensorpro/sensor.py index edfe2fb21c5..36eb3737884 100644 --- a/homeassistant/components/sensorpro/sensor.py +++ b/homeassistant/components/sensorpro/sensor.py @@ -1,8 +1,6 @@ """Support for SensorPro sensors.""" from __future__ import annotations -from typing import Optional, Union - from sensorpro_ble import ( SensorDeviceClass as SensorProSensorDeviceClass, SensorUpdate, @@ -128,9 +126,7 @@ async def async_setup_entry( class SensorProBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a SensorPro sensor.""" diff --git a/homeassistant/components/sensorpro/translations/lv.json b/homeassistant/components/sensorpro/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/sensorpro/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpro/translations/tr.json b/homeassistant/components/sensorpro/translations/tr.json index f0ddbc274c9..36347c44f7f 100644 --- a/homeassistant/components/sensorpro/translations/tr.json +++ b/homeassistant/components/sensorpro/translations/tr.json @@ -9,13 +9,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/sensorpro/translations/uk.json b/homeassistant/components/sensorpro/translations/uk.json new file mode 100644 index 00000000000..e58b49d4c9e --- /dev/null +++ b/homeassistant/components/sensorpro/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/manifest.json b/homeassistant/components/sensorpush/manifest.json index d1a370aa9d7..7046837e45f 100644 --- a/homeassistant/components/sensorpush/manifest.json +++ b/homeassistant/components/sensorpush/manifest.json @@ -10,7 +10,7 @@ } ], "requirements": ["sensorpush-ble==1.5.2"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" } diff --git a/homeassistant/components/sensorpush/sensor.py b/homeassistant/components/sensorpush/sensor.py index 7742642e92c..479acd8ac1e 100644 --- a/homeassistant/components/sensorpush/sensor.py +++ b/homeassistant/components/sensorpush/sensor.py @@ -1,8 +1,6 @@ """Support for sensorpush ble sensors.""" from __future__ import annotations -from typing import Optional, Union - from sensorpush_ble import DeviceClass, DeviceKey, SensorUpdate, Units from homeassistant import config_entries @@ -116,9 +114,7 @@ async def async_setup_entry( class SensorPushBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a sensorpush ble sensor.""" diff --git a/homeassistant/components/sensorpush/translations/lv.json b/homeassistant/components/sensorpush/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/sensorpush/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensorpush/translations/tr.json b/homeassistant/components/sensorpush/translations/tr.json index f63cee3493c..66d94aa9414 100644 --- a/homeassistant/components/sensorpush/translations/tr.json +++ b/homeassistant/components/sensorpush/translations/tr.json @@ -8,13 +8,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index 29490403316..0d9575d19c1 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.12.1"], + "requirements": ["sentry-sdk==1.13.0"], "codeowners": ["@dcramer", "@frenck"], "integration_type": "service", "iot_class": "cloud_polling" diff --git a/homeassistant/components/sentry/translations/el.json b/homeassistant/components/sentry/translations/el.json index d416005a9c2..12b873515e3 100644 --- a/homeassistant/components/sentry/translations/el.json +++ b/homeassistant/components/sentry/translations/el.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "dsn": "DSN" + "dsn": "Sentry DSN" } } } diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index cfe834bd648..34edc40c209 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,7 +2,7 @@ "domain": "seven_segments", "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": ["pillow==9.3.0"], + "requirements": ["pillow==9.4.0"], "codeowners": ["@fabaff"], "iot_class": "local_polling" } diff --git a/homeassistant/components/sfr_box/__init__.py b/homeassistant/components/sfr_box/__init__.py new file mode 100644 index 00000000000..07f122fa4b2 --- /dev/null +++ b/homeassistant/components/sfr_box/__init__.py @@ -0,0 +1,71 @@ +"""SFR Box.""" +from __future__ import annotations + +import asyncio + +from sfrbox_api.bridge import SFRBox +from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.httpx_client import get_async_client + +from .const import DOMAIN, PLATFORMS, PLATFORMS_WITH_AUTH +from .coordinator import SFRDataUpdateCoordinator +from .models import DomainData + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up SFR box as config entry.""" + box = SFRBox(ip=entry.data[CONF_HOST], client=get_async_client(hass)) + platforms = PLATFORMS + if (username := entry.data.get(CONF_USERNAME)) and ( + password := entry.data.get(CONF_PASSWORD) + ): + try: + await box.authenticate(username=username, password=password) + except SFRBoxAuthenticationError as err: + raise ConfigEntryAuthFailed() from err + except SFRBoxError as err: + raise ConfigEntryNotReady() from err + platforms = PLATFORMS_WITH_AUTH + + data = DomainData( + box=box, + dsl=SFRDataUpdateCoordinator(hass, box, "dsl", lambda b: b.dsl_get_info()), + system=SFRDataUpdateCoordinator( + hass, box, "system", lambda b: b.system_get_info() + ), + ) + tasks = [ + data.dsl.async_config_entry_first_refresh(), + data.system.async_config_entry_first_refresh(), + ] + await asyncio.gather(*tasks) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data + + system_info = data.system.data + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, system_info.mac_addr)}, + name="SFR Box", + model=system_info.product_id, + sw_version=system_info.version_mainfirmware, + configuration_url=f"http://{entry.data[CONF_HOST]}", + ) + + await hass.config_entries.async_forward_entry_setups(entry, platforms) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/sfr_box/binary_sensor.py b/homeassistant/components/sfr_box/binary_sensor.py new file mode 100644 index 00000000000..88066df15d1 --- /dev/null +++ b/homeassistant/components/sfr_box/binary_sensor.py @@ -0,0 +1,92 @@ +"""SFR Box sensor platform.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Generic, TypeVar + +from sfrbox_api.models import DslInfo, SystemInfo + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import SFRDataUpdateCoordinator +from .models import DomainData + +_T = TypeVar("_T") + + +@dataclass +class SFRBoxBinarySensorMixin(Generic[_T]): + """Mixin for SFR Box sensors.""" + + value_fn: Callable[[_T], bool | None] + + +@dataclass +class SFRBoxBinarySensorEntityDescription( + BinarySensorEntityDescription, SFRBoxBinarySensorMixin[_T] +): + """Description for SFR Box binary sensors.""" + + +DSL_SENSOR_TYPES: tuple[SFRBoxBinarySensorEntityDescription[DslInfo], ...] = ( + SFRBoxBinarySensorEntityDescription[DslInfo]( + key="status", + name="Status", + device_class=BinarySensorDeviceClass.CONNECTIVITY, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda x: x.status == "up", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the sensors.""" + data: DomainData = hass.data[DOMAIN][entry.entry_id] + + entities = [ + SFRBoxBinarySensor(data.dsl, description, data.system.data) + for description in DSL_SENSOR_TYPES + ] + + async_add_entities(entities) + + +class SFRBoxBinarySensor( + CoordinatorEntity[SFRDataUpdateCoordinator[_T]], BinarySensorEntity +): + """SFR Box sensor.""" + + entity_description: SFRBoxBinarySensorEntityDescription[_T] + _attr_has_entity_name = True + + def __init__( + self, + coordinator: SFRDataUpdateCoordinator[_T], + description: SFRBoxBinarySensorEntityDescription, + system_info: SystemInfo, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = ( + f"{system_info.mac_addr}_{coordinator.name}_{description.key}" + ) + self._attr_device_info = {"identifiers": {(DOMAIN, system_info.mac_addr)}} + + @property + def is_on(self) -> bool | None: + """Return the native value of the device.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/sfr_box/button.py b/homeassistant/components/sfr_box/button.py new file mode 100644 index 00000000000..a6fa9af5385 --- /dev/null +++ b/homeassistant/components/sfr_box/button.py @@ -0,0 +1,108 @@ +"""SFR Box button platform.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable, Coroutine +from dataclasses import dataclass +from functools import wraps +from typing import Any, Concatenate, ParamSpec, TypeVar + +from sfrbox_api.bridge import SFRBox +from sfrbox_api.exceptions import SFRBoxError +from sfrbox_api.models import SystemInfo + +from homeassistant.components.button import ( + ButtonDeviceClass, + ButtonEntity, + ButtonEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .models import DomainData + +_T = TypeVar("_T") +_P = ParamSpec("_P") + + +def with_error_wrapping( + func: Callable[Concatenate[SFRBoxButton, _P], Awaitable[_T]] +) -> Callable[Concatenate[SFRBoxButton, _P], Coroutine[Any, Any, _T]]: + """Catch SFR errors.""" + + @wraps(func) + async def wrapper( + self: SFRBoxButton, + *args: _P.args, + **kwargs: _P.kwargs, + ) -> _T: + """Catch SFRBoxError errors and raise HomeAssistantError.""" + try: + return await func(self, *args, **kwargs) + except SFRBoxError as err: + raise HomeAssistantError(err) from err + + return wrapper + + +@dataclass +class SFRBoxButtonMixin: + """Mixin for SFR Box buttons.""" + + async_press: Callable[[SFRBox], Coroutine[None, None, None]] + + +@dataclass +class SFRBoxButtonEntityDescription(ButtonEntityDescription, SFRBoxButtonMixin): + """Description for SFR Box buttons.""" + + +BUTTON_TYPES: tuple[SFRBoxButtonEntityDescription, ...] = ( + SFRBoxButtonEntityDescription( + async_press=lambda x: x.system_reboot(), + device_class=ButtonDeviceClass.RESTART, + entity_category=EntityCategory.CONFIG, + key="system_reboot", + name="Reboot", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the buttons.""" + data: DomainData = hass.data[DOMAIN][entry.entry_id] + + entities = [ + SFRBoxButton(data.box, description, data.system.data) + for description in BUTTON_TYPES + ] + async_add_entities(entities) + + +class SFRBoxButton(ButtonEntity): + """Mixin for button specific attributes.""" + + entity_description: SFRBoxButtonEntityDescription + _attr_has_entity_name = True + + def __init__( + self, + box: SFRBox, + description: SFRBoxButtonEntityDescription, + system_info: SystemInfo, + ) -> None: + """Initialize the sensor.""" + self.entity_description = description + self._box = box + self._attr_unique_id = f"{system_info.mac_addr}_{description.key}" + self._attr_device_info = {"identifiers": {(DOMAIN, system_info.mac_addr)}} + + @with_error_wrapping + async def async_press(self) -> None: + """Process the button press.""" + await self.entity_description.async_press(self._box) diff --git a/homeassistant/components/sfr_box/config_flow.py b/homeassistant/components/sfr_box/config_flow.py new file mode 100644 index 00000000000..836ed708743 --- /dev/null +++ b/homeassistant/components/sfr_box/config_flow.py @@ -0,0 +1,120 @@ +"""SFR Box config flow.""" +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +from sfrbox_api.bridge import SFRBox +from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry, ConfigFlow +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import selector +from homeassistant.helpers.httpx_client import get_async_client + +from .const import DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST, default=DEFAULT_HOST): selector.TextSelector(), + } +) +AUTH_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): selector.TextSelector(), + vol.Required(CONF_PASSWORD): selector.TextSelector( + selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD) + ), + } +) + + +class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN): + """SFR Box config flow.""" + + VERSION = 1 + _box: SFRBox + _config: dict[str, Any] = {} + _reauth_entry: ConfigEntry | None = None + + async def async_step_user( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors = {} + if user_input is not None: + box = SFRBox(ip=user_input[CONF_HOST], client=get_async_client(self.hass)) + try: + system_info = await box.system_get_info() + except SFRBoxError: + errors["base"] = "cannot_connect" + else: + await self.async_set_unique_id(system_info.mac_addr) + self._abort_if_unique_id_configured() + self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]}) + self._box = box + self._config.update(user_input) + return await self.async_step_choose_auth() + + data_schema = self.add_suggested_values_to_schema(DATA_SCHEMA, user_input) + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors + ) + + async def async_step_choose_auth( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + return self.async_show_menu( + step_id="choose_auth", + menu_options=["auth", "skip_auth"], + ) + + async def async_step_auth( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Check authentication.""" + errors = {} + if user_input is not None: + try: + await self._box.authenticate( + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + ) + except SFRBoxAuthenticationError: + errors["base"] = "invalid_auth" + else: + if reauth_entry := self._reauth_entry: + data = {**reauth_entry.data, **user_input} + self.hass.config_entries.async_update_entry(reauth_entry, data=data) + self.hass.async_create_task( + self.hass.config_entries.async_reload(reauth_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + self._config.update(user_input) + return self.async_create_entry(title="SFR Box", data=self._config) + + suggested_values: Mapping[str, Any] | None = user_input + if self._reauth_entry and not suggested_values: + suggested_values = self._reauth_entry.data + + data_schema = self.add_suggested_values_to_schema(AUTH_SCHEMA, suggested_values) + return self.async_show_form( + step_id="auth", data_schema=data_schema, errors=errors + ) + + async def async_step_skip_auth( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Skip authentication.""" + return self.async_create_entry(title="SFR Box", data=self._config) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle failed credentials.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + self._box = SFRBox(ip=entry_data[CONF_HOST], client=get_async_client(self.hass)) + return await self.async_step_auth() diff --git a/homeassistant/components/sfr_box/const.py b/homeassistant/components/sfr_box/const.py new file mode 100644 index 00000000000..3700890b957 --- /dev/null +++ b/homeassistant/components/sfr_box/const.py @@ -0,0 +1,10 @@ +"""SFR Box constants.""" +from homeassistant.const import Platform + +DEFAULT_HOST = "192.168.0.1" +DEFAULT_USERNAME = "admin" + +DOMAIN = "sfr_box" + +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] +PLATFORMS_WITH_AUTH = [*PLATFORMS, Platform.BUTTON] diff --git a/homeassistant/components/sfr_box/coordinator.py b/homeassistant/components/sfr_box/coordinator.py new file mode 100644 index 00000000000..739fc2a770b --- /dev/null +++ b/homeassistant/components/sfr_box/coordinator.py @@ -0,0 +1,39 @@ +"""SFR Box coordinator.""" +from collections.abc import Callable, Coroutine +from datetime import timedelta +import logging +from typing import Any, TypeVar + +from sfrbox_api.bridge import SFRBox +from sfrbox_api.exceptions import SFRBoxError + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +_LOGGER = logging.getLogger(__name__) +_SCAN_INTERVAL = timedelta(minutes=1) + +_T = TypeVar("_T") + + +class SFRDataUpdateCoordinator(DataUpdateCoordinator[_T]): + """Coordinator to manage data updates.""" + + def __init__( + self, + hass: HomeAssistant, + box: SFRBox, + name: str, + method: Callable[[SFRBox], Coroutine[Any, Any, _T]], + ) -> None: + """Initialize coordinator.""" + self.box = box + self._method = method + super().__init__(hass, _LOGGER, name=name, update_interval=_SCAN_INTERVAL) + + async def _async_update_data(self) -> _T: + """Update data.""" + try: + return await self._method(self.box) + except SFRBoxError as err: + raise UpdateFailed() from err diff --git a/homeassistant/components/sfr_box/diagnostics.py b/homeassistant/components/sfr_box/diagnostics.py new file mode 100644 index 00000000000..6a7ceb0e86b --- /dev/null +++ b/homeassistant/components/sfr_box/diagnostics.py @@ -0,0 +1,34 @@ +"""SFR Box diagnostics platform.""" +from __future__ import annotations + +import dataclasses +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 DomainData + +TO_REDACT = {"mac_addr", "serial_number"} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + data: DomainData = hass.data[DOMAIN][entry.entry_id] + + return { + "entry": { + "title": entry.title, + "data": dict(entry.data), + }, + "data": { + "dsl": async_redact_data(dataclasses.asdict(data.dsl.data), TO_REDACT), + "system": async_redact_data( + dataclasses.asdict(data.system.data), TO_REDACT + ), + }, + } diff --git a/homeassistant/components/sfr_box/manifest.json b/homeassistant/components/sfr_box/manifest.json new file mode 100644 index 00000000000..78901006d9a --- /dev/null +++ b/homeassistant/components/sfr_box/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "sfr_box", + "name": "SFR Box", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/sfr_box", + "requirements": ["sfrbox-api==0.0.5"], + "codeowners": ["@epenet"], + "iot_class": "local_polling", + "integration_type": "device" +} diff --git a/homeassistant/components/sfr_box/models.py b/homeassistant/components/sfr_box/models.py new file mode 100644 index 00000000000..e2f86aeb924 --- /dev/null +++ b/homeassistant/components/sfr_box/models.py @@ -0,0 +1,16 @@ +"""SFR Box models.""" +from dataclasses import dataclass + +from sfrbox_api.bridge import SFRBox +from sfrbox_api.models import DslInfo, SystemInfo + +from .coordinator import SFRDataUpdateCoordinator + + +@dataclass +class DomainData: + """Domain data for SFR Box.""" + + box: SFRBox + dsl: SFRDataUpdateCoordinator[DslInfo] + system: SFRDataUpdateCoordinator[SystemInfo] diff --git a/homeassistant/components/sfr_box/sensor.py b/homeassistant/components/sfr_box/sensor.py new file mode 100644 index 00000000000..58844b27610 --- /dev/null +++ b/homeassistant/components/sfr_box/sensor.py @@ -0,0 +1,244 @@ +"""SFR Box sensor platform.""" +from collections.abc import Callable, Iterable +from dataclasses import dataclass +from itertools import chain +from typing import Generic, TypeVar + +from sfrbox_api.models import DslInfo, SystemInfo + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + SIGNAL_STRENGTH_DECIBELS, + UnitOfDataRate, + UnitOfElectricPotential, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import SFRDataUpdateCoordinator +from .models import DomainData + +_T = TypeVar("_T") + + +@dataclass +class SFRBoxSensorMixin(Generic[_T]): + """Mixin for SFR Box sensors.""" + + value_fn: Callable[[_T], StateType] + + +@dataclass +class SFRBoxSensorEntityDescription(SensorEntityDescription, SFRBoxSensorMixin[_T]): + """Description for SFR Box sensors.""" + + +DSL_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[DslInfo], ...] = ( + SFRBoxSensorEntityDescription[DslInfo]( + key="linemode", + name="Line mode", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda x: x.linemode, + ), + SFRBoxSensorEntityDescription[DslInfo]( + key="counter", + name="Counter", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda x: x.counter, + ), + SFRBoxSensorEntityDescription[DslInfo]( + key="crc", + name="CRC", + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + value_fn=lambda x: x.crc, + ), + SFRBoxSensorEntityDescription[DslInfo]( + key="noise_down", + name="Noise down", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda x: x.noise_down, + ), + SFRBoxSensorEntityDescription[DslInfo]( + key="noise_up", + name="Noise up", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda x: x.noise_up, + ), + SFRBoxSensorEntityDescription[DslInfo]( + key="attenuation_down", + name="Attenuation down", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda x: x.attenuation_down, + ), + SFRBoxSensorEntityDescription[DslInfo]( + key="attenuation_up", + name="Attenuation up", + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda x: x.attenuation_up, + ), + SFRBoxSensorEntityDescription[DslInfo]( + key="rate_down", + name="Rate down", + device_class=SensorDeviceClass.DATA_RATE, + native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda x: x.rate_down, + ), + SFRBoxSensorEntityDescription[DslInfo]( + key="rate_up", + name="Rate up", + device_class=SensorDeviceClass.DATA_RATE, + native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda x: x.rate_up, + ), + SFRBoxSensorEntityDescription[DslInfo]( + key="line_status", + name="Line status", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + options=[ + "no_defect", + "of_frame", + "loss_of_signal", + "loss_of_power", + "loss_of_signal_quality", + "unknown", + ], + translation_key="line_status", + value_fn=lambda x: x.line_status.lower().replace(" ", "_"), + ), + SFRBoxSensorEntityDescription[DslInfo]( + key="training", + name="Training", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + options=[ + "idle", + "g_994_training", + "g_992_started", + "g_922_channel_analysis", + "g_992_message_exchange", + "g_993_started", + "g_993_channel_analysis", + "g_993_message_exchange", + "showtime", + "unknown", + ], + translation_key="training", + value_fn=lambda x: x.training.lower().replace(" ", "_").replace(".", "_"), + ), +) +SYSTEM_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[SystemInfo], ...] = ( + SFRBoxSensorEntityDescription[SystemInfo]( + key="net_infra", + name="Network infrastructure", + device_class=SensorDeviceClass.ENUM, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + options=[ + "adsl", + "ftth", + "gprs", + "unknown", + ], + translation_key="net_infra", + value_fn=lambda x: x.net_infra, + ), + SFRBoxSensorEntityDescription[SystemInfo]( + key="alimvoltage", + name="Voltage", + device_class=SensorDeviceClass.VOLTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, + value_fn=lambda x: x.alimvoltage, + ), + SFRBoxSensorEntityDescription[SystemInfo]( + key="temperature", + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + entity_registry_enabled_default=False, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_fn=lambda x: x.temperature / 1000, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the sensors.""" + data: DomainData = hass.data[DOMAIN][entry.entry_id] + + entities: Iterable[SFRBoxSensor] = chain( + ( + SFRBoxSensor(data.dsl, description, data.system.data) + for description in DSL_SENSOR_TYPES + ), + ( + SFRBoxSensor(data.system, description, data.system.data) + for description in SYSTEM_SENSOR_TYPES + ), + ) + + async_add_entities(entities) + + +class SFRBoxSensor(CoordinatorEntity[SFRDataUpdateCoordinator[_T]], SensorEntity): + """SFR Box sensor.""" + + entity_description: SFRBoxSensorEntityDescription[_T] + _attr_has_entity_name = True + + def __init__( + self, + coordinator: SFRDataUpdateCoordinator[_T], + description: SFRBoxSensorEntityDescription, + system_info: SystemInfo, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = ( + f"{system_info.mac_addr}_{coordinator.name}_{description.key}" + ) + self._attr_device_info = {"identifiers": {(DOMAIN, system_info.mac_addr)}} + + @property + def native_value(self) -> StateType: + """Return the native value of the device.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/sfr_box/strings.json b/homeassistant/components/sfr_box/strings.json new file mode 100644 index 00000000000..ddff342a10d --- /dev/null +++ b/homeassistant/components/sfr_box/strings.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "step": { + "auth": { + "data": { + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + } + }, + "choose_auth": { + "description": "Setting credentials enables additional functionality.", + "menu_options": { + "auth": "Set credentials (recommended)", + "skip_auth": "Skip authentication" + } + }, + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + }, + "description": "Setting the credentials is optional, but enables additional functionality." + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "no_defect": "No Defect", + "of_frame": "Of Frame", + "loss_of_signal": "Loss Of Signal", + "loss_of_power": "Loss Of Power", + "loss_of_signal_quality": "Loss Of Signal Quality", + "unknown": "Unknown" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Unknown" + } + }, + "training": { + "state": { + "idle": "Idle", + "g_994_training": "G.994 Training", + "g_992_started": "G.992 Started", + "g_922_channel_analysis": "G.922 Channel Analysis", + "g_992_message_exchange": "G.992 Message Exchange", + "g_993_started": "G.993 Started", + "g_993_channel_analysis": "G.993 Channel Analysis", + "g_993_message_exchange": "G.993 Message Exchange", + "showtime": "Showtime", + "unknown": "Unknown" + } + } + } + } +} diff --git a/homeassistant/components/sfr_box/translations/bg.json b/homeassistant/components/sfr_box/translations/bg.json new file mode 100644 index 00000000000..cc038c14696 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/bg.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", + "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f" + }, + "step": { + "auth": { + "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" + } + }, + "choose_auth": { + "menu_options": { + "skip_auth": "\u041f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f\u0442\u0430" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + }, + "entity": { + "sensor": { + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/ca.json b/homeassistant/components/sfr_box/translations/ca.json new file mode 100644 index 00000000000..21d4864fb56 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/ca.json @@ -0,0 +1,68 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "auth": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + }, + "choose_auth": { + "description": "La configuraci\u00f3 de credencials permet funcionalitats addicionals.", + "menu_options": { + "auth": "Estableix les credencials (recomanat)", + "skip_auth": "Omet l'autenticaci\u00f3" + } + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "La configuraci\u00f3 de les credencials \u00e9s opcional, per\u00f2 permet funcionalitats addicionals." + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "P\u00e8rdua de pot\u00e8ncia", + "loss_of_signal": "P\u00e8rdua de senyal", + "loss_of_signal_quality": "P\u00e8rdua de qualitat de senyal", + "no_defect": "Sense defectes", + "unknown": "Desconegut" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Desconegut" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "An\u00e0lisi de canal G.922", + "g_992_message_exchange": "Intercanvi de missatge G.992", + "g_992_started": "G.992 s'ha iniciat", + "g_993_channel_analysis": "An\u00e0lisi de canal G.993", + "g_993_message_exchange": "Intercanvi de missatge G.993", + "g_993_started": "G.993 s'ha iniciat", + "g_994_training": "Entrenant G.994", + "idle": "Inactiu", + "showtime": "Hora de l'espectacle", + "unknown": "Desconegut" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/cs.json b/homeassistant/components/sfr_box/translations/cs.json new file mode 100644 index 00000000000..e2a1175e561 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/cs.json @@ -0,0 +1,16 @@ +{ + "entity": { + "sensor": { + "line_status": { + "state": { + "unknown": "Nezn\u00e1m\u00fd" + } + }, + "training": { + "state": { + "unknown": "Nezn\u00e1m\u00fd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/de.json b/homeassistant/components/sfr_box/translations/de.json new file mode 100644 index 00000000000..a34b6f4ba20 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/de.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "auth": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + }, + "choose_auth": { + "description": "Die Angabe der Anmeldedaten erm\u00f6glicht zus\u00e4tzliche Funktionen.", + "menu_options": { + "auth": "Anmeldedaten festlegen (empfohlen)", + "skip_auth": "Authentifizierung \u00fcberspringen" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Die Angabe der Anmeldedaten ist optional, erm\u00f6glicht aber zus\u00e4tzliche Funktionen." + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "Stromausfall", + "loss_of_signal": "Signalverlust", + "loss_of_signal_quality": "Signalverlust-Qualit\u00e4t", + "no_defect": "Kein Defekt", + "of_frame": "des Frames", + "unknown": "Unbekannt" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Unbekannt" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.922-Kanalanalyse", + "g_992_message_exchange": "G.992-Nachrichtenaustausch", + "g_992_started": "G.992 gestartet", + "g_993_channel_analysis": "G.993-Kanalanalyse", + "g_993_message_exchange": "G.993-Nachrichtenaustausch", + "g_993_started": "G.993 gestartet", + "g_994_training": "G.994-Training", + "idle": "Unt\u00e4tig", + "showtime": "Showtime", + "unknown": "Unbekannt" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/el.json b/homeassistant/components/sfr_box/translations/el.json new file mode 100644 index 00000000000..fd1cb761241 --- /dev/null +++ b/homeassistant/components/sfr_box/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" + }, + "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" + } + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "\u0391\u03c0\u03ce\u03bb\u03b5\u03b9\u03b1 \u0394\u03cd\u03bd\u03b1\u03bc\u03b7\u03c2", + "loss_of_signal": "\u0391\u03c0\u03ce\u03bb\u03b5\u03b9\u03b1 \u03c3\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2", + "loss_of_signal_quality": "\u0391\u03c0\u03ce\u03bb\u03b5\u03b9\u03b1 \u03a0\u03bf\u03b9\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03a3\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2", + "no_defect": "\u039a\u03b1\u03bd\u03ad\u03bd\u03b1 \u03b5\u03bb\u03ac\u03c4\u03c4\u03c9\u03bc\u03b1", + "of_frame": "\u03a4\u03bf\u03c5 \u03a0\u03bb\u03b1\u03b9\u03c3\u03af\u03bf\u03c5", + "unknown": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.922 \u0391\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7 \u03ba\u03b1\u03bd\u03b1\u03bb\u03b9\u03ce\u03bd", + "g_992_message_exchange": "G.992 \u0391\u03bd\u03c4\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03bc\u03b7\u03bd\u03c5\u03bc\u03ac\u03c4\u03c9\u03bd", + "g_992_started": "G.992 \u039e\u03b5\u03ba\u03af\u03bd\u03b7\u03c3\u03b5", + "g_993_channel_analysis": "G.993 \u0391\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7 \u03ba\u03b1\u03bd\u03b1\u03bb\u03b9\u03ce\u03bd", + "g_993_message_exchange": "G.993 \u0391\u03bd\u03c4\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03bc\u03b7\u03bd\u03c5\u03bc\u03ac\u03c4\u03c9\u03bd", + "g_993_started": "G.993 \u039e\u03b5\u03ba\u03af\u03bd\u03b7\u03c3\u03b5", + "g_994_training": "G.994 \u0395\u03ba\u03c0\u03b1\u03af\u03b4\u03b5\u03c5\u03c3\u03b7", + "idle": "\u03a3\u03b5 \u03b1\u03b4\u03c1\u03ac\u03bd\u03b5\u03b9\u03b1", + "showtime": "\u038f\u03c1\u03b1 \u03b8\u03b5\u03ac\u03bc\u03b1\u03c4\u03bf\u03c2", + "unknown": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/en.json b/homeassistant/components/sfr_box/translations/en.json new file mode 100644 index 00000000000..59675fa7844 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/en.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "reauth_successful": "Re-authentication was successful" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "step": { + "auth": { + "data": { + "password": "Password", + "username": "Username" + } + }, + "choose_auth": { + "description": "Setting credentials enables additional functionality.", + "menu_options": { + "auth": "Set credentials (recommended)", + "skip_auth": "Skip authentication" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Setting the credentials is optional, but enables additional functionality." + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "Loss Of Power", + "loss_of_signal": "Loss Of Signal", + "loss_of_signal_quality": "Loss Of Signal Quality", + "no_defect": "No Defect", + "of_frame": "Of Frame", + "unknown": "Unknown" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Unknown" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.922 Channel Analysis", + "g_992_message_exchange": "G.992 Message Exchange", + "g_992_started": "G.992 Started", + "g_993_channel_analysis": "G.993 Channel Analysis", + "g_993_message_exchange": "G.993 Message Exchange", + "g_993_started": "G.993 Started", + "g_994_training": "G.994 Training", + "idle": "Idle", + "showtime": "Showtime", + "unknown": "Unknown" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/es.json b/homeassistant/components/sfr_box/translations/es.json new file mode 100644 index 00000000000..5fcf93daa98 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/es.json @@ -0,0 +1,68 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "step": { + "auth": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + } + }, + "choose_auth": { + "description": "La configuraci\u00f3n de credenciales habilita funciones adicionales.", + "menu_options": { + "auth": "Establecer credenciales (recomendado)", + "skip_auth": "Omitir autenticaci\u00f3n" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "La configuraci\u00f3n de las credenciales es opcional, pero habilita funciones adicionales." + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "P\u00e9rdida de potencia", + "loss_of_signal": "P\u00e9rdida de se\u00f1al", + "loss_of_signal_quality": "P\u00e9rdida de calidad de la se\u00f1al", + "no_defect": "Sin defectos", + "of_frame": "De marco", + "unknown": "Desconocido" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Desconocido" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "An\u00e1lisis de canal G.922", + "g_992_message_exchange": "Intercambio de mensaje G.992", + "g_992_started": "G.992 Iniciado", + "g_993_channel_analysis": "An\u00e1lisis de canal G.993", + "g_993_message_exchange": "Intercambio de mensaje G.993", + "g_993_started": "G.993 Iniciado", + "g_994_training": "Entrenamiento G.994", + "idle": "Inactivo", + "showtime": "Showtime", + "unknown": "Desconocido" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/et.json b/homeassistant/components/sfr_box/translations/et.json new file mode 100644 index 00000000000..22acf5595d0 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/et.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "reauth_successful": "Taastuvastamine \u00f5nnestus" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine" + }, + "step": { + "auth": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + }, + "choose_auth": { + "description": "Volituste seadistamine v\u00f5imaldab lisafunktsioone.", + "menu_options": { + "auth": "M\u00e4\u00e4ra mandaadid (soovitatav)", + "skip_auth": "J\u00e4ta autentimine vahele" + } + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Mandaadi m\u00e4\u00e4ramine on valikuline kuid lubab lisafunktsioone." + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "V\u00f5imsuse kaotus", + "loss_of_signal": "Signaali kadumine", + "loss_of_signal_quality": "Signaali kvaliteedi kaotus", + "no_defect": "Korras", + "of_frame": "Kaadrist", + "unknown": "Teadmata" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Teadmata" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.922 kanali anal\u00fc\u00fcs", + "g_992_message_exchange": "G.992 s\u00f5numivahetus", + "g_992_started": "G.992 algas", + "g_993_channel_analysis": "G.993 kanali anal\u00fc\u00fcs", + "g_993_message_exchange": "G.993 s\u00f5numivahetus", + "g_993_started": "G.993 algas", + "g_994_training": "G.994 koolitus", + "idle": "Ootel", + "showtime": "Kuvamise aeg", + "unknown": "Teadmata" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/fr.json b/homeassistant/components/sfr_box/translations/fr.json new file mode 100644 index 00000000000..4da885d870f --- /dev/null +++ b/homeassistant/components/sfr_box/translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00c9chec de connexion" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/he.json b/homeassistant/components/sfr_box/translations/he.json new file mode 100644 index 00000000000..25fe66938d7 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/he.json @@ -0,0 +1,17 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/hu.json b/homeassistant/components/sfr_box/translations/hu.json new file mode 100644 index 00000000000..6040e0926c7 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/hu.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm" + } + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "Teljes\u00edtm\u00e9nyveszt\u00e9s", + "loss_of_signal": "Jelveszt\u00e9s", + "loss_of_signal_quality": "Jelmin\u0151s\u00e9g-vesztes\u00e9g", + "no_defect": "Nincs hiba", + "of_frame": "Of keret", + "unknown": "Ismeretlen" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Ismeretlen" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.922 csatornaelemz\u00e9s", + "g_992_message_exchange": "G.992 \u00fczenetv\u00e1lt\u00e1s", + "g_992_started": "G.992 elindult", + "g_993_channel_analysis": "G.993 csatornaelemz\u00e9s", + "g_993_message_exchange": "G.993 \u00fczenetv\u00e1lt\u00e1s", + "g_993_started": "G.993 elindult", + "g_994_training": "G.994 kalibr\u00e1l\u00e1s", + "idle": "T\u00e9tlen", + "showtime": "Showtime", + "unknown": "Ismeretlen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/id.json b/homeassistant/components/sfr_box/translations/id.json new file mode 100644 index 00000000000..a30264679a5 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/id.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "Kehilangan Daya", + "loss_of_signal": "Kehilangan Sinyal", + "loss_of_signal_quality": "Kehilangan Kualitas Sinyal", + "no_defect": "Tidak Ada Cacat", + "of_frame": "Dari Bingkai", + "unknown": "Tidak Dikenal" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Tidak Dikenal" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "Analisis Saluran G.922", + "g_992_message_exchange": "Pertukaran Pesan G.992", + "g_992_started": "G.992 Dimulai", + "g_993_channel_analysis": "Analisis Saluran G.993", + "g_993_message_exchange": "Pertukaran Pesan G.993", + "g_993_started": "G.993 Dimulai", + "g_994_training": "G.994 Pelatihan", + "idle": "Siaga", + "showtime": "Waktu tayang", + "unknown": "Tidak Dikenal" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/it.json b/homeassistant/components/sfr_box/translations/it.json new file mode 100644 index 00000000000..638b0cb35fa --- /dev/null +++ b/homeassistant/components/sfr_box/translations/it.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "Perdita di potenza", + "loss_of_signal": "Perdita di segnale", + "loss_of_signal_quality": "Perdita di qualit\u00e0 del segnale", + "no_defect": "Nessun difetto", + "of_frame": "Di Telaio", + "unknown": "Sconosciuto" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Sconosciuto" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.992 Analisi del canale", + "g_992_message_exchange": "G.992 Scambio di messaggi", + "g_992_started": "G.992 Avviato", + "g_993_channel_analysis": "G.993 Analisi del canale", + "g_993_message_exchange": "G.993 Scambio di messaggi", + "g_993_started": "G.993 Avviato", + "g_994_training": "G.994 Formazione", + "idle": "Inattivo", + "showtime": "Orario dello spettacolo", + "unknown": "Sconosciuto" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/ja.json b/homeassistant/components/sfr_box/translations/ja.json new file mode 100644 index 00000000000..f2b8a58088c --- /dev/null +++ b/homeassistant/components/sfr_box/translations/ja.json @@ -0,0 +1,27 @@ +{ + "entity": { + "sensor": { + "line_status": { + "state": { + "unknown": "\u4e0d\u660e" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS" + } + }, + "training": { + "state": { + "g_993_started": "G.993\u958b\u59cb", + "g_994_training": "G.994\u30c8\u30ec\u30fc\u30cb\u30f3\u30b0", + "idle": "\u30a2\u30a4\u30c9\u30eb", + "showtime": "\u30b7\u30e7\u30fc\u30bf\u30a4\u30e0", + "unknown": "\u4e0d\u660e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/ko.json b/homeassistant/components/sfr_box/translations/ko.json new file mode 100644 index 00000000000..1ab9dfe9d76 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/ko.json @@ -0,0 +1,11 @@ +{ + "entity": { + "sensor": { + "training": { + "state": { + "unknown": "\uc54c \uc218 \uc5c6\uc74c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/lv.json b/homeassistant/components/sfr_box/translations/lv.json new file mode 100644 index 00000000000..6c8e5f424ff --- /dev/null +++ b/homeassistant/components/sfr_box/translations/lv.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + }, + "entity": { + "sensor": { + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Nezin\u0101ms" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/nl.json b/homeassistant/components/sfr_box/translations/nl.json new file mode 100644 index 00000000000..bd64330e60e --- /dev/null +++ b/homeassistant/components/sfr_box/translations/nl.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "unknown": "Onbekend" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Onbekend" + } + }, + "training": { + "state": { + "idle": "Inactief", + "unknown": "Onbekend" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/no.json b/homeassistant/components/sfr_box/translations/no.json new file mode 100644 index 00000000000..af245d78752 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/no.json @@ -0,0 +1,68 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "auth": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + }, + "choose_auth": { + "description": "Innstilling av legitimasjon muliggj\u00f8r ekstra funksjonalitet.", + "menu_options": { + "auth": "Angi legitimasjon (anbefalt)", + "skip_auth": "Hopp over autentisering" + } + }, + "user": { + "data": { + "host": "Vert" + }, + "description": "Innstilling av legitimasjon er valgfritt, men muliggj\u00f8r tilleggsfunksjonalitet." + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "Tap av kraft", + "loss_of_signal": "Tap av signal", + "loss_of_signal_quality": "Tap av signalkvalitet", + "no_defect": "Ingen feil", + "of_frame": "Av ramme", + "unknown": "Ukjent" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Ukjent" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.922 Kanalanalyse", + "g_992_message_exchange": "G.992 Meldingsutveksling", + "g_992_started": "G.992 Startet", + "g_993_channel_analysis": "G.993 Kanalanalyse", + "g_993_message_exchange": "G.993 Utveksling av meldinger", + "g_993_started": "G.993 Startet", + "g_994_training": "G.994 Oppl\u00e6ring", + "idle": "Tomgang", + "showtime": "Showtime", + "unknown": "Ukjent" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/pl.json b/homeassistant/components/sfr_box/translations/pl.json new file mode 100644 index 00000000000..bfccbf9b447 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/pl.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + } + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "utrata zasilania", + "loss_of_signal": "utrata sygna\u0142u", + "loss_of_signal_quality": "utrata jako\u015bci sygna\u0142u", + "no_defect": "brak uszkodze\u0144", + "of_frame": "ramka", + "unknown": "nieznany" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "nieznany" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "analiza kana\u0142u G.922", + "g_992_message_exchange": "wymiana komunikat\u00f3w G.992", + "g_992_started": "G.992 rozpocz\u0119ty", + "g_993_channel_analysis": "analiza kana\u0142u G.993", + "g_993_message_exchange": "wymiana komunikat\u00f3w G.993", + "g_993_started": "G.993 rozpocz\u0119ty", + "g_994_training": "trenowanie G.994", + "idle": "bezczynny", + "showtime": "showtime", + "unknown": "nieznany" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/pt-BR.json b/homeassistant/components/sfr_box/translations/pt-BR.json new file mode 100644 index 00000000000..e2fe22c40b0 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/pt-BR.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar" + }, + "step": { + "user": { + "data": { + "host": "Nome do host" + } + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "Perda de energia", + "loss_of_signal": "Perda de sinal", + "loss_of_signal_quality": "Perda de qualidade do sinal", + "no_defect": "Sem defeito", + "of_frame": "Da moldura", + "unknown": "Desconhecido" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Desconhecido" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.922 An\u00e1lise de Canais", + "g_992_message_exchange": "G.992 Troca de Mensagens", + "g_992_started": "G.992 Iniciado", + "g_993_channel_analysis": "G.993 An\u00e1lise de Canais", + "g_993_message_exchange": "G.993 Troca de Mensagens", + "g_993_started": "G.993 Iniciado", + "g_994_training": "Treinamento G.994", + "idle": "Ocioso", + "showtime": "Showtime", + "unknown": "Desconhecido" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/ru.json b/homeassistant/components/sfr_box/translations/ru.json new file mode 100644 index 00000000000..18174ec6450 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/ru.json @@ -0,0 +1,68 @@ +{ + "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.", + "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.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, + "step": { + "auth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + }, + "choose_auth": { + "description": "\u0412\u0432\u043e\u0434 \u0443\u0447\u0435\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438.", + "menu_options": { + "auth": "\u0412\u0432\u0435\u0441\u0442\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f)", + "skip_auth": "\u041f\u0440\u043e\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0443\u0447\u0435\u0442\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445 \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u0430, \u043d\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0444\u0443\u043d\u043a\u0446\u0438\u0438." + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "\u041f\u043e\u0442\u0435\u0440\u044f \u043c\u043e\u0449\u043d\u043e\u0441\u0442\u0438", + "loss_of_signal": "\u041f\u043e\u0442\u0435\u0440\u044f \u0441\u0438\u0433\u043d\u0430\u043b\u0430", + "loss_of_signal_quality": "\u041f\u043e\u0442\u0435\u0440\u044f \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0441\u0438\u0433\u043d\u0430\u043b\u0430", + "no_defect": "\u0411\u0435\u0437 \u0434\u0435\u0444\u0435\u043a\u0442\u043e\u0432", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "\u0410\u043d\u0430\u043b\u0438\u0437 \u043a\u0430\u043d\u0430\u043b\u043e\u0432 G.992", + "g_992_message_exchange": "\u041e\u0431\u043c\u0435\u043d \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f\u043c\u0438 G.992", + "g_992_started": "G.992 \u0437\u0430\u043f\u0443\u0449\u0435\u043d", + "g_993_channel_analysis": "\u0410\u043d\u0430\u043b\u0438\u0437 \u043a\u0430\u043d\u0430\u043b\u043e\u0432 G.993", + "g_993_message_exchange": "\u041e\u0431\u043c\u0435\u043d \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f\u043c\u0438 G.993", + "g_993_started": "G.993 \u0437\u0430\u043f\u0443\u0449\u0435\u043d", + "g_994_training": "\u041e\u0431\u0443\u0447\u0435\u043d\u0438\u0435 G.994", + "idle": "\u0411\u0435\u0437\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435", + "showtime": "\u0412\u0440\u0435\u043c\u044f \u0434\u043b\u044f \u0448\u043e\u0443", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/sk.json b/homeassistant/components/sfr_box/translations/sk.json new file mode 100644 index 00000000000..e6ae888ed14 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/sk.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "Strata nap\u00e1jania", + "loss_of_signal": "Strata sign\u00e1lu", + "loss_of_signal_quality": "Strata kvality sign\u00e1lu", + "no_defect": "Bez defektu", + "of_frame": "Of Frame", + "unknown": "Nezn\u00e1me" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Nezn\u00e1my" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.922 Anal\u00fdza kan\u00e1lov", + "g_992_message_exchange": "G.992 V\u00fdmena spr\u00e1v", + "g_992_started": "G.992 Spusten\u00e9", + "g_993_channel_analysis": "G.993 Anal\u00fdza kan\u00e1lov", + "g_993_message_exchange": "G.993 V\u00fdmena spr\u00e1v", + "g_993_started": "G.993 Spusten\u00e9", + "g_994_training": "G.994 \u0160kolenie", + "idle": "Ne\u010dinn\u00fd", + "showtime": "Showtime", + "unknown": "Nezn\u00e1me" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/sv.json b/homeassistant/components/sfr_box/translations/sv.json new file mode 100644 index 00000000000..41633445bd9 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/sv.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Tom" + }, + "step": { + "user": { + "data": { + "host": "V\u00e4rd" + } + } + } + }, + "entity": { + "sensor": { + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Ok\u00e4nd" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/tr.json b/homeassistant/components/sfr_box/translations/tr.json new file mode 100644 index 00000000000..044eadde550 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/tr.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Sunucu" + } + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "G\u00fc\u00e7 Kayb\u0131", + "loss_of_signal": "Sinyal Kayb\u0131", + "loss_of_signal_quality": "Sinyal Kalitesi Kayb\u0131", + "no_defect": "Hasar Yok", + "of_frame": "\u00c7er\u00e7evenin", + "unknown": "Bilinmeyen" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "Bilinmeyen" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.922 Kanal Analizi", + "g_992_message_exchange": "G.992 Mesaj De\u011fi\u015fimi", + "g_992_started": "G.992 Ba\u015flat\u0131ld\u0131", + "g_993_channel_analysis": "G.993 Kanal Analizi", + "g_993_message_exchange": "G.993 Mesaj De\u011fi\u015fimi", + "g_993_started": "G.993 Ba\u015flat\u0131ld\u0131", + "g_994_training": "G.994 E\u011fitimi", + "idle": "Bo\u015fta", + "showtime": "\u00c7al\u0131\u015f\u0131yor", + "unknown": "Bilinmeyen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/uk.json b/homeassistant/components/sfr_box/translations/uk.json new file mode 100644 index 00000000000..4b19d9fed05 --- /dev/null +++ b/homeassistant/components/sfr_box/translations/uk.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_signal": "\u0412\u0442\u0440\u0430\u0442\u0430 \u0441\u0438\u0433\u043d\u0430\u043b\u0443", + "unknown": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u043e" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u043e" + } + }, + "training": { + "state": { + "g_994_training": "G.994 \u041d\u0430\u0432\u0447\u0430\u043d\u043d\u044f", + "idle": "\u0411\u0435\u0437\u0434\u0456\u044f\u043b\u044c\u043d\u0456\u0441\u0442\u044c", + "unknown": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u043e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sfr_box/translations/zh-Hant.json b/homeassistant/components/sfr_box/translations/zh-Hant.json new file mode 100644 index 00000000000..9839496caaf --- /dev/null +++ b/homeassistant/components/sfr_box/translations/zh-Hant.json @@ -0,0 +1,69 @@ +{ + "config": { + "abort": { + "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", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "auth": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + }, + "choose_auth": { + "description": "\u8a2d\u5b9a\u6191\u8b49\u4ee5\u958b\u555f\u9644\u52a0\u529f\u80fd\u3002", + "menu_options": { + "auth": "\u8a2d\u5b9a\u6191\u8b49\uff08\u5efa\u8b70\uff09", + "skip_auth": "\u8df3\u904e\u6191\u8b49" + } + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u8a2d\u5b9a\u6191\u8b49\u70ba\u9078\u9805\u8a2d\u5b9a\u3001\u4f46\u662f\u80fd\u5920\u958b\u555f\u9644\u52a0\u5171\u80fd\u3002" + } + } + }, + "entity": { + "sensor": { + "line_status": { + "state": { + "loss_of_power": "\u65b7\u96fb", + "loss_of_signal": "\u65b7\u8a0a", + "loss_of_signal_quality": "\u8a0a\u865f\u54c1\u8cea\u640d\u5931", + "no_defect": "\u7121\u7f3a\u9677", + "of_frame": "\u5e40\u6578", + "unknown": "\u672a\u77e5" + } + }, + "net_infra": { + "state": { + "adsl": "ADSL", + "ftth": "FTTH", + "gprs": "GPRS", + "unknown": "\u672a\u77e5" + } + }, + "training": { + "state": { + "g_922_channel_analysis": "G.992 \u983b\u9053\u5206\u6790", + "g_992_message_exchange": "G.992 \u8a0a\u606f\u4ea4\u63db", + "g_992_started": "G.992 \u5df2\u958b\u59cb", + "g_993_channel_analysis": "G.993 \u983b\u9053\u5206\u6790", + "g_993_message_exchange": "G.993 \u8a0a\u606f\u4ea4\u63db", + "g_993_started": "G.993 \u5df2\u958b\u59cb", + "g_994_training": "G.994 \u8a13\u7df4", + "idle": "\u9592\u7f6e", + "showtime": "\u958b\u64ad\u6642\u9593", + "unknown": "\u672a\u77e5" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index ac47830f840..d4a0a3ac1d5 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -91,7 +91,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: with suppress(TypeError): process.kill() # https://bugs.python.org/issue43884 - # pylint: disable=protected-access + # pylint: disable-next=protected-access process._transport.close() # type: ignore[attr-defined] del process diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index df054598f5c..b57ec6fa96d 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -4,8 +4,8 @@ from __future__ import annotations import contextlib from typing import Any, Final -import aioshelly from aioshelly.block_device import BlockDevice +from aioshelly.common import ConnectionOptions from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError from aioshelly.rpc_device import RpcDevice, UpdateType import voluptuous as vol @@ -14,8 +14,13 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers import aiohttp_client, device_registry +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + async_get as dr_async_get, + format_mac, +) from homeassistant.helpers.typing import ConfigType from .const import ( @@ -116,32 +121,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Shelly block based device from a config entry.""" - options = aioshelly.common.ConnectionOptions( + options = ConnectionOptions( entry.data[CONF_HOST], entry.data.get(CONF_USERNAME), entry.data.get(CONF_PASSWORD), + device_mac=entry.unique_id, ) coap_context = await get_coap_context(hass) device = await BlockDevice.create( - aiohttp_client.async_get_clientsession(hass), + async_get_clientsession(hass), coap_context, options, False, ) - dev_reg = device_registry.async_get(hass) + dev_reg = dr_async_get(hass) device_entry = None if entry.unique_id is not None: device_entry = dev_reg.async_get_device( identifiers=set(), - connections={ - ( - device_registry.CONNECTION_NETWORK_MAC, - device_registry.format_mac(entry.unique_id), - ) - }, + connections={(CONNECTION_NETWORK_MAC, format_mac(entry.unique_id))}, ) # https://github.com/home-assistant/core/pull/48076 if device_entry and entry.entry_id not in device_entry.config_entries: @@ -150,8 +151,7 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b sleep_period = entry.data.get(CONF_SLEEP_PERIOD) shelly_entry_data = get_entry_data(hass)[entry.entry_id] - @callback - def _async_block_device_setup() -> None: + async def _async_block_device_setup() -> None: """Set up a block based device that is online.""" shelly_entry_data.block = ShellyBlockCoordinator(hass, entry, device) shelly_entry_data.block.async_setup() @@ -162,7 +162,7 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b shelly_entry_data.rest = ShellyRestCoordinator(hass, device, entry) platforms = BLOCK_PLATFORMS - hass.config_entries.async_setup_platforms(entry, platforms) + await hass.config_entries.async_forward_entry_setups(entry, platforms) @callback def _async_device_online(_: Any) -> None: @@ -175,7 +175,7 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b data["model"] = device.settings["device"]["type"] hass.config_entries.async_update_entry(entry, data=data) - _async_block_device_setup() + hass.async_create_task(_async_block_device_setup()) if sleep_period == 0: # Not a sleeping device, finish setup @@ -187,7 +187,7 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b except InvalidAuthError as err: raise ConfigEntryAuthFailed(repr(err)) from err - _async_block_device_setup() + await _async_block_device_setup() elif sleep_period is None or device_entry is None: # Need to get sleep info or first time sleeping device setup, wait for device shelly_entry_data.device = device @@ -198,39 +198,35 @@ async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> b else: # Restore sensors for sleeping device LOGGER.debug("Setting up offline block device %s", entry.title) - _async_block_device_setup() + await _async_block_device_setup() return True async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Shelly RPC based device from a config entry.""" - options = aioshelly.common.ConnectionOptions( + options = ConnectionOptions( entry.data[CONF_HOST], entry.data.get(CONF_USERNAME), entry.data.get(CONF_PASSWORD), + device_mac=entry.unique_id, ) ws_context = await get_ws_context(hass) device = await RpcDevice.create( - aiohttp_client.async_get_clientsession(hass), + async_get_clientsession(hass), ws_context, options, False, ) - dev_reg = device_registry.async_get(hass) + dev_reg = dr_async_get(hass) device_entry = None if entry.unique_id is not None: device_entry = dev_reg.async_get_device( identifiers=set(), - connections={ - ( - device_registry.CONNECTION_NETWORK_MAC, - device_registry.format_mac(entry.unique_id), - ) - }, + connections={(CONNECTION_NETWORK_MAC, format_mac(entry.unique_id))}, ) # https://github.com/home-assistant/core/pull/48076 if device_entry and entry.entry_id not in device_entry.config_entries: @@ -239,8 +235,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo sleep_period = entry.data.get(CONF_SLEEP_PERIOD) shelly_entry_data = get_entry_data(hass)[entry.entry_id] - @callback - def _async_rpc_device_setup() -> None: + async def _async_rpc_device_setup() -> None: """Set up a RPC based device that is online.""" shelly_entry_data.rpc = ShellyRpcCoordinator(hass, entry, device) shelly_entry_data.rpc.async_setup() @@ -253,7 +248,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo ) platforms = RPC_PLATFORMS - hass.config_entries.async_setup_platforms(entry, platforms) + await hass.config_entries.async_forward_entry_setups(entry, platforms) @callback def _async_device_online(_: Any, update_type: UpdateType) -> None: @@ -265,7 +260,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo data[CONF_SLEEP_PERIOD] = get_rpc_device_sleep_period(device.config) hass.config_entries.async_update_entry(entry, data=data) - _async_rpc_device_setup() + hass.async_create_task(_async_rpc_device_setup()) if sleep_period == 0: # Not a sleeping device, finish setup @@ -277,7 +272,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo except InvalidAuthError as err: raise ConfigEntryAuthFailed(repr(err)) from err - _async_rpc_device_setup() + await _async_rpc_device_setup() elif sleep_period is None or device_entry is None: # Need to get sleep info or first time sleeping device setup, wait for device shelly_entry_data.device = device @@ -288,7 +283,7 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ConfigEntry) -> boo else: # Restore sensors for sleeping device LOGGER.debug("Setting up offline block device %s", entry.title) - _async_rpc_device_setup() + await _async_rpc_device_setup() return True diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index a5265241da3..716303b7bda 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -126,7 +126,7 @@ SENSORS: Final = { ), ("sensor", "extInput"): BlockBinarySensorDescription( key="sensor|extInput", - name="External Input", + name="External input", device_class=BinarySensorDeviceClass.POWER, entity_registry_enabled_default=False, ), @@ -190,6 +190,12 @@ RPC_SENSORS: Final = { entity_category=EntityCategory.DIAGNOSTIC, supported=lambda status: status.get("apower") is not None, ), + "smoke": RpcBinarySensorDescription( + key="smoke", + sub_key="alarm", + name="Smoke", + device_class=BinarySensorDeviceClass.SMOKE, + ), } diff --git a/homeassistant/components/shelly/bluetooth/scanner.py b/homeassistant/components/shelly/bluetooth/scanner.py index f255d01c78b..5b302e0da62 100644 --- a/homeassistant/components/shelly/bluetooth/scanner.py +++ b/homeassistant/components/shelly/bluetooth/scanner.py @@ -1,7 +1,6 @@ """Bluetooth scanner for shelly.""" from __future__ import annotations -import logging from typing import Any from aioshelly.ble import parse_ble_scan_result_event @@ -10,7 +9,7 @@ from aioshelly.ble.const import BLE_SCAN_RESULT_EVENT, BLE_SCAN_RESULT_VERSION from homeassistant.components.bluetooth import BaseHaRemoteScanner from homeassistant.core import callback -_LOGGER = logging.getLogger(__name__) +from ..const import LOGGER class ShellyBLEScanner(BaseHaRemoteScanner): @@ -25,7 +24,7 @@ class ShellyBLEScanner(BaseHaRemoteScanner): data = event["data"] if data[0] != BLE_SCAN_RESULT_VERSION: - _LOGGER.warning("Unsupported BLE scan result version: %s", data[0]) + LOGGER.warning("Unsupported BLE scan result version: %s", data[0]) return try: @@ -33,7 +32,7 @@ class ShellyBLEScanner(BaseHaRemoteScanner): except Exception as err: # pylint: disable=broad-except # Broad exception catch because we have no # control over the data that is coming in. - _LOGGER.error("Failed to parse BLE event: %s", err, exc_info=True) + LOGGER.error("Failed to parse BLE event: %s", err, exc_info=True) return self._async_on_advertisement( diff --git a/homeassistant/components/shelly/button.py b/homeassistant/components/shelly/button.py index e7989dd9417..077fea16b6c 100644 --- a/homeassistant/components/shelly/button.py +++ b/homeassistant/components/shelly/button.py @@ -1,9 +1,9 @@ """Button for Shelly.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Coroutine from dataclasses import dataclass -from typing import Final +from typing import Any, Final, Generic, TypeVar from homeassistant.components.button import ( ButtonDeviceClass, @@ -22,38 +22,44 @@ from .const import SHELLY_GAS_MODELS from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data from .utils import get_block_device_name, get_device_entry_gen, get_rpc_device_name +_ShellyCoordinatorT = TypeVar( + "_ShellyCoordinatorT", bound=ShellyBlockCoordinator | ShellyRpcCoordinator +) + @dataclass -class ShellyButtonDescriptionMixin: +class ShellyButtonDescriptionMixin(Generic[_ShellyCoordinatorT]): """Mixin to describe a Button entity.""" - press_action: Callable + press_action: Callable[[_ShellyCoordinatorT], Coroutine[Any, Any, None]] @dataclass -class ShellyButtonDescription(ButtonEntityDescription, ShellyButtonDescriptionMixin): +class ShellyButtonDescription( + ButtonEntityDescription, ShellyButtonDescriptionMixin[_ShellyCoordinatorT] +): """Class to describe a Button entity.""" - supported: Callable = lambda _: True + supported: Callable[[_ShellyCoordinatorT], bool] = lambda _: True -BUTTONS: Final = [ - ShellyButtonDescription( +BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [ + ShellyButtonDescription[ShellyBlockCoordinator | ShellyRpcCoordinator]( key="reboot", name="Reboot", device_class=ButtonDeviceClass.RESTART, entity_category=EntityCategory.CONFIG, press_action=lambda coordinator: coordinator.device.trigger_reboot(), ), - ShellyButtonDescription( + ShellyButtonDescription[ShellyBlockCoordinator]( key="self_test", - name="Self Test", + name="Self test", icon="mdi:progress-wrench", entity_category=EntityCategory.DIAGNOSTIC, press_action=lambda coordinator: coordinator.device.trigger_shelly_gas_self_test(), supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS, ), - ShellyButtonDescription( + ShellyButtonDescription[ShellyBlockCoordinator]( key="mute", name="Mute", icon="mdi:volume-mute", @@ -61,7 +67,7 @@ BUTTONS: Final = [ press_action=lambda coordinator: coordinator.device.trigger_shelly_gas_mute(), supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS, ), - ShellyButtonDescription( + ShellyButtonDescription[ShellyBlockCoordinator]( key="unmute", name="Unmute", icon="mdi:volume-high", @@ -85,7 +91,7 @@ async def async_setup_entry( coordinator = get_entry_data(hass)[config_entry.entry_id].block if coordinator is not None: - entities = [] + entities: list[ShellyButton] = [] for button in BUTTONS: if not button.supported(coordinator): @@ -95,15 +101,21 @@ async def async_setup_entry( async_add_entities(entities) -class ShellyButton(CoordinatorEntity, ButtonEntity): +class ShellyButton( + CoordinatorEntity[ShellyRpcCoordinator | ShellyBlockCoordinator], ButtonEntity +): """Defines a Shelly base button.""" - entity_description: ShellyButtonDescription + entity_description: ShellyButtonDescription[ + ShellyRpcCoordinator | ShellyBlockCoordinator + ] def __init__( self, coordinator: ShellyRpcCoordinator | ShellyBlockCoordinator, - description: ShellyButtonDescription, + description: ShellyButtonDescription[ + ShellyRpcCoordinator | ShellyBlockCoordinator + ], ) -> None: """Initialize Shelly button.""" super().__init__(coordinator) diff --git a/homeassistant/components/shelly/climate.py b/homeassistant/components/shelly/climate.py index b9435a56ebd..5102ed9e4c3 100644 --- a/homeassistant/components/shelly/climate.py +++ b/homeassistant/components/shelly/climate.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Mapping +from dataclasses import asdict, dataclass from typing import Any, cast from aioshelly.block_device import Block @@ -19,10 +20,15 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import device_registry, entity_registry +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.restore_state import RestoreEntity +from homeassistant.helpers.entity_registry import ( + RegistryEntry, + async_entries_for_config_entry, + async_get as er_async_get, +) +from homeassistant.helpers.restore_state import ExtraStoredData, RestoreEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.unit_conversion import TemperatureConverter from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM @@ -81,10 +87,8 @@ def async_restore_climate_entities( ) -> None: """Restore sleeping climate devices.""" - ent_reg = entity_registry.async_get(hass) - entries = entity_registry.async_entries_for_config_entry( - ent_reg, config_entry.entry_id - ) + ent_reg = er_async_get(hass) + entries = async_entries_for_config_entry(ent_reg, config_entry.entry_id) for entry in entries: @@ -97,6 +101,17 @@ def async_restore_climate_entities( break +@dataclass +class ShellyClimateExtraStoredData(ExtraStoredData): + """Object to hold extra stored data.""" + + last_target_temp: float | None = None + + def as_dict(self) -> dict[str, Any]: + """Return a dict representation of the text data.""" + return asdict(self) + + class BlockSleepingClimate( CoordinatorEntity[ShellyBlockCoordinator], RestoreEntity, ClimateEntity ): @@ -117,7 +132,7 @@ class BlockSleepingClimate( coordinator: ShellyBlockCoordinator, sensor_block: Block | None, device_block: Block | None, - entry: entity_registry.RegistryEntry | None = None, + entry: RegistryEntry | None = None, ) -> None: """Initialize climate.""" super().__init__(coordinator) @@ -128,14 +143,7 @@ class BlockSleepingClimate( self.last_state: State | None = None self.last_state_attributes: Mapping[str, Any] self._preset_modes: list[str] = [] - if coordinator.hass.config.units is US_CUSTOMARY_SYSTEM: - self._last_target_temp = TemperatureConverter.convert( - SHTRV_01_TEMPERATURE_SETTINGS["default"], - UnitOfTemperature.CELSIUS, - UnitOfTemperature.FAHRENHEIT, - ) - else: - self._last_target_temp = SHTRV_01_TEMPERATURE_SETTINGS["default"] + self._last_target_temp = SHTRV_01_TEMPERATURE_SETTINGS["default"] if self.block is not None and self.device_block is not None: self._unique_id = f"{self.coordinator.mac}-{self.block.description}" @@ -151,6 +159,11 @@ class BlockSleepingClimate( self._channel = cast(int, self._unique_id.split("_")[1]) + @property + def extra_restore_state_data(self) -> ShellyClimateExtraStoredData: + """Return text specific state data to be restored.""" + return ShellyClimateExtraStoredData(self._last_target_temp) + @property def unique_id(self) -> str: """Set unique id of entity.""" @@ -242,11 +255,7 @@ class BlockSleepingClimate( @property def device_info(self) -> DeviceInfo: """Device info.""" - return { - "connections": { - (device_registry.CONNECTION_NETWORK_MAC, self.coordinator.mac) - } - } + return {"connections": {(CONNECTION_NETWORK_MAC, self.coordinator.mac)}} def _check_is_off(self) -> bool: """Return if valve is off or on.""" @@ -309,7 +318,6 @@ class BlockSleepingClimate( LOGGER.info("Restoring entity %s", self.name) last_state = await self.async_get_last_state() - if last_state is not None: self.last_state = last_state self.last_state_attributes = self.last_state.attributes @@ -317,6 +325,10 @@ class BlockSleepingClimate( list, self.last_state.attributes.get("preset_modes") ) + last_extra_data = await self.async_get_last_extra_data() + if last_extra_data is not None: + self._last_target_temp = last_extra_data.as_dict()["last_target_temp"] + await super().async_added_to_hass() @callback diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index f6be4a254c6..0979e6e036a 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -4,8 +4,8 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any, Final -import aioshelly from aioshelly.block_device import BlockDevice +from aioshelly.common import ConnectionOptions, get_info from aioshelly.exceptions import ( DeviceConnectionError, FirmwareUnsupported, @@ -15,12 +15,13 @@ from aioshelly.rpc_device import RpcDevice from awesomeversion import AwesomeVersion import voluptuous as vol -from homeassistant import config_entries -from homeassistant.components import zeroconf +from homeassistant.components.zeroconf import ZeroconfServiceInfo +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers import aiohttp_client, selector +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig from .const import ( BLE_MIN_VERSION, @@ -48,9 +49,9 @@ HOST_SCHEMA: Final = vol.Schema({vol.Required(CONF_HOST): str}) BLE_SCANNER_OPTIONS = [ - selector.SelectOptionDict(value=BLEScannerMode.DISABLED, label="Disabled"), - selector.SelectOptionDict(value=BLEScannerMode.ACTIVE, label="Active"), - selector.SelectOptionDict(value=BLEScannerMode.PASSIVE, label="Passive"), + BLEScannerMode.DISABLED, + BLEScannerMode.ACTIVE, + BLEScannerMode.PASSIVE, ] INTERNAL_WIFI_AP_IP = "192.168.33.1" @@ -66,16 +67,12 @@ async def validate_input( Data has the keys from HOST_SCHEMA with values provided by the user. """ - options = aioshelly.common.ConnectionOptions( - host, - data.get(CONF_USERNAME), - data.get(CONF_PASSWORD), - ) + options = ConnectionOptions(host, data.get(CONF_USERNAME), data.get(CONF_PASSWORD)) if get_info_gen(info) == 2: ws_context = await get_ws_context(hass) rpc_device = await RpcDevice.create( - aiohttp_client.async_get_clientsession(hass), + async_get_clientsession(hass), ws_context, options, ) @@ -92,7 +89,7 @@ async def validate_input( # Gen1 coap_context = await get_coap_context(hass) block_device = await BlockDevice.create( - aiohttp_client.async_get_clientsession(hass), + async_get_clientsession(hass), coap_context, options, ) @@ -105,7 +102,7 @@ async def validate_input( } -class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Shelly.""" VERSION = 1 @@ -113,7 +110,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): host: str = "" info: dict[str, Any] = {} device_info: dict[str, Any] = {} - entry: config_entries.ConfigEntry | None = None + entry: ConfigEntry | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -232,7 +229,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured({CONF_HOST: host}) async def async_step_zeroconf( - self, discovery_info: zeroconf.ZeroconfServiceInfo + self, discovery_info: ZeroconfServiceInfo ) -> FlowResult: """Handle zeroconf discovery.""" host = discovery_info.host @@ -329,12 +326,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await validate_input(self.hass, host, info, user_input) except (DeviceConnectionError, InvalidAuthError, FirmwareUnsupported): return self.async_abort(reason="reauth_unsuccessful") - else: - self.hass.config_entries.async_update_entry( - self.entry, data={**self.entry.data, **user_input} - ) - await self.hass.config_entries.async_reload(self.entry.entry_id) - return self.async_abort(reason="reauth_successful") + + self.hass.config_entries.async_update_entry( + self.entry, data={**self.entry.data, **user_input} + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") if self.entry.data.get("gen", 1) == 1: schema = { @@ -352,33 +349,27 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_get_info(self, host: str) -> dict[str, Any]: """Get info from shelly device.""" - return await aioshelly.common.get_info( - aiohttp_client.async_get_clientsession(self.hass), host - ) + return await get_info(async_get_clientsession(self.hass), host) @staticmethod @callback - def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> OptionsFlowHandler: + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) @classmethod @callback - def async_supports_options_flow( - cls, config_entry: config_entries.ConfigEntry - ) -> bool: + def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool: """Return options flow support for this handler.""" return config_entry.data.get("gen") == 2 and not config_entry.data.get( CONF_SLEEP_PERIOD ) -class OptionsFlowHandler(config_entries.OptionsFlow): +class OptionsFlowHandler(OptionsFlow): """Handle the option flow for shelly.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry @@ -407,8 +398,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow): default=self.config_entry.options.get( CONF_BLE_SCANNER_MODE, BLEScannerMode.DISABLED ), - ): selector.SelectSelector( - selector.SelectSelectorConfig(options=BLE_SCANNER_OPTIONS), + ): SelectSelector( + SelectSelectorConfig( + options=BLE_SCANNER_OPTIONS, + translation_key=CONF_BLE_SCANNER_MODE, + ), ), } ), diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 9029f18eb22..37f1461f62f 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -5,7 +5,7 @@ import asyncio from collections.abc import Callable, Coroutine from dataclasses import dataclass from datetime import timedelta -from typing import Any, cast +from typing import Any, Generic, TypeVar, cast import aioshelly from aioshelly.ble import async_ensure_ble_enabled, async_stop_scanner @@ -14,12 +14,14 @@ from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCal from aioshelly.rpc_device import RpcDevice, UpdateType from awesomeversion import AwesomeVersion -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ATTR_DEVICE_ID, CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback -from homeassistant.helpers import device_registry from homeassistant.helpers.debounce import Debouncer +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + async_get as dr_async_get, +) from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .bluetooth import async_connect_scanner @@ -49,12 +51,9 @@ from .const import ( UPDATE_PERIOD_MULTIPLIER, BLEScannerMode, ) -from .utils import ( - device_update_info, - get_block_device_name, - get_rpc_device_name, - get_rpc_device_wakeup_period, -) +from .utils import device_update_info, get_device_name, get_rpc_device_wakeup_period + +_DeviceT = TypeVar("_DeviceT", bound="BlockDevice|RpcDevice") @dataclass @@ -73,34 +72,23 @@ def get_entry_data(hass: HomeAssistant) -> dict[str, ShellyEntryData]: return cast(dict[str, ShellyEntryData], hass.data[DOMAIN][DATA_CONFIG_ENTRY]) -class ShellyBlockCoordinator(DataUpdateCoordinator[None]): - """Coordinator for a Shelly block based device.""" +class ShellyCoordinatorBase(DataUpdateCoordinator[None], Generic[_DeviceT]): + """Coordinator for a Shelly device.""" def __init__( - self, hass: HomeAssistant, entry: ConfigEntry, device: BlockDevice + self, + hass: HomeAssistant, + entry: ConfigEntry, + device: _DeviceT, + update_interval: float, ) -> None: - """Initialize the Shelly block device coordinator.""" - self.device_id: str | None = None - - if sleep_period := entry.data[CONF_SLEEP_PERIOD]: - update_interval = SLEEP_PERIOD_MULTIPLIER * sleep_period - else: - update_interval = ( - UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] - ) - - device_name = ( - get_block_device_name(device) if device.initialized else entry.title - ) - super().__init__( - hass, - LOGGER, - name=device_name, - update_interval=timedelta(seconds=update_interval), - ) - self.hass = hass + """Initialize the Shelly device coordinator.""" self.entry = entry self.device = device + self.device_id: str | None = None + device_name = get_device_name(device) if device.initialized else entry.title + interval_td = timedelta(seconds=update_interval) + super().__init__(hass, LOGGER, name=device_name, update_interval=interval_td) self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer( hass, @@ -110,24 +98,77 @@ class ShellyBlockCoordinator(DataUpdateCoordinator[None]): function=self._async_reload_entry, ) entry.async_on_unload(self._debounced_reload.async_cancel) + + @property + def model(self) -> str: + """Model of the device.""" + return cast(str, self.entry.data["model"]) + + @property + def mac(self) -> str: + """Mac address of the device.""" + return cast(str, self.entry.unique_id) + + @property + def sw_version(self) -> str: + """Firmware version of the device.""" + return self.device.firmware_version if self.device.initialized else "" + + @property + def sleep_period(self) -> int: + """Sleep period of the device.""" + return self.entry.data.get(CONF_SLEEP_PERIOD, 0) + + def async_setup(self) -> None: + """Set up the coordinator.""" + dev_reg = dr_async_get(self.hass) + device_entry = dev_reg.async_get_or_create( + config_entry_id=self.entry.entry_id, + name=self.name, + connections={(CONNECTION_NETWORK_MAC, self.mac)}, + manufacturer="Shelly", + model=aioshelly.const.MODEL_NAMES.get(self.model, self.model), + sw_version=self.sw_version, + hw_version=f"gen{self.device.gen} ({self.model})", + configuration_url=f"http://{self.entry.data[CONF_HOST]}", + ) + self.device_id = device_entry.id + + async def _async_reload_entry(self) -> None: + """Reload entry.""" + self._debounced_reload.async_cancel() + LOGGER.debug("Reloading entry %s", self.name) + await self.hass.config_entries.async_reload(self.entry.entry_id) + + +class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): + """Coordinator for a Shelly block based device.""" + + def __init__( + self, hass: HomeAssistant, entry: ConfigEntry, device: BlockDevice + ) -> None: + """Initialize the Shelly block device coordinator.""" + self.entry = entry + if self.sleep_period: + update_interval = SLEEP_PERIOD_MULTIPLIER * self.sleep_period + else: + update_interval = ( + UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] + ) + super().__init__(hass, entry, device, update_interval) + self._last_cfg_changed: int | None = None self._last_mode: str | None = None self._last_effect: int | None = None + self._last_input_events_count: dict = {} entry.async_on_unload( self.async_add_listener(self._async_device_updates_handler) ) - self._last_input_events_count: dict = {} - entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) ) - async def _async_reload_entry(self) -> None: - """Reload entry.""" - LOGGER.debug("Reloading entry %s", self.name) - await self.hass.config_entries.async_reload(self.entry.entry_id) - @callback def _async_device_updates_handler(self) -> None: """Handle device updates.""" @@ -209,10 +250,10 @@ class ShellyBlockCoordinator(DataUpdateCoordinator[None]): async def _async_update_data(self) -> None: """Fetch data.""" - if sleep_period := self.entry.data.get(CONF_SLEEP_PERIOD): + if self.sleep_period: # Sleeping device, no point polling it, just mark it unavailable raise UpdateFailed( - f"Sleeping device did not update within {sleep_period} seconds interval" + f"Sleeping device did not update within {self.sleep_period} seconds interval" ) LOGGER.debug("Polling Shelly Block Device - %s", self.name) @@ -225,35 +266,9 @@ class ShellyBlockCoordinator(DataUpdateCoordinator[None]): else: device_update_info(self.hass, self.device, self.entry) - @property - def model(self) -> str: - """Model of the device.""" - return cast(str, self.entry.data["model"]) - - @property - def mac(self) -> str: - """Mac address of the device.""" - return cast(str, self.entry.unique_id) - - @property - def sw_version(self) -> str: - """Firmware version of the device.""" - return self.device.firmware_version if self.device.initialized else "" - def async_setup(self) -> None: """Set up the coordinator.""" - dev_reg = device_registry.async_get(self.hass) - entry = dev_reg.async_get_or_create( - config_entry_id=self.entry.entry_id, - name=self.name, - connections={(device_registry.CONNECTION_NETWORK_MAC, self.mac)}, - manufacturer="Shelly", - model=aioshelly.const.MODEL_NAMES.get(self.model, self.model), - sw_version=self.sw_version, - hw_version=f"gen{self.device.gen} ({self.model})", - configuration_url=f"http://{self.entry.data[CONF_HOST]}", - ) - self.device_id = entry.id + super().async_setup() self.device.subscribe_updates(self.async_set_updated_data) def shutdown(self) -> None: @@ -267,13 +282,14 @@ class ShellyBlockCoordinator(DataUpdateCoordinator[None]): self.shutdown() -class ShellyRestCoordinator(DataUpdateCoordinator): +class ShellyRestCoordinator(ShellyCoordinatorBase[BlockDevice]): """Coordinator for a Shelly REST device.""" def __init__( self, hass: HomeAssistant, device: BlockDevice, entry: ConfigEntry ) -> None: """Initialize the Shelly REST device coordinator.""" + update_interval = REST_SENSORS_UPDATE_INTERVAL if ( device.settings["device"]["type"] in BATTERY_DEVICES_WITH_PERMANENT_CONNECTION @@ -281,17 +297,7 @@ class ShellyRestCoordinator(DataUpdateCoordinator): update_interval = ( SLEEP_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] ) - else: - update_interval = REST_SENSORS_UPDATE_INTERVAL - - super().__init__( - hass, - LOGGER, - name=get_block_device_name(device), - update_interval=timedelta(seconds=update_interval), - ) - self.device = device - self.entry = entry + super().__init__(hass, entry, device, update_interval) async def _async_update_data(self) -> None: """Fetch data.""" @@ -312,64 +318,37 @@ class ShellyRestCoordinator(DataUpdateCoordinator): else: device_update_info(self.hass, self.device, self.entry) - @property - def mac(self) -> str: - """Mac address of the device.""" - return cast(str, self.device.settings["device"]["mac"]) - -class ShellyRpcCoordinator(DataUpdateCoordinator[None]): +class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]): """Coordinator for a Shelly RPC based device.""" def __init__( self, hass: HomeAssistant, entry: ConfigEntry, device: RpcDevice ) -> None: """Initialize the Shelly RPC device coordinator.""" - self.device_id: str | None = None - - if sleep_period := entry.data[CONF_SLEEP_PERIOD]: - update_interval = SLEEP_PERIOD_MULTIPLIER * sleep_period + self.entry = entry + if self.sleep_period: + update_interval = SLEEP_PERIOD_MULTIPLIER * self.sleep_period else: update_interval = RPC_RECONNECT_INTERVAL - device_name = get_rpc_device_name(device) if device.initialized else entry.title - super().__init__( - hass, - LOGGER, - name=device_name, - update_interval=timedelta(seconds=update_interval), - ) - self.entry = entry - self.device = device - self.connected = False + super().__init__(hass, entry, device, update_interval) + self.connected = False self._disconnected_callbacks: list[CALLBACK_TYPE] = [] self._connection_lock = asyncio.Lock() self._event_listeners: list[Callable[[dict[str, Any]], None]] = [] - self._debounced_reload: Debouncer[Coroutine[Any, Any, None]] = Debouncer( - hass, - LOGGER, - cooldown=ENTRY_RELOAD_COOLDOWN, - immediate=False, - function=self._async_reload_entry, - ) - entry.async_on_unload(self._debounced_reload.async_cancel) + entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) ) entry.async_on_unload(entry.add_update_listener(self._async_update_listener)) - async def _async_reload_entry(self) -> None: - """Reload entry.""" - self._debounced_reload.async_cancel() - LOGGER.debug("Reloading entry %s", self.name) - await self.hass.config_entries.async_reload(self.entry.entry_id) - def update_sleep_period(self) -> bool: """Check device sleep period & update if changed.""" if ( not self.device.initialized or not (wakeup_period := get_rpc_device_wakeup_period(self.device.status)) - or wakeup_period == self.entry.data.get(CONF_SLEEP_PERIOD) + or wakeup_period == self.sleep_period ): return False @@ -441,10 +420,10 @@ class ShellyRpcCoordinator(DataUpdateCoordinator[None]): if self.update_sleep_period(): return - if sleep_period := self.entry.data.get(CONF_SLEEP_PERIOD): + if self.sleep_period: # Sleeping device, no point polling it, just mark it unavailable raise UpdateFailed( - f"Sleeping device did not update within {sleep_period} seconds interval" + f"Sleeping device did not update within {self.sleep_period} seconds interval" ) if self.device.connected: return @@ -458,26 +437,11 @@ class ShellyRpcCoordinator(DataUpdateCoordinator[None]): except InvalidAuthError: self.entry.async_start_reauth(self.hass) - @property - def model(self) -> str: - """Model of the device.""" - return cast(str, self.entry.data["model"]) - - @property - def mac(self) -> str: - """Mac address of the device.""" - return cast(str, self.entry.unique_id) - - @property - def sw_version(self) -> str: - """Firmware version of the device.""" - return self.device.firmware_version if self.device.initialized else "" - async def _async_disconnected(self) -> None: """Handle device disconnected.""" - # Sleeping devices send data and disconnects + # Sleeping devices send data and disconnect # There are no disconnect events for sleeping devices - if self.entry.data.get(CONF_SLEEP_PERIOD): + if self.sleep_period: return async with self._connection_lock: @@ -514,7 +478,7 @@ class ShellyRpcCoordinator(DataUpdateCoordinator[None]): This will be executed on connect or when the config entry is updated. """ - if not self.entry.data.get(CONF_SLEEP_PERIOD): + if not self.sleep_period: await self._async_connect_ble_scanner() async def _async_connect_ble_scanner(self) -> None: @@ -546,6 +510,7 @@ class ShellyRpcCoordinator(DataUpdateCoordinator[None]): """Handle device update.""" if update_type is UpdateType.INITIALIZED: self.hass.async_create_task(self._async_connected()) + self.async_set_updated_data(None) elif update_type is UpdateType.DISCONNECTED: self.hass.async_create_task(self._async_disconnected()) elif update_type is UpdateType.STATUS: @@ -555,18 +520,7 @@ class ShellyRpcCoordinator(DataUpdateCoordinator[None]): def async_setup(self) -> None: """Set up the coordinator.""" - dev_reg = device_registry.async_get(self.hass) - entry = dev_reg.async_get_or_create( - config_entry_id=self.entry.entry_id, - name=self.name, - connections={(device_registry.CONNECTION_NETWORK_MAC, self.mac)}, - manufacturer="Shelly", - model=aioshelly.const.MODEL_NAMES.get(self.model, self.model), - sw_version=self.sw_version, - hw_version=f"gen{self.device.gen} ({self.model})", - configuration_url=f"http://{self.entry.data[CONF_HOST]}", - ) - self.device_id = entry.id + super().async_setup() self.device.subscribe_updates(self._async_handle_update) if self.device.initialized: # If we are already initialized, we are connected @@ -585,24 +539,14 @@ class ShellyRpcCoordinator(DataUpdateCoordinator[None]): await self.shutdown() -class ShellyRpcPollingCoordinator(DataUpdateCoordinator): +class ShellyRpcPollingCoordinator(ShellyCoordinatorBase[RpcDevice]): """Polling coordinator for a Shelly RPC based device.""" def __init__( self, hass: HomeAssistant, entry: ConfigEntry, device: RpcDevice ) -> None: """Initialize the RPC polling coordinator.""" - self.device_id: str | None = None - - device_name = get_rpc_device_name(device) if device.initialized else entry.title - super().__init__( - hass, - LOGGER, - name=device_name, - update_interval=timedelta(seconds=RPC_SENSORS_POLLING_INTERVAL), - ) - self.entry = entry - self.device = device + super().__init__(hass, entry, device, RPC_SENSORS_POLLING_INTERVAL) async def _async_update_data(self) -> None: """Fetch data.""" @@ -617,17 +561,12 @@ class ShellyRpcPollingCoordinator(DataUpdateCoordinator): except InvalidAuthError: self.entry.async_start_reauth(self.hass) - @property - def mac(self) -> str: - """Mac address of the device.""" - return cast(str, self.entry.unique_id) - def get_block_coordinator_by_device_id( hass: HomeAssistant, device_id: str ) -> ShellyBlockCoordinator | None: """Get a Shelly block device coordinator for the given device id.""" - dev_reg = device_registry.async_get(hass) + dev_reg = dr_async_get(hass) if device := dev_reg.async_get(device_id): for config_entry in device.config_entries: if not (entry_data := get_entry_data(hass).get(config_entry)): @@ -643,7 +582,7 @@ def get_rpc_coordinator_by_device_id( hass: HomeAssistant, device_id: str ) -> ShellyRpcCoordinator | None: """Get a Shelly RPC device coordinator for the given device id.""" - dev_reg = device_registry.async_get(hass) + dev_reg = dr_async_get(hass) if device := dev_reg.async_get(device_id): for config_entry in device.config_entries: if not (entry_data := get_entry_data(hass).get(config_entry)): @@ -655,14 +594,12 @@ def get_rpc_coordinator_by_device_id( return None -async def async_reconnect_soon( - hass: HomeAssistant, entry: config_entries.ConfigEntry -) -> None: +async def async_reconnect_soon(hass: HomeAssistant, entry: ConfigEntry) -> None: """Try to reconnect soon.""" if ( not entry.data.get(CONF_SLEEP_PERIOD) and not hass.is_stopping - and entry.state == config_entries.ConfigEntryState.LOADED + and entry.state == ConfigEntryState.LOADED and (entry_data := get_entry_data(hass).get(entry.entry_id)) and (coordinator := entry_data.rpc) ): diff --git a/homeassistant/components/shelly/diagnostics.py b/homeassistant/components/shelly/diagnostics.py index f73de5fb32d..0a1fa0e21fc 100644 --- a/homeassistant/components/shelly/diagnostics.py +++ b/homeassistant/components/shelly/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Shelly.""" from __future__ import annotations +from typing import Any + from homeassistant.components.bluetooth import async_scanner_by_source from homeassistant.components.diagnostics import async_redact_data from homeassistant.config_entries import ConfigEntry @@ -15,7 +17,7 @@ TO_REDACT = {CONF_USERNAME, CONF_PASSWORD} async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" shelly_entry_data = get_entry_data(hass)[entry.entry_id] diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index 2ab0af8f18e..4811334b285 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -12,10 +12,14 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import device_registry, entity, entity_registry -from homeassistant.helpers.entity import DeviceInfo, EntityDescription +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity_registry import RegistryEntry +from homeassistant.helpers.entity_registry import ( + RegistryEntry, + async_entries_for_config_entry, + async_get as er_async_get, +) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -115,10 +119,8 @@ def async_restore_block_attribute_entities( """Restore block attributes entities.""" entities = [] - ent_reg = entity_registry.async_get(hass) - entries = entity_registry.async_entries_for_config_entry( - ent_reg, config_entry.entry_id - ) + ent_reg = er_async_get(hass) + entries = async_entries_for_config_entry(ent_reg, config_entry.entry_id) domain = sensor_class.__module__.split(".")[-1] @@ -228,10 +230,8 @@ def async_restore_rpc_attribute_entities( """Restore block attributes entities.""" entities = [] - ent_reg = entity_registry.async_get(hass) - entries = entity_registry.async_entries_for_config_entry( - ent_reg, config_entry.entry_id - ) + ent_reg = er_async_get(hass) + entries = async_entries_for_config_entry(ent_reg, config_entry.entry_id) domain = sensor_class.__module__.split(".")[-1] @@ -321,7 +321,7 @@ class ShellyBlockEntity(CoordinatorEntity[ShellyBlockCoordinator]): self._attr_name = get_block_entity_name(coordinator.device, block) self._attr_should_poll = False self._attr_device_info = DeviceInfo( - connections={(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)} + connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} ) self._attr_unique_id = f"{coordinator.mac}-{block.description}" @@ -363,7 +363,7 @@ class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]): self.key = key self._attr_should_poll = False self._attr_device_info = { - "connections": {(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)} + "connections": {(CONNECTION_NETWORK_MAC, coordinator.mac)} } self._attr_unique_id = f"{coordinator.mac}-{key}" self._attr_name = get_rpc_entity_name(coordinator.device, key) @@ -412,7 +412,7 @@ class ShellyRpcEntity(CoordinatorEntity[ShellyRpcCoordinator]): self.coordinator.entry.async_start_reauth(self.hass) -class ShellyBlockAttributeEntity(ShellyBlockEntity, entity.Entity): +class ShellyBlockAttributeEntity(ShellyBlockEntity, Entity): """Helper class to represent a block attribute.""" entity_description: BlockEntityDescription @@ -482,7 +482,7 @@ class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]): ) self._attr_unique_id = f"{coordinator.mac}-{attribute}" self._attr_device_info = DeviceInfo( - connections={(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)} + connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} ) self._last_value = None @@ -501,7 +501,7 @@ class ShellyRestAttributeEntity(CoordinatorEntity[ShellyBlockCoordinator]): return self._last_value -class ShellyRpcAttributeEntity(ShellyRpcEntity, entity.Entity): +class ShellyRpcAttributeEntity(ShellyRpcEntity, Entity): """Helper class to represent a rpc attribute.""" entity_description: RpcEntityDescription @@ -575,7 +575,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti self._attr_should_poll = False self._attr_device_info = DeviceInfo( - connections={(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)} + connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} ) if block is not None: @@ -658,7 +658,7 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity, RestoreEntity): self._attr_should_poll = False self._attr_device_info = DeviceInfo( - connections={(device_registry.CONNECTION_NETWORK_MAC, coordinator.mac)} + connections={(CONNECTION_NETWORK_MAC, coordinator.mac)} ) self._attr_unique_id = ( self._attr_unique_id diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index b28218c3cfa..ff5de472005 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==5.2.1"], + "requirements": ["aioshelly==5.3.1"], "dependencies": ["bluetooth", "http"], "zeroconf": [ { diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index d13c891e13a..b7b8c3300d3 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -40,7 +40,7 @@ NUMBERS: Final = { ("device", "valvePos"): BlockNumberDescription( key="device|valvepos", icon="mdi:pipe-valve", - name="Valve Position", + name="Valve position", native_unit_of_measurement=PERCENTAGE, available=lambda block: cast(int, block.valveError) != 1, entity_category=EntityCategory.CONFIG, diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 70a29857708..c344e522716 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -20,6 +20,7 @@ from homeassistant.const import ( LIGHT_LUX, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + UnitOfApparentPower, UnitOfElectricCurrent, UnitOfElectricPotential, UnitOfEnergy, @@ -78,7 +79,7 @@ SENSORS: Final = { ), ("device", "deviceTemp"): BlockSensorDescription( key="device|deviceTemp", - name="Device Temperature", + name="Device temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, value=lambda value: round(value, 1), device_class=SensorDeviceClass.TEMPERATURE, @@ -138,7 +139,7 @@ SENSORS: Final = { ), ("emeter", "powerFactor"): BlockSensorDescription( key="emeter|powerFactor", - name="Power Factor", + name="Power factor", native_unit_of_measurement=PERCENTAGE, value=lambda value: round(value * 100, 1), device_class=SensorDeviceClass.POWER_FACTOR, @@ -179,7 +180,7 @@ SENSORS: Final = { ), ("emeter", "energyReturned"): BlockSensorDescription( key="emeter|energyReturned", - name="Energy Returned", + name="Energy returned", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, value=lambda value: round(value / 1000, 2), device_class=SensorDeviceClass.ENERGY, @@ -213,7 +214,7 @@ SENSORS: Final = { ), ("sensor", "concentration"): BlockSensorDescription( key="sensor|concentration", - name="Gas Concentration", + name="Gas concentration", native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, icon="mdi:gauge", state_class=SensorStateClass.MEASUREMENT, @@ -264,7 +265,7 @@ SENSORS: Final = { ), ("relay", "totalWorkTime"): BlockSensorDescription( key="relay|totalWorkTime", - name="Lamp Life", + name="Lamp life", native_unit_of_measurement=PERCENTAGE, icon="mdi:progress-wrench", value=lambda value: round(100 - (value / 3600 / SHAIR_MAX_WORK_HOURS), 1), @@ -318,20 +319,142 @@ RPC_SENSORS: Final = { sub_key="apower", name="Power", native_unit_of_measurement=UnitOfPower.WATT, - value=lambda status, _: round(float(status), 1), device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), + "a_act_power": RpcSensorDescription( + key="em", + sub_key="a_act_power", + name="Phase A active power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + "b_act_power": RpcSensorDescription( + key="em", + sub_key="b_act_power", + name="Phase B active power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + "c_act_power": RpcSensorDescription( + key="em", + sub_key="c_act_power", + name="Phase C active power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + "a_aprt_power": RpcSensorDescription( + key="em", + sub_key="a_aprt_power", + name="Phase A apparent power", + native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, + device_class=SensorDeviceClass.APPARENT_POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + "b_aprt_power": RpcSensorDescription( + key="em", + sub_key="b_aprt_power", + name="Phase B apparent power", + native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, + device_class=SensorDeviceClass.APPARENT_POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + "c_aprt_power": RpcSensorDescription( + key="em", + sub_key="c_aprt_power", + name="Phase C apparent power", + native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE, + device_class=SensorDeviceClass.APPARENT_POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + "a_pf": RpcSensorDescription( + key="em", + sub_key="a_pf", + name="Phase A power factor", + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + ), + "b_pf": RpcSensorDescription( + key="em", + sub_key="b_pf", + name="Phase B power factor", + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + ), + "c_pf": RpcSensorDescription( + key="em", + sub_key="c_pf", + name="Phase C power factor", + device_class=SensorDeviceClass.POWER_FACTOR, + state_class=SensorStateClass.MEASUREMENT, + ), "voltage": RpcSensorDescription( key="switch", sub_key="voltage", name="Voltage", native_unit_of_measurement=UnitOfElectricPotential.VOLT, - value=lambda status, _: round(float(status), 1), + value=lambda status, _: None if status is None else round(float(status), 1), device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, ), + "a_voltage": RpcSensorDescription( + key="em", + sub_key="a_voltage", + name="Phase A voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + "b_voltage": RpcSensorDescription( + key="em", + sub_key="b_voltage", + name="Phase B voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + "c_voltage": RpcSensorDescription( + key="em", + sub_key="c_voltage", + name="Phase C voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + "a_current": RpcSensorDescription( + key="em", + sub_key="a_current", + name="Phase A current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + "b_current": RpcSensorDescription( + key="em", + sub_key="b_current", + name="Phase B current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + "c_current": RpcSensorDescription( + key="em", + sub_key="c_current", + name="Phase C current", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), "energy": RpcSensorDescription( key="switch", sub_key="aenergy", @@ -344,7 +467,7 @@ RPC_SENSORS: Final = { "temperature": RpcSensorDescription( key="switch", sub_key="temperature", - name="Device Temperature", + name="Device temperature", native_unit_of_measurement=UnitOfTemperature.CELSIUS, value=lambda status, _: round(status["tC"], 1), device_class=SensorDeviceClass.TEMPERATURE, @@ -361,7 +484,6 @@ RPC_SENSORS: Final = { value=lambda status, _: round(status, 1), device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - entity_registry_enabled_default=True, ), "rssi": RpcSensorDescription( key="wifi", @@ -392,7 +514,6 @@ RPC_SENSORS: Final = { value=lambda status, _: round(status, 1), device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, - entity_registry_enabled_default=True, ), "battery": RpcSensorDescription( key="devicepower:0", @@ -402,7 +523,6 @@ RPC_SENSORS: Final = { value=lambda status, _: status["percent"], device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, - entity_registry_enabled_default=True, entity_category=EntityCategory.DIAGNOSTIC, ), "voltmeter": RpcSensorDescription( @@ -413,17 +533,15 @@ RPC_SENSORS: Final = { value=lambda status, _: round(float(status), 2), device_class=SensorDeviceClass.VOLTAGE, state_class=SensorStateClass.MEASUREMENT, - entity_registry_enabled_default=True, available=lambda status: status is not None, ), "analoginput": RpcSensorDescription( key="input", sub_key="percent", - name="Analog Input", + name="Analog input", native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, - entity_registry_enabled_default=True, ), } diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 9f67ed0181d..1f05364ca3e 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -71,5 +71,14 @@ "abort": { "ble_unsupported": "Bluetooth support requires firmware version {ble_min_version} or newer." } + }, + "selector": { + "ble_scanner_mode": { + "options": { + "disabled": "Disabled", + "active": "Active", + "passive": "Passive" + } + } } } diff --git a/homeassistant/components/shelly/translations/bg.json b/homeassistant/components/shelly/translations/bg.json index 8e3487284ef..d6c75f4d601 100644 --- a/homeassistant/components/shelly/translations/bg.json +++ b/homeassistant/components/shelly/translations/bg.json @@ -40,5 +40,19 @@ "button3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", "button4": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u0438 \u0431\u0443\u0442\u043e\u043d" } + }, + "options": { + "step": { + "init": { + "description": "Bluetooth \u0441\u043a\u0430\u043d\u0438\u0440\u0430\u043d\u0435\u0442\u043e \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0430\u043a\u0442\u0438\u0432\u043d\u043e \u0438\u043b\u0438 \u043f\u0430\u0441\u0438\u0432\u043d\u043e. \u041a\u043e\u0433\u0430\u0442\u043e \u0435 \u0430\u043a\u0442\u0438\u0432\u043d\u043e, Shelly \u0438\u0441\u043a\u0430 \u0434\u0430\u043d\u043d\u0438 \u043e\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0430\u0431\u043b\u0438\u0437\u043e; \u043a\u043e\u0433\u0430\u0442\u043e \u0435 \u043f\u0430\u0441\u0438\u0432\u043d\u043e, Shelly \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430 \u043d\u0435\u043f\u043e\u0438\u0441\u043a\u0430\u043d\u0438 \u0434\u0430\u043d\u043d\u0438 \u043e\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0430\u0431\u043b\u0438\u0437\u043e." + } + } + }, + "selector": { + "ble_scanner_mode": { + "options": { + "passive": "\u041f\u0430\u0441\u0438\u0432\u043d\u043e" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/ca.json b/homeassistant/components/shelly/translations/ca.json index 2fa312a93af..7ea8202984e 100644 --- a/homeassistant/components/shelly/translations/ca.json +++ b/homeassistant/components/shelly/translations/ca.json @@ -71,5 +71,14 @@ "description": "L'escaneig Bluetooth pot ser actiu o passiu. Si \u00e9s actiu, el Shelly sol\u00b7licita dades a dispositius propers; en passiu, el Shelly rep dades no sol\u00b7licitades de dispositius propers." } } + }, + "selector": { + "ble_scanner_mode": { + "options": { + "active": "Actiu", + "disabled": "Desactivat", + "passive": "Passiu" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json index c0cc533d92d..53f9703a71c 100644 --- a/homeassistant/components/shelly/translations/de.json +++ b/homeassistant/components/shelly/translations/de.json @@ -71,5 +71,14 @@ "description": "Bluetooth-Scannen kann aktiv oder passiv sein. Bei aktiv fordert Shelly Daten von Ger\u00e4ten in der N\u00e4he an; Mit Passiv empf\u00e4ngt Shelly unaufgefordert Daten von Ger\u00e4ten in der N\u00e4he." } } + }, + "selector": { + "ble_scanner_mode": { + "options": { + "active": "Aktiv", + "disabled": "Deaktiviert", + "passive": "Passiv" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json index d6e41c0d118..7b23f7029ff 100644 --- a/homeassistant/components/shelly/translations/en.json +++ b/homeassistant/components/shelly/translations/en.json @@ -71,5 +71,14 @@ "description": "Bluetooth scanning can be active or passive. With active, the Shelly requests data from nearby devices; with passive, the Shelly receives unsolicited data from nearby devices." } } + }, + "selector": { + "ble_scanner_mode": { + "options": { + "active": "Active", + "disabled": "Disabled", + "passive": "Passive" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/et.json b/homeassistant/components/shelly/translations/et.json index 69889526619..5f15c747588 100644 --- a/homeassistant/components/shelly/translations/et.json +++ b/homeassistant/components/shelly/translations/et.json @@ -71,5 +71,14 @@ "description": "Bluetoothi skannimine v\u00f5ib olla aktiivne v\u00f5i passiivne. Aktiivse oleku korral k\u00fcsib Shelly andmeid l\u00e4hedalasuvatest seadmetest, passiivse puhul saab Shelly l\u00e4hedalasuvatest seadmetest soovimatuid andmeid." } } + }, + "selector": { + "ble_scanner_mode": { + "options": { + "active": "Aktiivne", + "disabled": "Keelatud", + "passive": "Passiivne" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/hu.json b/homeassistant/components/shelly/translations/hu.json index 8b7ebcac15e..af0d73849a3 100644 --- a/homeassistant/components/shelly/translations/hu.json +++ b/homeassistant/components/shelly/translations/hu.json @@ -33,7 +33,7 @@ "data": { "host": "C\u00edm" }, - "description": "A be\u00e1ll\u00edt\u00e1s el\u0151tt az akkumul\u00e1toros eszk\u00f6z\u00f6ket fel kell \u00e9breszteni, most egy rajta l\u00e9v\u0151 gombbal fel\u00e9bresztheted az eszk\u00f6zt." + "description": "A be\u00e1ll\u00edt\u00e1s el\u0151tt az akkumul\u00e1toros k\u00e9sz\u00fcl\u00e9keket fel kell \u00e9breszteni, mostant\u00f3l a k\u00e9sz\u00fcl\u00e9ket a rajta l\u00e9v\u0151 gomb seg\u00edts\u00e9g\u00e9vel \u00e9bresztheti fel." } } }, diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json index e2aefb4ac7a..247d4788497 100644 --- a/homeassistant/components/shelly/translations/pl.json +++ b/homeassistant/components/shelly/translations/pl.json @@ -71,5 +71,14 @@ "description": "Skanowanie Bluetooth mo\u017ce by\u0107 aktywne lub pasywne. Gdy jest aktywne, Shelly \u017c\u0105da danych z pobliskich urz\u0105dze\u0144; z pasywnym Shelly otrzymuje niechciane dane z pobliskich urz\u0105dze\u0144." } } + }, + "selector": { + "ble_scanner_mode": { + "options": { + "active": "aktywny", + "disabled": "wy\u0142\u0105czony", + "passive": "pasywny" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/ru.json b/homeassistant/components/shelly/translations/ru.json index 30f28100fa9..3f7b807574e 100644 --- a/homeassistant/components/shelly/translations/ru.json +++ b/homeassistant/components/shelly/translations/ru.json @@ -71,5 +71,14 @@ "description": "\u0421\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 Bluetooth \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0430\u043a\u0442\u0438\u0432\u043d\u044b\u043c \u0438\u043b\u0438 \u043f\u0430\u0441\u0441\u0438\u0432\u043d\u044b\u043c. \u041f\u0440\u0438 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u043c \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438 Shelly \u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u0442 \u0434\u0430\u043d\u043d\u044b\u0435 \u0443 \u0431\u043b\u0438\u0437\u043b\u0435\u0436\u0430\u0449\u0438\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432; \u043f\u0440\u0438 \u043f\u0430\u0441\u0441\u0438\u0432\u043d\u043e\u043c Shelly \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442 \u043d\u0435\u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043e\u0442 \u0431\u043b\u0438\u0437\u043b\u0435\u0436\u0430\u0449\u0438\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432." } } + }, + "selector": { + "ble_scanner_mode": { + "options": { + "active": "\u0410\u043a\u0442\u0438\u0432\u043d\u044b\u0439", + "disabled": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d", + "passive": "\u041f\u0430\u0441\u0441\u0438\u0432\u043d\u044b\u0439" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/sv.json b/homeassistant/components/shelly/translations/sv.json index fb5a480f9e7..365401971e7 100644 --- a/homeassistant/components/shelly/translations/sv.json +++ b/homeassistant/components/shelly/translations/sv.json @@ -47,7 +47,7 @@ }, "trigger_type": { "btn_down": "\"{subtype}\" knappen nedtryckt", - "btn_up": "\"{subtype}\" knappen uppsl\u00e4ppt", + "btn_up": "\"{subtype}\" knappen sl\u00e4ppt", "double": "\"{subtyp}\" dubbelklickad", "double_push": "{subtype} dubbeltryck", "long": "{subtype} l\u00e5ngklickad", @@ -55,7 +55,7 @@ "long_single": "{subtype} l\u00e5ngklickad och sedan enkelklickad", "single": "{subtype} enkelklickad", "single_long": "{subtype} enkelklickad och sedan l\u00e5ngklickad", - "single_push": "{subtyp} enkeltryck", + "single_push": "{subtype} enkeltryck", "triple": "{subtype} trippelklickad" } } diff --git a/homeassistant/components/shelly/translations/tr.json b/homeassistant/components/shelly/translations/tr.json index 435d5a3ec56..b49b1834d00 100644 --- a/homeassistant/components/shelly/translations/tr.json +++ b/homeassistant/components/shelly/translations/tr.json @@ -33,7 +33,7 @@ "data": { "host": "Sunucu" }, - "description": "Kurulumdan \u00f6nce pille \u00e7al\u0131\u015fan cihazlar uyand\u0131r\u0131lmal\u0131d\u0131r, art\u0131k \u00fczerindeki bir d\u00fc\u011fmeyi kullanarak cihaz\u0131 uyand\u0131rabilirsiniz." + "description": "Kurulumdan \u00f6nce pille \u00e7al\u0131\u015fan cihazlar\u0131n uyand\u0131r\u0131lmas\u0131 gerekir, art\u0131k cihaz\u0131 \u00fczerindeki bir d\u00fc\u011fmeyi kullanarak uyand\u0131rabilirsiniz." } } }, @@ -58,5 +58,18 @@ "single_push": "{subtype} tek basma", "triple": "{subtype} \u00fc\u00e7 kez t\u0131kland\u0131" } + }, + "options": { + "abort": { + "ble_unsupported": "Bluetooth deste\u011fi, donan\u0131m yaz\u0131l\u0131m\u0131 s\u00fcr\u00fcm\u00fc {ble_min_version} veya daha yenisini gerektirir." + }, + "step": { + "init": { + "data": { + "ble_scanner_mode": "Bluetooth taray\u0131c\u0131 modu" + }, + "description": "Bluetooth taramas\u0131 aktif veya pasif olabilir. Etkin oldu\u011funda, Shelly yak\u0131ndaki cihazlardan veri ister; pasif ile, Shelly yak\u0131ndaki cihazlardan istenmeyen verileri al\u0131r." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/uk.json b/homeassistant/components/shelly/translations/uk.json index 7ad70b0f0da..00d9f595532 100644 --- a/homeassistant/components/shelly/translations/uk.json +++ b/homeassistant/components/shelly/translations/uk.json @@ -2,6 +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.", + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043d\u0435 \u0432\u0434\u0430\u043b\u0430\u0441\u044f, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0438\u0434\u0430\u043b\u0456\u0442\u044c \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u0442\u0430 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0457\u0457 \u0437\u043d\u043e\u0432\u0443.", "unsupported_firmware": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454 \u043d\u0435\u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0443 \u0432\u0435\u0440\u0441\u0456\u044e \u043c\u0456\u043a\u0440\u043e\u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0438." }, "error": { @@ -20,6 +21,12 @@ "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" } }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442" diff --git a/homeassistant/components/shelly/translations/zh-Hant.json b/homeassistant/components/shelly/translations/zh-Hant.json index e3165f04a09..8f192e8ff70 100644 --- a/homeassistant/components/shelly/translations/zh-Hant.json +++ b/homeassistant/components/shelly/translations/zh-Hant.json @@ -71,5 +71,14 @@ "description": "\u85cd\u82bd\u6383\u63cf\u53ef\u4ee5\u70ba\u4e3b\u52d5\u6216\u88ab\u52d5\u6a21\u5f0f\u3002\u4e3b\u52d5\u6a21\u5f0f\u4e0b\u3001Shelly \u6703\u5411\u5468\u570d\u7684\u88dd\u7f6e\u8acb\u6c42\u8cc7\u6599\uff1b\u88ab\u52d5\u6a21\u5f0f\u4e0b\u3001Shelly \u6703\u7531\u5468\u570d\u7684\u88dd\u7f6e\u63a5\u6536\u5ee3\u64ad\u8cc7\u6599\u3002" } } + }, + "selector": { + "ble_scanner_mode": { + "options": { + "active": "\u4e3b\u52d5", + "disabled": "\u95dc\u9589", + "passive": "\u88ab\u52d5" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index 58bd3b4b8b1..20b3833443c 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -73,7 +73,7 @@ class RestUpdateDescription( REST_UPDATES: Final = { "fwupdate": RestUpdateDescription( - name="Firmware Update", + name="Firmware update", key="fwupdate", latest_version=lambda status: status["update"]["new_version"], beta=False, @@ -82,7 +82,7 @@ REST_UPDATES: Final = { entity_registry_enabled_default=False, ), "fwupdate_beta": RestUpdateDescription( - name="Beta Firmware Update", + name="Beta firmware update", key="fwupdate", latest_version=lambda status: status["update"].get("beta_version"), beta=True, @@ -94,7 +94,7 @@ REST_UPDATES: Final = { RPC_UPDATES: Final = { "fwupdate": RpcUpdateDescription( - name="Firmware Update", + name="Firmware update", key="sys", sub_key="available_updates", latest_version=lambda status: status.get("stable", {"version": ""})["version"], @@ -104,7 +104,7 @@ RPC_UPDATES: Final = { entity_registry_enabled_default=False, ), "fwupdate_beta": RpcUpdateDescription( - name="Beta Firmware Update", + name="Beta firmware update", key="sys", sub_key="available_updates", latest_version=lambda status: status.get("beta", {"version": ""})["version"], diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index b048b219e6b..ea95bb930c7 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -13,7 +13,13 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry, entity_registry, singleton +from homeassistant.helpers import singleton +from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, + async_get as dr_async_get, + format_mac, +) +from homeassistant.helpers.entity_registry import async_get as er_async_get from homeassistant.helpers.typing import EventType from homeassistant.util.dt import utcnow @@ -36,7 +42,7 @@ def async_remove_shelly_entity( hass: HomeAssistant, domain: str, unique_id: str ) -> None: """Remove a Shelly entity.""" - entity_reg = entity_registry.async_get(hass) + entity_reg = er_async_get(hass) entity_id = entity_reg.async_get_entity_id(domain, DOMAIN, unique_id) if entity_id: LOGGER.debug("Removing entity: %s", entity_id) @@ -44,15 +50,23 @@ def async_remove_shelly_entity( def get_block_device_name(device: BlockDevice) -> str: - """Naming for device.""" + """Get Block device name.""" return cast(str, device.settings["name"] or device.settings["device"]["hostname"]) def get_rpc_device_name(device: RpcDevice) -> str: - """Naming for device.""" + """Get RPC device name.""" return cast(str, device.config["sys"]["device"].get("name") or device.hostname) +def get_device_name(device: BlockDevice | RpcDevice) -> str: + """Get device name.""" + if isinstance(device, BlockDevice): + return get_block_device_name(device) + + return get_rpc_device_name(device) + + def get_number_of_channels(device: BlockDevice, block: Block) -> int: """Get number of channels for block type.""" assert isinstance(device.shelly, dict) @@ -84,7 +98,7 @@ def get_block_entity_name( channel_name = get_block_channel_name(device, block) if description: - return f"{channel_name} {description}" + return f"{channel_name} {description.lower()}" return channel_name @@ -312,7 +326,7 @@ def get_rpc_entity_name( channel_name = get_rpc_channel_name(device, key) if description: - return f"{channel_name} {description}" + return f"{channel_name} {description.lower()}" return channel_name @@ -330,12 +344,12 @@ def get_rpc_key_instances(keys_dict: dict[str, Any], key: str) -> list[str]: if key == "switch" and "cover:0" in keys_dict: key = "cover" - return [k for k in keys_dict if k.startswith(key)] + return [k for k in keys_dict if k.startswith(f"{key}:")] def get_rpc_key_ids(keys_dict: dict[str, Any], key: str) -> list[int]: """Return list of key ids for RPC device from a dict.""" - return [int(k.split(":")[1]) for k in keys_dict if k.startswith(key)] + return [int(k.split(":")[1]) for k in keys_dict if k.startswith(f"{key}:")] def is_rpc_momentary_input( @@ -385,19 +399,12 @@ def device_update_info( assert entry.unique_id - dev_registry = device_registry.async_get(hass) - if device := dev_registry.async_get_device( + dev_reg = dr_async_get(hass) + if device := dev_reg.async_get_device( identifiers={(DOMAIN, entry.entry_id)}, - connections={ - ( - device_registry.CONNECTION_NETWORK_MAC, - device_registry.format_mac(entry.unique_id), - ) - }, + connections={(CONNECTION_NETWORK_MAC, format_mac(entry.unique_id))}, ): - dev_registry.async_update_device( - device.id, sw_version=shellydevice.firmware_version - ) + dev_reg.async_update_device(device.id, sw_version=shellydevice.firmware_version) def brightness_to_percentage(brightness: int) -> int: diff --git a/homeassistant/components/sia/strings.json b/homeassistant/components/sia/strings.json index fe648c24e75..e5eb4770db5 100644 --- a/homeassistant/components/sia/strings.json +++ b/homeassistant/components/sia/strings.json @@ -24,6 +24,9 @@ "title": "Add another account to the current port." } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, "error": { "invalid_key_format": "The key is not a hex value, please use only 0-9 and A-F.", "invalid_key_length": "The key is not the right length, it has to be 16, 24 or 32 hex characters.", diff --git a/homeassistant/components/sia/translations/bg.json b/homeassistant/components/sia/translations/bg.json index 20d8511d195..bd3a7d1b36a 100644 --- a/homeassistant/components/sia/translations/bg.json +++ b/homeassistant/components/sia/translations/bg.json @@ -1,5 +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" + }, "error": { "invalid_zones": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0435 \u0434\u0430 \u0438\u043c\u0430 \u043f\u043e\u043d\u0435 1 \u0437\u043e\u043d\u0430.", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" diff --git a/homeassistant/components/sia/translations/ca.json b/homeassistant/components/sia/translations/ca.json index ed51fec34cd..91a9fc8c980 100644 --- a/homeassistant/components/sia/translations/ca.json +++ b/homeassistant/components/sia/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, "error": { "invalid_account_format": "El compte no \u00e9s un valor hexadecimal, utilitza nom\u00e9s 0-9 i A-F.", "invalid_account_length": "El compte no t\u00e9 la longitud correcta, ha de tenir entre 3 i 16 car\u00e0cters.", diff --git a/homeassistant/components/sia/translations/de.json b/homeassistant/components/sia/translations/de.json index 3aa36279ce1..d2785883abf 100644 --- a/homeassistant/components/sia/translations/de.json +++ b/homeassistant/components/sia/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { "invalid_account_format": "Das Konto ist kein Hex-Wert. Bitte verwende nur 0\u20139 und A\u2013F.", "invalid_account_length": "Das Konto hat nicht die richtige L\u00e4nge. Es muss zwischen 3 und 16 Zeichen lang sein.", diff --git a/homeassistant/components/sia/translations/en.json b/homeassistant/components/sia/translations/en.json index 9dea235b379..739d7d3935c 100644 --- a/homeassistant/components/sia/translations/en.json +++ b/homeassistant/components/sia/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Device is already configured" + }, "error": { "invalid_account_format": "The account is not a hex value, please use only 0-9 and A-F.", "invalid_account_length": "The account is not the right length, it has to be between 3 and 16 characters.", diff --git a/homeassistant/components/sia/translations/et.json b/homeassistant/components/sia/translations/et.json index ff1f73d217e..ada3b7c6382 100644 --- a/homeassistant/components/sia/translations/et.json +++ b/homeassistant/components/sia/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, "error": { "invalid_account_format": "Konto ei ole HEX v\u00e4\u00e4rtus, lubatud on ainult 0\u20139 ja A-F.", "invalid_account_length": "Kontonimi ei ole \u00f5ige pikkusega, see peab olema 3\u201316 t\u00e4hem\u00e4rki.", diff --git a/homeassistant/components/sia/translations/no.json b/homeassistant/components/sia/translations/no.json index 7f8cee998e5..9fc7035ca73 100644 --- a/homeassistant/components/sia/translations/no.json +++ b/homeassistant/components/sia/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, "error": { "invalid_account_format": "Kontoen er ikke en hex-verdi. Bruk bare 0-9 og AF.", "invalid_account_length": "Kontoen har ikke riktig lengde, den m\u00e5 v\u00e6re mellom 3 og 16 tegn.", diff --git a/homeassistant/components/sia/translations/ru.json b/homeassistant/components/sia/translations/ru.json index 807a7a80877..413adcc5626 100644 --- a/homeassistant/components/sia/translations/ru.json +++ b/homeassistant/components/sia/translations/ru.json @@ -1,5 +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." + }, "error": { "invalid_account_format": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u0435\u0441\u0442\u043d\u0430\u0434\u0446\u0430\u0442\u0435\u0440\u0438\u0447\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435\u043c, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0442\u043e\u043b\u044c\u043a\u043e 0\u20139 \u0438 AF.", "invalid_account_length": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043d\u0435 \u043f\u043e\u0434\u0445\u043e\u0434\u044f\u0449\u0435\u0439 \u0434\u043b\u0438\u043d\u044b, \u043e\u043d\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0441\u043e\u0434\u0435\u0440\u0436\u0430\u0442\u044c \u043e\u0442 3 \u0434\u043e 16 \u0441\u0438\u043c\u0432\u043e\u043b\u043e\u0432.", diff --git a/homeassistant/components/sia/translations/zh-Hant.json b/homeassistant/components/sia/translations/zh-Hant.json index 310099e4882..9ab0d40e238 100644 --- a/homeassistant/components/sia/translations/zh-Hant.json +++ b/homeassistant/components/sia/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "invalid_account_format": "\u5e33\u865f\u70ba\u5341\u516d\u9032\u4f4d\u6578\u503c\u3001\u8acb\u4f7f\u7528 0-9 \u53ca A-F\u3002", "invalid_account_length": "\u5e33\u865f\u9577\u5ea6\u4e0d\u6b63\u78ba\u3001\u5fc5\u9808\u4ecb\u65bc 3 \u81f3 16 \u500b\u5b57\u5143\u4e4b\u9593\u3002", diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index 042a642429f..18ce667c2dc 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -2,7 +2,7 @@ "domain": "sighthound", "name": "Sighthound", "documentation": "https://www.home-assistant.io/integrations/sighthound", - "requirements": ["pillow==9.3.0", "simplehound==0.3"], + "requirements": ["pillow==9.4.0", "simplehound==0.3"], "codeowners": ["@robmarkcole"], "iot_class": "cloud_polling", "loggers": ["simplehound"] diff --git a/homeassistant/components/simplepush/translations/lv.json b/homeassistant/components/simplepush/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/simplepush/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/simplepush/translations/nl.json b/homeassistant/components/simplepush/translations/nl.json index 176318b3f3c..07d9feb4ea3 100644 --- a/homeassistant/components/simplepush/translations/nl.json +++ b/homeassistant/components/simplepush/translations/nl.json @@ -9,6 +9,7 @@ "step": { "user": { "data": { + "device_key": "De apparaatsleutel van uw apparaat", "name": "Naam" } } diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 7b431368328..6a3523f07c9 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -489,7 +489,7 @@ class SimpliSafe: self.systems: dict[int, SystemType] = {} # This will get filled in by async_init: - self.coordinator: DataUpdateCoordinator | None = None + self.coordinator: DataUpdateCoordinator[None] | None = None @callback def _async_process_new_notifications(self, system: SystemType) -> None: @@ -692,7 +692,7 @@ class SimpliSafe: raise UpdateFailed(f"SimpliSafe error while updating: {result}") -class SimpliSafeEntity(CoordinatorEntity): +class SimpliSafeEntity(CoordinatorEntity[DataUpdateCoordinator[None]]): """Define a base SimpliSafe entity.""" _attr_has_entity_name = True diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 7ae363c3be3..dcfcd6cd9d3 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -156,7 +156,7 @@ class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow): ) -> FlowResult: """Manage the options.""" if user_input is not None: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry(data=user_input) return self.async_show_form( step_id="init", diff --git a/homeassistant/components/simplisafe/translations/el.json b/homeassistant/components/simplisafe/translations/el.json index 896c5bb8909..5e03fe0c619 100644 --- a/homeassistant/components/simplisafe/translations/el.json +++ b/homeassistant/components/simplisafe/translations/el.json @@ -16,7 +16,7 @@ "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": "\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." + "description": "\u03a4\u03bf SimpliSafe \u03c0\u03b9\u03c3\u03c4\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c4\u03bf\u03c5\u03c2 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2 \u03bc\u03ad\u03c3\u03c9 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03b9\u03ba\u03c4\u03c5\u03b1\u03ba\u03ae\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03c4\u03bf\u03c5. \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](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code) \u03c0\u03c1\u03b9\u03bd \u03be\u03b5\u03ba\u03b9\u03bd\u03ae\u03c3\u03b5\u03c4\u03b5.\n\n\u038c\u03c4\u03b1\u03bd \u03b5\u03af\u03c3\u03c4\u03b5 \u03ad\u03c4\u03bf\u03b9\u03bc\u03bf\u03b9, \u03ba\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba [\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. \u0395\u03ac\u03bd \u03ad\u03c7\u03b5\u03c4\u03b5 \u03ae\u03b4\u03b7 \u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03bf SimpliSafe \u03c3\u03c4\u03bf \u03c0\u03c1\u03cc\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03c0\u03b5\u03c1\u03b9\u03ae\u03b3\u03b7\u03c3\u03ae\u03c2 \u03c3\u03b1\u03c2, \u03af\u03c3\u03c9\u03c2 \u03b8\u03b5\u03bb\u03ae\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b1\u03bd\u03bf\u03af\u03be\u03b5\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03bd\u03ad\u03b1 \u03ba\u03b1\u03c1\u03c4\u03ad\u03bb\u03b1 \u03ba\u03b1\u03b9, \u03c3\u03c4\u03b7 \u03c3\u03c5\u03bd\u03ad\u03c7\u03b5\u03b9\u03b1, \u03bd\u03b1 \u03b1\u03bd\u03c4\u03b9\u03b3\u03c1\u03ac\u03c8\u03b5\u03c4\u03b5/\u03b5\u03c0\u03b9\u03ba\u03bf\u03bb\u03bb\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c0\u03b1\u03c1\u03b1\u03c0\u03ac\u03bd\u03c9 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 URL \u03c3\u03b5 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7\u03bd \u03ba\u03b1\u03c1\u03c4\u03ad\u03bb\u03b1.\n\n\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, \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 \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 `com.simplisafe.mobile`." } } }, diff --git a/homeassistant/components/simplisafe/translations/hu.json b/homeassistant/components/simplisafe/translations/hu.json index 1d2f4842c76..01f80c9bbb0 100644 --- a/homeassistant/components/simplisafe/translations/hu.json +++ b/homeassistant/components/simplisafe/translations/hu.json @@ -16,13 +16,13 @@ "data": { "auth_code": "Enged\u00e9lyez\u00e9si k\u00f3d" }, - "description": "A SimpliSafe a webes alkalmaz\u00e1son kereszt\u00fcl hiteles\u00edti a felhaszn\u00e1l\u00f3kat. A technikai korl\u00e1toz\u00e1sok miatt a folyamat v\u00e9g\u00e9n van egy manu\u00e1lis l\u00e9p\u00e9s; k\u00e9rj\u00fck, hogy a kezd\u00e9s el\u0151tt olvassa el a [dokument\u00e1ci\u00f3t](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code).\n\nHa k\u00e9szen \u00e1ll, kattintson [ide]({url}) a SimpliSafe webes alkalmaz\u00e1s megnyit\u00e1s\u00e1hoz \u00e9s adja meg a hiteles\u00edt\u0151 adatokat. Ha m\u00e1r bejelentkezett a SimpliSafe rendszerbe a b\u00f6ng\u00e9sz\u0151ben, akkor \u00e9rdemes egy \u00faj lapot nyitni, majd a fenti URL-t bem\u00e1solni/beilleszteni abba a lapba.\n\nHa a folyamat befejez\u0151d\u00f6tt, t\u00e9rjen vissza ide, \u00e9s adja meg az enged\u00e9lyez\u00e9si k\u00f3dot a `com.simplisafe.mobile` URL-r\u0151l." + "description": "A SimpliSafe a webes alkalmaz\u00e1son kereszt\u00fcl hiteles\u00edti a felhaszn\u00e1l\u00f3kat. A technikai korl\u00e1toz\u00e1sok miatt a folyamat v\u00e9g\u00e9n van egy manu\u00e1lis l\u00e9p\u00e9s; k\u00e9rem, hogy a kezd\u00e9s el\u0151tt olvassa el a [dokument\u00e1ci\u00f3t](http://home-assistant.io/integrations/simplisafe#getting-an-authorization-code).\n\nHa k\u00e9szen \u00e1ll, kattintson [ide]({url}) a SimpliSafe webes alkalmaz\u00e1s megnyit\u00e1s\u00e1hoz \u00e9s adja meg a hiteles\u00edt\u0151 adatokat. Ha m\u00e1r bejelentkezett a SimpliSafe rendszerbe a b\u00f6ng\u00e9sz\u0151ben, akkor \u00e9rdemes egy \u00faj lapot nyitni, majd a fenti URL-t bem\u00e1solni/beilleszteni abba a lapba.\n\nHa a folyamat befejez\u0151d\u00f6tt, t\u00e9rjen vissza ide, \u00e9s adja meg az enged\u00e9lyez\u00e9si k\u00f3dot a `com.simplisafe.mobile` URL-r\u0151l." } } }, "issues": { "deprecated_service": { - "description": "Friss\u00edtsen minden olyan automatiz\u00e1l\u00e1st vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, hogy helyette a(z) `{alternate_service}` szolg\u00e1ltat\u00e1st haszn\u00e1lja a(z) `{alternate_target}` entit\u00e1ssal. Ezut\u00e1n kattintson az al\u00e1bbi MEHET gombra a probl\u00e9ma megoldottk\u00e9nt val\u00f3 megjel\u00f6l\u00e9s\u00e9hez.", + "description": "Friss\u00edtsen minden olyan automatizmust vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, hogy helyette a(z) `{alternate_service}` szolg\u00e1ltat\u00e1st haszn\u00e1lja a(z) `{alternate_target}` entit\u00e1ssal. Ezut\u00e1n kattintson az al\u00e1bbi MEHET gombra a probl\u00e9ma megoldottk\u00e9nt val\u00f3 megjel\u00f6l\u00e9s\u00e9hez.", "title": "A {deprecated_service} szolg\u00e1ltat\u00e1s elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" } }, diff --git a/homeassistant/components/simplisafe/translations/nl.json b/homeassistant/components/simplisafe/translations/nl.json index d7da5fbd4ce..32fd31ee1d5 100644 --- a/homeassistant/components/simplisafe/translations/nl.json +++ b/homeassistant/components/simplisafe/translations/nl.json @@ -5,11 +5,15 @@ "reauth_successful": "Herauthenticatie geslaagd" }, "error": { + "identifier_exists": "Account al geregistreerd", "invalid_auth": "Ongeldige authenticatie", "unknown": "Onverwachte fout" }, "step": { "user": { + "data": { + "auth_code": "Authorisatie Code" + }, "description": "Voer uw gebruikersnaam en wachtwoord in." } } diff --git a/homeassistant/components/simplisafe/typing.py b/homeassistant/components/simplisafe/typing.py index 10f4fadc1c5..d49d356036a 100644 --- a/homeassistant/components/simplisafe/typing.py +++ b/homeassistant/components/simplisafe/typing.py @@ -1,7 +1,5 @@ """Define typing helpers for SimpliSafe.""" -from typing import Union - from simplipy.system.v2 import SystemV2 from simplipy.system.v3 import SystemV3 -SystemType = Union[SystemV2, SystemV3] +SystemType = SystemV2 | SystemV3 diff --git a/homeassistant/components/skybeacon/sensor.py b/homeassistant/components/skybeacon/sensor.py index 0ff6ebd41cd..17bf8a3ab7f 100644 --- a/homeassistant/components/skybeacon/sensor.py +++ b/homeassistant/components/skybeacon/sensor.py @@ -158,7 +158,7 @@ class Monitor(threading.Thread, SensorEntity): ) if SKIP_HANDLE_LOOKUP: # HACK: inject handle mapping collected offline - # pylint: disable=protected-access + # pylint: disable-next=protected-access device._characteristics[UUID(BLE_TEMP_UUID)] = cached_char # Magic: writing this makes device happy device.char_write_handle(0x1B, bytearray([255]), False) diff --git a/homeassistant/components/skybell/translations/hu.json b/homeassistant/components/skybell/translations/hu.json index ca267fab1d9..97ebe79ba4c 100644 --- a/homeassistant/components/skybell/translations/hu.json +++ b/homeassistant/components/skybell/translations/hu.json @@ -14,7 +14,7 @@ "data": { "password": "Jelsz\u00f3" }, - "description": "K\u00e9rj\u00fck, friss\u00edtse jelszav\u00e1t a k\u00f6vetkez\u0151h\u00f6z: {email}", + "description": "K\u00e9rem, friss\u00edtse jelszav\u00e1t a k\u00f6vetkez\u0151h\u00f6z: {email}", "title": "Integr\u00e1ci\u00f3 \u00fajrahiteles\u00edt\u00e9se" }, "user": { diff --git a/homeassistant/components/skybell/translations/uk.json b/homeassistant/components/skybell/translations/uk.json index 19744315085..3177207db0d 100644 --- a/homeassistant/components/skybell/translations/uk.json +++ b/homeassistant/components/skybell/translations/uk.json @@ -1,6 +1,11 @@ { "config": { "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + }, "user": { "data": { "email": "Email", diff --git a/homeassistant/components/slack/translations/uk.json b/homeassistant/components/slack/translations/uk.json new file mode 100644 index 00000000000..673b7d2572b --- /dev/null +++ b/homeassistant/components/slack/translations/uk.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "data_description": { + "icon": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043e\u0434\u0438\u043d \u0456\u0437 \u0435\u043c\u043e\u0434\u0437\u0456 Slack \u044f\u043a \u0437\u043d\u0430\u0447\u043e\u043a \u0434\u043b\u044f \u043d\u0430\u0434\u0430\u043d\u043e\u0433\u043e \u0456\u043c\u0435\u043d\u0456 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430.", + "username": "Home Assistant \u0431\u0443\u0434\u0435 \u043f\u0443\u0431\u043b\u0456\u043a\u0443\u0432\u0430\u0442\u0438 \u0432 Slack \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u044e\u0447\u0438 \u0432\u043a\u0430\u0437\u0430\u043d\u0435 \u0456\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sleepiq/binary_sensor.py b/homeassistant/components/sleepiq/binary_sensor.py index b176320e671..e137edb29ce 100644 --- a/homeassistant/components/sleepiq/binary_sensor.py +++ b/homeassistant/components/sleepiq/binary_sensor.py @@ -8,10 +8,9 @@ from homeassistant.components.binary_sensor import ( 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 DOMAIN, ICON_EMPTY, ICON_OCCUPIED, IS_IN_BED -from .coordinator import SleepIQData +from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator from .entity import SleepIQSleeperEntity @@ -29,14 +28,16 @@ async def async_setup_entry( ) -class IsInBedBinarySensor(SleepIQSleeperEntity, BinarySensorEntity): +class IsInBedBinarySensor( + SleepIQSleeperEntity[SleepIQDataUpdateCoordinator], BinarySensorEntity +): """Implementation of a SleepIQ presence sensor.""" _attr_device_class = BinarySensorDeviceClass.OCCUPANCY def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: SleepIQDataUpdateCoordinator, bed: SleepIQBed, sleeper: SleepIQSleeper, ) -> None: diff --git a/homeassistant/components/sleepiq/entity.py b/homeassistant/components/sleepiq/entity.py index c73988ce638..d4ca2c894da 100644 --- a/homeassistant/components/sleepiq/entity.py +++ b/homeassistant/components/sleepiq/entity.py @@ -1,17 +1,21 @@ """Entity for the SleepIQ integration.""" from abc import abstractmethod +from typing import TypeVar 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 homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ENTITY_TYPES, ICON_OCCUPIED +from .coordinator import SleepIQDataUpdateCoordinator, SleepIQPauseUpdateCoordinator + +_SleepIQCoordinatorT = TypeVar( + "_SleepIQCoordinatorT", + bound=SleepIQDataUpdateCoordinator | SleepIQPauseUpdateCoordinator, +) def device_from_bed(bed: SleepIQBed) -> DeviceInfo: @@ -33,14 +37,14 @@ class SleepIQEntity(Entity): self._attr_device_info = device_from_bed(bed) -class SleepIQBedEntity(CoordinatorEntity): +class SleepIQBedEntity(CoordinatorEntity[_SleepIQCoordinatorT]): """Implementation of a SleepIQ sensor.""" _attr_icon = ICON_OCCUPIED def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: _SleepIQCoordinatorT, bed: SleepIQBed, ) -> None: """Initialize the SleepIQ sensor entity.""" @@ -61,14 +65,14 @@ class SleepIQBedEntity(CoordinatorEntity): """Update sensor attributes.""" -class SleepIQSleeperEntity(SleepIQBedEntity): +class SleepIQSleeperEntity(SleepIQBedEntity[_SleepIQCoordinatorT]): """Implementation of a SleepIQ sensor.""" _attr_icon = ICON_OCCUPIED def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: _SleepIQCoordinatorT, bed: SleepIQBed, sleeper: SleepIQSleeper, name: str, diff --git a/homeassistant/components/sleepiq/light.py b/homeassistant/components/sleepiq/light.py index e0b98a37362..e684d383b40 100644 --- a/homeassistant/components/sleepiq/light.py +++ b/homeassistant/components/sleepiq/light.py @@ -8,10 +8,9 @@ from homeassistant.components.light import ColorMode, LightEntity 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 DOMAIN -from .coordinator import SleepIQData +from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator from .entity import SleepIQBedEntity _LOGGER = logging.getLogger(__name__) @@ -31,14 +30,17 @@ async def async_setup_entry( ) -class SleepIQLightEntity(SleepIQBedEntity, LightEntity): +class SleepIQLightEntity(SleepIQBedEntity[SleepIQDataUpdateCoordinator], LightEntity): """Representation of a light.""" _attr_color_mode = ColorMode.ONOFF _attr_supported_color_modes = {ColorMode.ONOFF} def __init__( - self, coordinator: DataUpdateCoordinator, bed: SleepIQBed, light: SleepIQLight + self, + coordinator: SleepIQDataUpdateCoordinator, + bed: SleepIQBed, + light: SleepIQLight, ) -> None: """Initialize the light.""" self.light = light diff --git a/homeassistant/components/sleepiq/number.py b/homeassistant/components/sleepiq/number.py index 1f8ef80a1f1..14af25019b8 100644 --- a/homeassistant/components/sleepiq/number.py +++ b/homeassistant/components/sleepiq/number.py @@ -11,10 +11,9 @@ from homeassistant.components.number import NumberEntity, NumberEntityDescriptio 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 ACTUATOR, DOMAIN, ENTITY_TYPES, FIRMNESS, ICON_OCCUPIED -from .coordinator import SleepIQData +from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator from .entity import SleepIQBedEntity @@ -130,7 +129,7 @@ async def async_setup_entry( async_add_entities(entities) -class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity): +class SleepIQNumberEntity(SleepIQBedEntity[SleepIQDataUpdateCoordinator], NumberEntity): """Representation of a SleepIQ number entity.""" entity_description: SleepIQNumberEntityDescription @@ -138,7 +137,7 @@ class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: SleepIQDataUpdateCoordinator, bed: SleepIQBed, device: Any, description: SleepIQNumberEntityDescription, diff --git a/homeassistant/components/sleepiq/select.py b/homeassistant/components/sleepiq/select.py index 0cbf1671e2b..184e57541c6 100644 --- a/homeassistant/components/sleepiq/select.py +++ b/homeassistant/components/sleepiq/select.py @@ -7,10 +7,9 @@ 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 homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN -from .coordinator import SleepIQData +from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator from .entity import SleepIQBedEntity @@ -28,13 +27,16 @@ async def async_setup_entry( ) -class SleepIQSelectEntity(SleepIQBedEntity, SelectEntity): +class SleepIQSelectEntity(SleepIQBedEntity[SleepIQDataUpdateCoordinator], SelectEntity): """Representation of a SleepIQ select entity.""" _attr_options = list(BED_PRESETS) def __init__( - self, coordinator: DataUpdateCoordinator, bed: SleepIQBed, preset: SleepIQPreset + self, + coordinator: SleepIQDataUpdateCoordinator, + bed: SleepIQBed, + preset: SleepIQPreset, ) -> None: """Initialize the select entity.""" self.preset = preset diff --git a/homeassistant/components/sleepiq/sensor.py b/homeassistant/components/sleepiq/sensor.py index 71618dab056..c463c80224e 100644 --- a/homeassistant/components/sleepiq/sensor.py +++ b/homeassistant/components/sleepiq/sensor.py @@ -7,10 +7,9 @@ from homeassistant.components.sensor import SensorEntity, SensorStateClass 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 DOMAIN, PRESSURE, SLEEP_NUMBER -from .coordinator import SleepIQData +from .coordinator import SleepIQData, SleepIQDataUpdateCoordinator from .entity import SleepIQSleeperEntity SENSORS = [PRESSURE, SLEEP_NUMBER] @@ -31,14 +30,16 @@ async def async_setup_entry( ) -class SleepIQSensorEntity(SleepIQSleeperEntity, SensorEntity): +class SleepIQSensorEntity( + SleepIQSleeperEntity[SleepIQDataUpdateCoordinator], SensorEntity +): """Representation of an SleepIQ Entity with CoordinatorEntity.""" _attr_icon = "mdi:bed" def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: SleepIQDataUpdateCoordinator, bed: SleepIQBed, sleeper: SleepIQSleeper, sensor_type: str, diff --git a/homeassistant/components/sleepiq/switch.py b/homeassistant/components/sleepiq/switch.py index ebc0f720b43..62ad72d9db4 100644 --- a/homeassistant/components/sleepiq/switch.py +++ b/homeassistant/components/sleepiq/switch.py @@ -28,7 +28,9 @@ async def async_setup_entry( ) -class SleepNumberPrivateSwitch(SleepIQBedEntity, SwitchEntity): +class SleepNumberPrivateSwitch( + SleepIQBedEntity[SleepIQPauseUpdateCoordinator], SwitchEntity +): """Representation of SleepIQ privacy mode.""" def __init__( diff --git a/homeassistant/components/sleepiq/translations/uk.json b/homeassistant/components/sleepiq/translations/uk.json new file mode 100644 index 00000000000..d62cea42c81 --- /dev/null +++ b/homeassistant/components/sleepiq/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/slimproto/translations/pl.json b/homeassistant/components/slimproto/translations/pl.json index 96fba53c20f..44bce8fbf38 100644 --- a/homeassistant/components/slimproto/translations/pl.json +++ b/homeassistant/components/slimproto/translations/pl.json @@ -2,6 +2,14 @@ "config": { "abort": { "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "step": { + "user": { + "few": "Pustych", + "many": "Pustych", + "one": "Pusty", + "other": "Puste" + } } } } \ No newline at end of file diff --git a/homeassistant/components/sma/translations/lv.json b/homeassistant/components/sma/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/sma/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/lt.json b/homeassistant/components/smart_meter_texas/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/el.json b/homeassistant/components/smartthings/translations/el.json index ea4c841cd13..08fe66ede9d 100644 --- a/homeassistant/components/smartthings/translations/el.json +++ b/homeassistant/components/smartthings/translations/el.json @@ -5,7 +5,7 @@ "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.", + "app_setup_error": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 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.", diff --git a/homeassistant/components/smartthings/translations/he.json b/homeassistant/components/smartthings/translations/he.json index b73162134be..8ded45279ce 100644 --- a/homeassistant/components/smartthings/translations/he.json +++ b/homeassistant/components/smartthings/translations/he.json @@ -2,10 +2,10 @@ "config": { "abort": { "invalid_webhook_url": "\u05ea\u05e6\u05d5\u05e8\u05ea Home Assistant \u05d0\u05d9\u05e0\u05d4 \u05de\u05d5\u05d2\u05d3\u05e8\u05ea \u05db\u05e8\u05d0\u05d5\u05d9 \u05dc\u05e7\u05d1\u05dc\u05ea \u05e2\u05d3\u05db\u05d5\u05e0\u05d9\u05dd \u05de-SmartThings. \u05db\u05ea\u05d5\u05d1\u05ea \u05d4\u05d0\u05ea\u05e8 \u05e9\u05dc webhook \u05d0\u05d9\u05e0\u05d4 \u05d7\u05d5\u05e7\u05d9\u05ea:\n> {webhook_url}\n\n\u05e0\u05d0 \u05dc\u05e2\u05d3\u05db\u05df \u05d0\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05dc\u05e4\u05d9 [\u05d4\u05d4\u05d5\u05e8\u05d0\u05d5\u05ea]({component_url}), \u05dc\u05d0\u05d7\u05e8 \u05de\u05db\u05df \u05dc\u05d4\u05e4\u05e2\u05d9\u05dc \u05de\u05d7\u05d3\u05e9 \u05d0\u05ea Home Assistant \u05d5\u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05d5\u05d1.", - "no_available_locations": "\u05d0\u05d9\u05df \u05de\u05d9\u05e7\u05d5\u05de\u05d9 SmartThings \u05d6\u05de\u05d9\u05e0\u05d9\u05dd \u05dc\u05d4\u05ea\u05e7\u05e0\u05d4 \u05d1-Home Assistant." + "no_available_locations": "\u05d0\u05d9\u05df \u05de\u05d9\u05e7\u05d5\u05de\u05d9 SmartThings \u05d6\u05de\u05d9\u05e0\u05d9\u05dd \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4 \u05d1-Home Assistant." }, "error": { - "app_setup_error": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea SmartApp. \u05e0\u05d0 \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.", + "app_setup_error": "\u05d0\u05d9\u05df \u05d0\u05e4\u05e9\u05e8\u05d5\u05ea \u05dc\u05d4\u05d2\u05d3\u05d9\u05e8 \u05d0\u05ea SmartApp. \u05e0\u05d0 \u05dc\u05e0\u05e1\u05d5\u05ea \u05e9\u05d5\u05d1.", "token_forbidden": "\u05dc\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d0\u05d9\u05df \u05d0\u05ea \u05d8\u05d5\u05d5\u05d7\u05d9 OAuth \u05d4\u05d3\u05e8\u05d5\u05e9\u05d9\u05dd.", "token_invalid_format": "\u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d7\u05d9\u05d9\u05d1 \u05dc\u05d4\u05d9\u05d5\u05ea \u05d1\u05e4\u05d5\u05e8\u05de\u05d8 UID / GUID", "token_unauthorized": "\u05d4\u05d0\u05e1\u05d9\u05de\u05d5\u05df \u05d0\u05d9\u05e0\u05d5 \u05d7\u05d5\u05e7\u05d9 \u05d0\u05d5 \u05d0\u05d9\u05e0\u05d5 \u05de\u05d5\u05e8\u05e9\u05d4 \u05e2\u05d5\u05d3.", diff --git a/homeassistant/components/smartthings/translations/tr.json b/homeassistant/components/smartthings/translations/tr.json index 83293cc5a03..fab73a52c39 100644 --- a/homeassistant/components/smartthings/translations/tr.json +++ b/homeassistant/components/smartthings/translations/tr.json @@ -2,10 +2,10 @@ "config": { "abort": { "invalid_webhook_url": "Home Assistant, SmartThings'ten g\u00fcncellemeleri almak i\u00e7in do\u011fru \u015fekilde yap\u0131land\u0131r\u0131lmam\u0131\u015f. Webhook URL'si ge\u00e7ersiz:\n > {webhook_url} \n\n L\u00fctfen yap\u0131land\u0131rman\u0131z\u0131 [talimatlara]( {component_url} ) g\u00f6re g\u00fcncelleyin, Home Assistant'\u0131 yeniden ba\u015flat\u0131n ve tekrar deneyin.", - "no_available_locations": "Home Assistant'ta kurulacak kullan\u0131labilir SmartThings Locations yok." + "no_available_locations": "Home Assistant'ta kurulabilecek SmartThings Konumu yok." }, "error": { - "app_setup_error": "SmartApp kurulamad\u0131. L\u00fctfen tekrar deneyin.", + "app_setup_error": "SmartApp ayarlanam\u0131yor. L\u00fctfen tekrar deneyin.", "token_forbidden": "Anahtar, gerekli OAuth kapsam\u0131na sahip de\u011fil.", "token_invalid_format": "Anahtar UID/GUID bi\u00e7iminde olmal\u0131d\u0131r", "token_unauthorized": "Anahtar art\u0131k ge\u00e7ersiz veya yetkilendirilmemi\u015f.", diff --git a/homeassistant/components/smarttub/translations/uk.json b/homeassistant/components/smarttub/translations/uk.json new file mode 100644 index 00000000000..ed251640012 --- /dev/null +++ b/homeassistant/components/smarttub/translations/uk.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f SmartTub \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0454 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 \u0432\u0430\u0448\u043e\u0433\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/snooz/const.py b/homeassistant/components/snooz/const.py index 9ce16b80e05..7a39ed49105 100644 --- a/homeassistant/components/snooz/const.py +++ b/homeassistant/components/snooz/const.py @@ -4,3 +4,11 @@ from homeassistant.const import Platform DOMAIN = "snooz" PLATFORMS: list[Platform] = [Platform.FAN] + +SERVICE_TRANSITION_ON = "transition_on" +SERVICE_TRANSITION_OFF = "transition_off" + +ATTR_VOLUME = "volume" +ATTR_DURATION = "duration" + +DEFAULT_TRANSITION_DURATION = 20 diff --git a/homeassistant/components/snooz/fan.py b/homeassistant/components/snooz/fan.py index d8c8f54d7bb..a34989d1a03 100644 --- a/homeassistant/components/snooz/fan.py +++ b/homeassistant/components/snooz/fan.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Callable +from datetime import timedelta from typing import Any from pysnooz.api import UnknownSnoozState @@ -12,16 +13,25 @@ from pysnooz.commands import ( turn_off, turn_on, ) +import voluptuous as vol from homeassistant.components.fan import ATTR_PERCENTAGE, FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_platform from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity -from .const import DOMAIN +from .const import ( + ATTR_DURATION, + ATTR_VOLUME, + DEFAULT_TRANSITION_DURATION, + DOMAIN, + SERVICE_TRANSITION_OFF, + SERVICE_TRANSITION_ON, +) from .models import SnoozConfigurationData @@ -30,6 +40,29 @@ async def async_setup_entry( ) -> None: """Set up Snooz device from a config entry.""" + platform = entity_platform.async_get_current_platform() + platform.async_register_entity_service( + SERVICE_TRANSITION_ON, + { + vol.Optional(ATTR_VOLUME): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ), + vol.Optional(ATTR_DURATION, default=DEFAULT_TRANSITION_DURATION): vol.All( + vol.Coerce(int), vol.Range(min=1, max=300) + ), + }, + "async_transition_on", + ) + platform.async_register_entity_service( + SERVICE_TRANSITION_OFF, + { + vol.Optional(ATTR_DURATION, default=DEFAULT_TRANSITION_DURATION): vol.All( + vol.Coerce(int), vol.Range(min=1, max=300) + ), + }, + "async_transition_off", + ) + data: SnoozConfigurationData = hass.data[DOMAIN][entry.entry_id] async_add_entities([SnoozFan(data)]) @@ -108,6 +141,18 @@ class SnoozFan(FanEntity, RestoreEntity): set_volume(percentage) if percentage > 0 else turn_off() ) + async def async_transition_on(self, duration: int, **kwargs: Any) -> None: + """Transition on the device.""" + await self._async_execute_command( + turn_on(volume=kwargs.get("volume"), duration=timedelta(seconds=duration)) + ) + + async def async_transition_off(self, duration: int, **kwargs: Any) -> None: + """Transition off the device.""" + await self._async_execute_command( + turn_off(duration=timedelta(seconds=duration)) + ) + async def _async_execute_command(self, command: SnoozCommandData) -> None: result = await self._device.async_execute_command(command) diff --git a/homeassistant/components/snooz/manifest.json b/homeassistant/components/snooz/manifest.json index 91185bcd5b2..56e75e0046e 100644 --- a/homeassistant/components/snooz/manifest.json +++ b/homeassistant/components/snooz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/snooz", "requirements": ["pysnooz==0.8.3"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@AustinBrunkhorst"], "bluetooth": [ { diff --git a/homeassistant/components/snooz/services.yaml b/homeassistant/components/snooz/services.yaml new file mode 100644 index 00000000000..f795edf213a --- /dev/null +++ b/homeassistant/components/snooz/services.yaml @@ -0,0 +1,43 @@ +transition_on: + name: Transition on + description: Transition to a target volume level over time. + target: + entity: + integration: snooz + domain: fan + fields: + duration: + name: Transition duration + description: Time it takes to reach the target volume level. + selector: + number: + min: 1 + max: 300 + unit_of_measurement: seconds + mode: box + volume: + name: Target volume + description: If not specified, the volume level is read from the device. + selector: + number: + min: 1 + max: 100 + unit_of_measurement: "%" + +transition_off: + name: Transition off + description: Transition volume off over time. + target: + entity: + integration: snooz + domain: fan + fields: + duration: + name: Transition duration + description: Time it takes to turn off. + selector: + number: + min: 1 + max: 300 + unit_of_measurement: seconds + mode: box diff --git a/homeassistant/components/snooz/translations/lv.json b/homeassistant/components/snooz/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/snooz/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/snooz/translations/tr.json b/homeassistant/components/snooz/translations/tr.json index 8b4d9cc646c..6c3e99a2af1 100644 --- a/homeassistant/components/snooz/translations/tr.json +++ b/homeassistant/components/snooz/translations/tr.json @@ -11,7 +11,7 @@ }, "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "pairing_timeout": { "description": "Cihaz e\u015fle\u015ftirme moduna girmedi. Tekrar denemek i\u00e7in G\u00f6nder'i t\u0131klay\u0131n. \n\n ### Sorun giderme\n 1. Cihaz\u0131n mobil uygulamaya ba\u011fl\u0131 olmad\u0131\u011f\u0131n\u0131 kontrol edin.\n 2. Ayg\u0131t\u0131n fi\u015fini 5 saniyeli\u011fine \u00e7ekin, ard\u0131ndan tekrar tak\u0131n." @@ -20,7 +20,7 @@ "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/solaredge/coordinator.py b/homeassistant/components/solaredge/coordinator.py index 839f2cf37eb..4938a54ed65 100644 --- a/homeassistant/components/solaredge/coordinator.py +++ b/homeassistant/components/solaredge/coordinator.py @@ -24,7 +24,7 @@ from .const import ( class SolarEdgeDataService(ABC): """Get and update the latest data.""" - coordinator: DataUpdateCoordinator + coordinator: DataUpdateCoordinator[None] def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: """Initialize the data object.""" diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index a27c180e0b9..3a4b5ad90c2 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -9,7 +9,10 @@ from homeassistant.components.sensor import SensorDeviceClass, SensorEntity 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 homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) from .const import CONF_SITE_ID, DATA_API_CLIENT, DOMAIN, SENSOR_TYPES from .coordinator import ( @@ -108,7 +111,9 @@ class SolarEdgeSensorFactory: return sensor_class(self.platform_name, sensor_type, service) -class SolarEdgeSensorEntity(CoordinatorEntity, SensorEntity): +class SolarEdgeSensorEntity( + CoordinatorEntity[DataUpdateCoordinator[None]], SensorEntity +): """Abstract class for a solaredge sensor.""" entity_description: SolarEdgeSensorEntityDescription diff --git a/homeassistant/components/solaredge/translations/lv.json b/homeassistant/components/solaredge/translations/lv.json new file mode 100644 index 00000000000..862ef1ca431 --- /dev/null +++ b/homeassistant/components/solaredge/translations/lv.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/translations/lv.json b/homeassistant/components/solarlog/translations/lv.json new file mode 100644 index 00000000000..862ef1ca431 --- /dev/null +++ b/homeassistant/components/solarlog/translations/lv.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, + "error": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solax/strings.json b/homeassistant/components/solax/strings.json index e73c9f3bc88..75d0a8d87db 100644 --- a/homeassistant/components/solax/strings.json +++ b/homeassistant/components/solax/strings.json @@ -9,6 +9,9 @@ } } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" diff --git a/homeassistant/components/solax/translations/bg.json b/homeassistant/components/solax/translations/bg.json index d6778a65bc7..4ba26d39640 100644 --- a/homeassistant/components/solax/translations/bg.json +++ b/homeassistant/components/solax/translations/bg.json @@ -1,5 +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" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" diff --git a/homeassistant/components/solax/translations/ca.json b/homeassistant/components/solax/translations/ca.json index 7f7ce67da39..8ca994c25ba 100644 --- a/homeassistant/components/solax/translations/ca.json +++ b/homeassistant/components/solax/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "unknown": "Error inesperat" diff --git a/homeassistant/components/solax/translations/de.json b/homeassistant/components/solax/translations/de.json index 45c80923777..ab9f0132a67 100644 --- a/homeassistant/components/solax/translations/de.json +++ b/homeassistant/components/solax/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" diff --git a/homeassistant/components/solax/translations/en.json b/homeassistant/components/solax/translations/en.json index ec20dc22d44..36e4aca055a 100644 --- a/homeassistant/components/solax/translations/en.json +++ b/homeassistant/components/solax/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Device is already configured" + }, "error": { "cannot_connect": "Failed to connect", "unknown": "Unexpected error" diff --git a/homeassistant/components/solax/translations/et.json b/homeassistant/components/solax/translations/et.json index e89f90a6119..fdff6606721 100644 --- a/homeassistant/components/solax/translations/et.json +++ b/homeassistant/components/solax/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, "error": { "cannot_connect": "\u00dchendamine nurjus", "unknown": "Ootamatu t\u00f5rge" diff --git a/homeassistant/components/solax/translations/no.json b/homeassistant/components/solax/translations/no.json index 8e82b60bce1..26db3719095 100644 --- a/homeassistant/components/solax/translations/no.json +++ b/homeassistant/components/solax/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, "error": { "cannot_connect": "Tilkobling mislyktes", "unknown": "Uventet feil" diff --git a/homeassistant/components/solax/translations/ru.json b/homeassistant/components/solax/translations/ru.json index c05b4b56bc4..d5859083ec4 100644 --- a/homeassistant/components/solax/translations/ru.json +++ b/homeassistant/components/solax/translations/ru.json @@ -1,5 +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." + }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." diff --git a/homeassistant/components/solax/translations/zh-Hant.json b/homeassistant/components/solax/translations/zh-Hant.json index 7896b5796ed..d148f35560e 100644 --- a/homeassistant/components/solax/translations/zh-Hant.json +++ b/homeassistant/components/solax/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index 7172704c796..09576f07e6b 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -54,9 +54,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Soma from a config entry.""" hass.data[DOMAIN] = {} - hass.data[DOMAIN][API] = SomaApi(entry.data[HOST], entry.data[PORT]) - devices = await hass.async_add_executor_job(hass.data[DOMAIN][API].list_devices) - hass.data[DOMAIN][DEVICES] = devices["shades"] + api = await hass.async_add_executor_job(SomaApi, entry.data[HOST], entry.data[PORT]) + devices = await hass.async_add_executor_job(api.list_devices) + hass.data[DOMAIN] = {API: api, DEVICES: devices["shades"]} await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/soma/config_flow.py b/homeassistant/components/soma/config_flow.py index b696d583c04..a29b1b9bf9b 100644 --- a/homeassistant/components/soma/config_flow.py +++ b/homeassistant/components/soma/config_flow.py @@ -37,7 +37,13 @@ class SomaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_creation(self, user_input=None): """Finish config flow.""" - api = SomaApi(user_input["host"], user_input["port"]) + try: + api = await self.hass.async_add_executor_job( + SomaApi, user_input["host"], user_input["port"] + ) + except RequestException: + _LOGGER.error("Connection to SOMA Connect failed with RequestException") + return self.async_abort(reason="connection_error") try: result = await self.hass.async_add_executor_job(api.list_devices) _LOGGER.info("Successfully set up Soma Connect") diff --git a/homeassistant/components/soma/manifest.json b/homeassistant/components/soma/manifest.json index 39029199c29..b9fd5ef45f6 100644 --- a/homeassistant/components/soma/manifest.json +++ b/homeassistant/components/soma/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/soma", "codeowners": ["@ratsept", "@sebfortier2288"], - "requirements": ["pysoma==0.0.10"], + "requirements": ["pysoma==0.0.12"], "iot_class": "local_polling", "loggers": ["api"] } diff --git a/homeassistant/components/somfy_mylink/translations/lv.json b/homeassistant/components/somfy_mylink/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/coordinator.py b/homeassistant/components/sonarr/coordinator.py index 9b9a06b15f8..1010c196c21 100644 --- a/homeassistant/components/sonarr/coordinator.py +++ b/homeassistant/components/sonarr/coordinator.py @@ -2,7 +2,7 @@ from __future__ import annotations from datetime import timedelta -from typing import TypeVar, Union, cast +from typing import TypeVar, cast from aiopyarr import ( Command, @@ -27,15 +27,15 @@ from .const import CONF_UPCOMING_DAYS, CONF_WANTED_MAX_ITEMS, DOMAIN, LOGGER SonarrDataT = TypeVar( "SonarrDataT", - bound=Union[ - list[SonarrCalendar], - list[Command], - list[Diskspace], - SonarrQueue, - list[SonarrSeries], - SystemStatus, - SonarrWantedMissing, - ], + bound=( + list[SonarrCalendar] + | list[Command] + | list[Diskspace] + | SonarrQueue + | list[SonarrSeries] + | SystemStatus + | SonarrWantedMissing + ), ) diff --git a/homeassistant/components/songpal/translations/lv.json b/homeassistant/components/songpal/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/songpal/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index 2f003e4bde9..f8047ba371f 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -8,12 +8,14 @@ import datetime from functools import partial import logging import socket -from typing import TYPE_CHECKING, Any, Optional, cast +from typing import TYPE_CHECKING, Any, cast from urllib.parse import urlparse -from soco import events_asyncio +from requests.exceptions import Timeout +from soco import events_asyncio, zonegroupstate import soco.config as soco_config from soco.core import SoCo +from soco.events_base import Event as SonosEvent, SubscriptionBase from soco.exceptions import SoCoException import voluptuous as vol @@ -24,8 +26,8 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOSTS, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, device_registry as dr -from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send -from homeassistant.helpers.event import async_track_time_interval, call_later +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_call_later, async_track_time_interval from homeassistant.helpers.typing import ConfigType from .alarms import SonosAlarms @@ -40,6 +42,7 @@ from .const import ( SONOS_REBOOTED, SONOS_SPEAKER_ACTIVITY, SONOS_VANISHED, + SUBSCRIPTION_TIMEOUT, UPNP_ST, ) from .exception import SonosUpdateError @@ -51,7 +54,7 @@ _LOGGER = logging.getLogger(__name__) CONF_ADVERTISE_ADDR = "advertise_addr" CONF_INTERFACE_ADDR = "interface_addr" DISCOVERY_IGNORED_MODELS = ["Sonos Boost"] - +ZGS_SUBSCRIPTION_TIMEOUT = 2 CONFIG_SCHEMA = vol.Schema( { @@ -122,6 +125,8 @@ 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 + soco_config.ZGT_EVENT_FALLBACK = False + zonegroupstate.EVENT_CACHE_TIMEOUT = SUBSCRIPTION_TIMEOUT if DATA_SONOS not in hass.data: hass.data[DATA_SONOS] = SonosData() @@ -172,6 +177,7 @@ class SonosDiscoveryManager: self.data = data self.hosts = set(hosts) self.discovery_lock = asyncio.Lock() + self.creation_lock = asyncio.Lock() self._known_invisible: set[SoCo] = set() self._manual_config_required = bool(hosts) @@ -184,21 +190,70 @@ class SonosDiscoveryManager: """Check if device at provided IP is known to be invisible.""" return any(x for x in self._known_invisible if x.ip_address == ip_address) - def _create_visible_speakers(self, ip_address: str) -> None: - """Create all visible SonosSpeaker instances with the provided seed IP.""" - try: - soco = SoCo(ip_address) + async def async_subscribe_to_zone_updates(self, ip_address: str) -> None: + """Test subscriptions and create SonosSpeakers based on results.""" + soco = SoCo(ip_address) + # Cache now to avoid household ID lookup during first ZoneGroupState processing + await self.hass.async_add_executor_job( + getattr, + soco, + "household_id", + ) + sub = await soco.zoneGroupTopology.subscribe() + + @callback + def _async_add_visible_zones(subscription_succeeded: bool = False) -> None: + """Determine visible zones and create SonosSpeaker instances.""" + zones_to_add = set() + subscription = None + if subscription_succeeded: + subscription = sub + visible_zones = soco.visible_zones self._known_invisible = soco.all_zones - visible_zones - except (OSError, SoCoException) as ex: - _LOGGER.warning( - "Failed to request visible zones from %s: %s", ip_address, ex - ) - return + for zone in visible_zones: + if zone.uid not in self.data.discovered: + zones_to_add.add(zone) - for zone in visible_zones: - if zone.uid not in self.data.discovered: - self._add_speaker(zone) + if not zones_to_add: + return + + self.hass.async_create_task( + self.async_add_speakers(zones_to_add, subscription, soco.uid) + ) + + async def async_subscription_failed(now: datetime.datetime) -> None: + """Fallback logic if the subscription callback never arrives.""" + _LOGGER.warning( + "Subscription to %s failed, attempting to poll directly", ip_address + ) + try: + await sub.unsubscribe() + await self.hass.async_add_executor_job(soco.zone_group_state.poll, soco) + except (OSError, SoCoException, Timeout) as ex: + _LOGGER.warning( + "Fallback pollling to %s failed, setup cannot continue: %s", + ip_address, + ex, + ) + return + _LOGGER.debug("Fallback ZoneGroupState poll to %s succeeded", ip_address) + _async_add_visible_zones() + + cancel_failure_callback = async_call_later( + self.hass, ZGS_SUBSCRIPTION_TIMEOUT, async_subscription_failed + ) + + @callback + def _async_subscription_succeeded(event: SonosEvent) -> None: + """Create SonosSpeakers when subscription callbacks successfully arrive.""" + _LOGGER.debug("Subscription to %s succeeded", ip_address) + cancel_failure_callback() + _async_add_visible_zones(subscription_succeeded=True) + + sub.callback = _async_subscription_succeeded + # Hold lock to prevent concurrent subscription attempts + await asyncio.sleep(ZGS_SUBSCRIPTION_TIMEOUT * 2) async def _async_stop_event_listener(self, event: Event | None = None) -> None: for speaker in self.data.discovered.values(): @@ -227,14 +282,35 @@ class SonosDiscoveryManager: self.data.hosts_heartbeat() self.data.hosts_heartbeat = None - def _add_speaker(self, soco: SoCo) -> None: + async def async_add_speakers( + self, + socos: set[SoCo], + zgs_subscription: SubscriptionBase | None, + zgs_subscription_uid: str | None, + ) -> None: + """Create and set up new SonosSpeaker instances.""" + + def _add_speakers(): + """Add all speakers in a single executor job.""" + for soco in socos: + sub = None + if soco.uid == zgs_subscription_uid and zgs_subscription: + sub = zgs_subscription + self._add_speaker(soco, sub) + + async with self.creation_lock: + await self.hass.async_add_executor_job(_add_speakers) + + def _add_speaker( + self, soco: SoCo, zone_group_state_sub: SubscriptionBase | None + ) -> None: """Create and set up a new SonosSpeaker instance.""" try: speaker_info = soco.get_speaker_info(True, timeout=7) if soco.uid not in self.data.boot_counts: self.data.boot_counts[soco.uid] = soco.boot_seqnum _LOGGER.debug("Adding new speaker: %s", speaker_info) - speaker = SonosSpeaker(self.hass, soco, speaker_info) + speaker = SonosSpeaker(self.hass, soco, speaker_info, zone_group_state_sub) self.data.discovered[soco.uid] = speaker for coordinator, coord_dict in ( (SonosAlarms, self.data.alarms), @@ -247,18 +323,32 @@ class SonosDiscoveryManager: new_coordinator.setup(soco) coord_dict[soco.household_id] = new_coordinator speaker.setup(self.entry) - except (OSError, SoCoException): - _LOGGER.warning("Failed to add SonosSpeaker using %s", soco, exc_info=True) + except (OSError, SoCoException, Timeout) as ex: + _LOGGER.warning("Failed to add SonosSpeaker using %s: %s", soco, ex) - def _poll_manual_hosts(self, now: datetime.datetime | None = None) -> None: + async def async_poll_manual_hosts( + self, now: datetime.datetime | None = None + ) -> None: """Add and maintain Sonos devices from a manual configuration.""" + + def get_sync_attributes(soco: SoCo) -> set[SoCo]: + """Ensure I/O attributes are cached and return visible zones.""" + _ = soco.household_id + _ = soco.uid + return soco.visible_zones + for host in self.hosts: ip_addr = socket.gethostbyname(host) soco = SoCo(ip_addr) try: - visible_zones = soco.visible_zones - except OSError: - _LOGGER.warning("Could not get visible Sonos devices from %s", ip_addr) + visible_zones = await self.hass.async_add_executor_job( + get_sync_attributes, + soco, + ) + except (OSError, SoCoException, Timeout) as ex: + _LOGGER.warning( + "Could not get visible Sonos devices from %s: %s", ip_addr, ex + ) else: if new_hosts := { x.ip_address @@ -267,7 +357,7 @@ class SonosDiscoveryManager: }: _LOGGER.debug("Adding to manual hosts: %s", new_hosts) self.hosts.update(new_hosts) - dispatcher_send( + async_dispatcher_send( self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{soco.uid}", "manual zone scan", @@ -290,7 +380,9 @@ class SonosDiscoveryManager: None, ) if not known_speaker: - self._create_visible_speakers(ip_addr) + await self._async_handle_discovery_message( + soco.uid, ip_addr, "manual zone scan" + ) elif not known_speaker.available: try: known_speaker.ping() @@ -299,33 +391,32 @@ class SonosDiscoveryManager: "Manual poll to %s failed, keeping unavailable", ip_addr ) - self.data.hosts_heartbeat = call_later( - self.hass, DISCOVERY_INTERVAL.total_seconds(), self._poll_manual_hosts + self.data.hosts_heartbeat = async_call_later( + self.hass, DISCOVERY_INTERVAL.total_seconds(), self.async_poll_manual_hosts ) async def _async_handle_discovery_message( - self, uid: str, discovered_ip: str, boot_seqnum: int | None + self, + uid: str, + discovered_ip: str, + source: str, + boot_seqnum: int | None = None, ) -> None: """Handle discovered player creation and activity.""" async with self.discovery_lock: if not self.data.discovered: # Initial discovery, attempt to add all visible zones - await self.hass.async_add_executor_job( - self._create_visible_speakers, - discovered_ip, - ) + await self.async_subscribe_to_zone_updates(discovered_ip) elif uid not in self.data.discovered: if self.is_device_invisible(discovered_ip): return - await self.hass.async_add_executor_job( - self._add_speaker, SoCo(discovered_ip) - ) + await self.async_subscribe_to_zone_updates(discovered_ip) elif boot_seqnum and boot_seqnum > self.data.boot_counts[uid]: self.data.boot_counts[uid] = boot_seqnum async_dispatcher_send(self.hass, f"{SONOS_REBOOTED}-{uid}") else: async_dispatcher_send( - self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{uid}", "discovery" + self.hass, f"{SONOS_SPEAKER_ACTIVITY}-{uid}", source ) async def _async_ssdp_discovered_player( @@ -389,7 +480,10 @@ class SonosDiscoveryManager: self.data.discovery_known.add(uid) asyncio.create_task( self._async_handle_discovery_message( - uid, discovered_ip, cast(Optional[int], boot_seqnum) + uid, + discovered_ip, + "discovery", + boot_seqnum=cast(int | None, boot_seqnum), ) ) @@ -408,7 +502,7 @@ class SonosDiscoveryManager: EVENT_HOMEASSISTANT_STOP, self._stop_manual_heartbeat ) ) - await self.hass.async_add_executor_job(self._poll_manual_hosts) + await self.async_poll_manual_hosts() self.entry.async_on_unload( await ssdp.async_register_callback( diff --git a/homeassistant/components/sonos/diagnostics.py b/homeassistant/components/sonos/diagnostics.py index fda96b86215..96ffeb1df2a 100644 --- a/homeassistant/components/sonos/diagnostics.py +++ b/homeassistant/components/sonos/diagnostics.py @@ -65,17 +65,17 @@ async def async_get_config_entry_diagnostics( async def async_get_device_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry -) -> dict[str, Any] | None: +) -> dict[str, Any]: """Return diagnostics for a device.""" uid = next( (identifier[1] for identifier in device.identifiers if identifier[0] == DOMAIN), None, ) if uid is None: - return None + return {} if (speaker := hass.data[DATA_SONOS].discovered.get(uid)) is None: - return None + return {} return await async_generate_speaker_info(hass, speaker) diff --git a/homeassistant/components/sonos/helpers.py b/homeassistant/components/sonos/helpers.py index 552d104786e..f44775e2f31 100644 --- a/homeassistant/components/sonos/helpers.py +++ b/homeassistant/components/sonos/helpers.py @@ -3,11 +3,11 @@ from __future__ import annotations from collections.abc import Callable import logging -from typing import TYPE_CHECKING, Any, TypeVar, overload +from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, overload +from requests.exceptions import Timeout from soco import SoCo from soco.exceptions import SoCoException, SoCoUPnPException -from typing_extensions import Concatenate, ParamSpec from homeassistant.helpers.dispatcher import dispatcher_send @@ -65,7 +65,7 @@ def soco_error( args_soco = next((arg for arg in args if isinstance(arg, SoCo)), None) try: result = funct(self, *args, **kwargs) - except (OSError, SoCoException, SoCoUPnPException) as err: + except (OSError, SoCoException, SoCoUPnPException, Timeout) as err: error_code = getattr(err, "error_code", None) function = funct.__qualname__ if errorcodes and error_code in errorcodes: @@ -114,9 +114,9 @@ def _find_target_identifier(instance: Any, fallback_soco: SoCo | None) -> str | def hostname_to_uid(hostname: str) -> str: """Convert a Sonos hostname to a uid.""" if hostname.startswith("Sonos-"): - baseuid = hostname.split("-")[1].replace(".local.", "") + baseuid = hostname.removeprefix("Sonos-").replace(".local.", "") elif hostname.startswith("sonos"): - baseuid = hostname[5:].replace(".local.", "") + baseuid = hostname.removeprefix("sonos").replace(".local.", "") else: raise ValueError(f"{hostname} is not a sonos device.") return f"{UID_PREFIX}{baseuid}{UID_POSTFIX}" diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 57438d1864a..73ad2a46c5f 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.28.1"], + "requirements": ["soco==0.29.0"], "dependencies": ["ssdp"], "after_dependencies": ["plex", "spotify", "zeroconf", "media_source"], "zeroconf": ["_sonos._tcp.local."], diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index bd50a090175..90b72a663aa 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -21,6 +21,7 @@ from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_ENQUEUE, BrowseMedia, + MediaPlayerDeviceClass, MediaPlayerEnqueue, MediaPlayerEntity, MediaPlayerEntityFeature, @@ -205,6 +206,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): | MediaPlayerEntityFeature.VOLUME_SET ) _attr_media_content_type = MediaType.MUSIC + _attr_device_class = MediaPlayerDeviceClass.SPEAKER def __init__(self, speaker: SonosSpeaker) -> None: """Initialize the media player entity.""" diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 5460230b66f..b1dfac7beed 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -69,14 +69,14 @@ EVENT_CHARGING = { "CHARGING": True, "NOT_CHARGING": False, } -SUBSCRIPTION_SERVICES = [ +SUBSCRIPTION_SERVICES = { "alarmClock", "avTransport", "contentDirectory", "deviceProperties", "renderingControl", "zoneGroupTopology", -] +} SUPPORTED_VANISH_REASONS = ("sleeping", "switch to bluetooth", "upgrade") UNUSED_DEVICE_KEYS = ["SPID", "TargetRoomName"] @@ -88,7 +88,11 @@ class SonosSpeaker: """Representation of a Sonos speaker.""" def __init__( - self, hass: HomeAssistant, soco: SoCo, speaker_info: dict[str, Any] + self, + hass: HomeAssistant, + soco: SoCo, + speaker_info: dict[str, Any], + zone_group_state_sub: SubscriptionBase | None, ) -> None: """Initialize a SonosSpeaker.""" self.hass = hass @@ -112,6 +116,9 @@ class SonosSpeaker: # Subscriptions and events self.subscriptions_failed: bool = False self._subscriptions: list[SubscriptionBase] = [] + if zone_group_state_sub: + zone_group_state_sub.callback = self.async_dispatch_event + self._subscriptions.append(zone_group_state_sub) self._subscription_lock: asyncio.Lock | None = None self._event_dispatchers: dict[str, Callable] = {} self._last_activity: float = NEVER_TIME @@ -289,6 +296,12 @@ class SonosSpeaker: addr, port = self._subscriptions[0].event_listener.address return ":".join([addr, str(port)]) + @property + def missing_subscriptions(self) -> set[str]: + """Return a list of missing service subscriptions.""" + subscribed_services = {sub.service.service_type for sub in self._subscriptions} + return SUBSCRIPTION_SERVICES - subscribed_services + # # Subscription handling and event dispatchers # @@ -321,8 +334,6 @@ class SonosSpeaker: self._subscription_lock = asyncio.Lock() async with self._subscription_lock: - if self._subscriptions: - return try: await self._async_subscribe() except SonosSubscriptionsFailed: @@ -331,12 +342,14 @@ class SonosSpeaker: async def _async_subscribe(self) -> None: """Create event subscriptions.""" - _LOGGER.debug("Creating subscriptions for %s", self.zone_name) - subscriptions = [ self._subscribe(getattr(self.soco, service), self.async_dispatch_event) - for service in SUBSCRIPTION_SERVICES + for service in self.missing_subscriptions ] + if not subscriptions: + return + + _LOGGER.debug("Creating subscriptions for %s", self.zone_name) results = await asyncio.gather(*subscriptions, return_exceptions=True) for result in results: self.log_subscription_result( diff --git a/homeassistant/components/soundtouch/__init__.py b/homeassistant/components/soundtouch/__init__.py index 69e0eef687e..f3fa221db7f 100644 --- a/homeassistant/components/soundtouch/__init__.py +++ b/homeassistant/components/soundtouch/__init__.py @@ -131,7 +131,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SoundTouchData(device) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/soundtouch/translations/lv.json b/homeassistant/components/soundtouch/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/soundtouch/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soundtouch/translations/nl.json b/homeassistant/components/soundtouch/translations/nl.json index 8328756da76..7bf30dd5253 100644 --- a/homeassistant/components/soundtouch/translations/nl.json +++ b/homeassistant/components/soundtouch/translations/nl.json @@ -11,6 +11,9 @@ "data": { "host": "Host" } + }, + "zeroconf_confirm": { + "title": "Bevestig het toevoegen van Bose SoundTouch-apparaat" } } }, diff --git a/homeassistant/components/speedtestdotnet/translations/ar.json b/homeassistant/components/speedtestdotnet/translations/ar.json index 943182c47a4..71105373e1f 100644 --- a/homeassistant/components/speedtestdotnet/translations/ar.json +++ b/homeassistant/components/speedtestdotnet/translations/ar.json @@ -5,14 +5,5 @@ "description": "\u0647\u0644 \u0623\u0646\u062a \u0645\u062a\u0623\u0643\u062f \u0645\u0646 \u0623\u0646\u0643 \u062a\u0631\u064a\u062f \u0625\u0639\u062f\u0627\u062f SpeedTest\u061f" } } - }, - "options": { - "step": { - "init": { - "data": { - "manual": "\u062a\u0639\u0637\u064a\u0644 \u0627\u0644\u062a\u062d\u062f\u064a\u062b \u0627\u0644\u062a\u0644\u0642\u0627\u0626\u064a" - } - } - } } } \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/ca.json b/homeassistant/components/speedtestdotnet/translations/ca.json index c1c5dda71cf..2e0fbc817e2 100644 --- a/homeassistant/components/speedtestdotnet/translations/ca.json +++ b/homeassistant/components/speedtestdotnet/translations/ca.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Desactiva l'actualitzaci\u00f3 autom\u00e0tica", - "scan_interval": "Freq\u00fc\u00e8ncia d'actualitzaci\u00f3 (minuts)", "server_name": "Seleccion el servidor de proves" } } diff --git a/homeassistant/components/speedtestdotnet/translations/cs.json b/homeassistant/components/speedtestdotnet/translations/cs.json index 22ad2f23322..1fab12319c3 100644 --- a/homeassistant/components/speedtestdotnet/translations/cs.json +++ b/homeassistant/components/speedtestdotnet/translations/cs.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Zak\u00e1zat automatickou aktualizaci", - "scan_interval": "Frekvence aktualizac\u00ed (v minut\u00e1ch)", "server_name": "Vyberte testovac\u00ed server" } } diff --git a/homeassistant/components/speedtestdotnet/translations/de.json b/homeassistant/components/speedtestdotnet/translations/de.json index 81910cb9c70..6afdc9bdc01 100644 --- a/homeassistant/components/speedtestdotnet/translations/de.json +++ b/homeassistant/components/speedtestdotnet/translations/de.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Automatische Updates deaktivieren", - "scan_interval": "Aktualisierungsfrequenz (Minuten)", "server_name": "Testserver ausw\u00e4hlen" } } diff --git a/homeassistant/components/speedtestdotnet/translations/el.json b/homeassistant/components/speedtestdotnet/translations/el.json index 25b5e23ab69..150c8fcd9e0 100644 --- a/homeassistant/components/speedtestdotnet/translations/el.json +++ b/homeassistant/components/speedtestdotnet/translations/el.json @@ -13,8 +13,6 @@ "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", - "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/en.json b/homeassistant/components/speedtestdotnet/translations/en.json index 8b487f5fa1e..53d9a78c311 100644 --- a/homeassistant/components/speedtestdotnet/translations/en.json +++ b/homeassistant/components/speedtestdotnet/translations/en.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Disable auto update", - "scan_interval": "Update frequency (minutes)", "server_name": "Select test server" } } diff --git a/homeassistant/components/speedtestdotnet/translations/es.json b/homeassistant/components/speedtestdotnet/translations/es.json index 9ba5fcbd4bb..8c8cdca193a 100644 --- a/homeassistant/components/speedtestdotnet/translations/es.json +++ b/homeassistant/components/speedtestdotnet/translations/es.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Desactivar actualizaci\u00f3n autom\u00e1tica", - "scan_interval": "Frecuencia de actualizaci\u00f3n (minutos)", "server_name": "Selecciona el servidor de prueba" } } diff --git a/homeassistant/components/speedtestdotnet/translations/et.json b/homeassistant/components/speedtestdotnet/translations/et.json index ac1915e760a..d157473df94 100644 --- a/homeassistant/components/speedtestdotnet/translations/et.json +++ b/homeassistant/components/speedtestdotnet/translations/et.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Keela automaatne v\u00e4rskendamine", - "scan_interval": "Uuendamise sagedus (minutites)", "server_name": "Testiserveri valimine" } } diff --git a/homeassistant/components/speedtestdotnet/translations/fr.json b/homeassistant/components/speedtestdotnet/translations/fr.json index d2efebd0eb1..33d637ba268 100644 --- a/homeassistant/components/speedtestdotnet/translations/fr.json +++ b/homeassistant/components/speedtestdotnet/translations/fr.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "D\u00e9sactiver la mise \u00e0 jour automatique", - "scan_interval": "Fr\u00e9quence de mise \u00e0 jour (minutes)", "server_name": "S\u00e9lectionner le serveur de test" } } diff --git a/homeassistant/components/speedtestdotnet/translations/hu.json b/homeassistant/components/speedtestdotnet/translations/hu.json index c223e8b9376..97a26cc6a78 100644 --- a/homeassistant/components/speedtestdotnet/translations/hu.json +++ b/homeassistant/components/speedtestdotnet/translations/hu.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Automatikus friss\u00edt\u00e9s letilt\u00e1sa", - "scan_interval": "Friss\u00edt\u00e9si gyakoris\u00e1g (perc)", "server_name": "V\u00e1lassza ki a teszt szervert" } } diff --git a/homeassistant/components/speedtestdotnet/translations/id.json b/homeassistant/components/speedtestdotnet/translations/id.json index f609c3d384a..4658151e91b 100644 --- a/homeassistant/components/speedtestdotnet/translations/id.json +++ b/homeassistant/components/speedtestdotnet/translations/id.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Nonaktifkan pembaruan otomatis", - "scan_interval": "Frekuensi pembaruan (menit)", "server_name": "Pilih server uji" } } diff --git a/homeassistant/components/speedtestdotnet/translations/it.json b/homeassistant/components/speedtestdotnet/translations/it.json index 84d0a311de9..f8da1920124 100644 --- a/homeassistant/components/speedtestdotnet/translations/it.json +++ b/homeassistant/components/speedtestdotnet/translations/it.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Disabilita l'aggiornamento automatico", - "scan_interval": "Frequenza di aggiornamento (minuti)", "server_name": "Seleziona il server di prova" } } diff --git a/homeassistant/components/speedtestdotnet/translations/ja.json b/homeassistant/components/speedtestdotnet/translations/ja.json index 40f592b2c46..91aa0103384 100644 --- a/homeassistant/components/speedtestdotnet/translations/ja.json +++ b/homeassistant/components/speedtestdotnet/translations/ja.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "\u81ea\u52d5\u66f4\u65b0\u3092\u7121\u52b9\u306b\u3059\u308b", - "scan_interval": "\u66f4\u65b0\u983b\u5ea6(\u5206)", "server_name": "\u30c6\u30b9\u30c8\u30b5\u30fc\u30d0\u30fc\u306e\u9078\u629e" } } diff --git a/homeassistant/components/speedtestdotnet/translations/ko.json b/homeassistant/components/speedtestdotnet/translations/ko.json index dbef7c3b7d3..ccc349162a5 100644 --- a/homeassistant/components/speedtestdotnet/translations/ko.json +++ b/homeassistant/components/speedtestdotnet/translations/ko.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "\uc790\ub3d9 \uc5c5\ub370\uc774\ud2b8 \ube44\ud65c\uc131\ud654", - "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4 (\ubd84)", "server_name": "\ud14c\uc2a4\ud2b8 \uc11c\ubc84 \uc120\ud0dd" } } diff --git a/homeassistant/components/speedtestdotnet/translations/lb.json b/homeassistant/components/speedtestdotnet/translations/lb.json index 4007faf71e7..dfcd72535e4 100644 --- a/homeassistant/components/speedtestdotnet/translations/lb.json +++ b/homeassistant/components/speedtestdotnet/translations/lb.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Auto Update deaktiv\u00e9ieren", - "scan_interval": "Intervalle vun de Mise \u00e0 jour (Minutten)", "server_name": "Test Server auswielen" } } diff --git a/homeassistant/components/speedtestdotnet/translations/nl.json b/homeassistant/components/speedtestdotnet/translations/nl.json index b5af5dbc78d..0e373e2a9f8 100644 --- a/homeassistant/components/speedtestdotnet/translations/nl.json +++ b/homeassistant/components/speedtestdotnet/translations/nl.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Automatische updaten uitschakelen", - "scan_interval": "Update frequentie (minuten)", "server_name": "Selecteer testserver" } } diff --git a/homeassistant/components/speedtestdotnet/translations/no.json b/homeassistant/components/speedtestdotnet/translations/no.json index 01909d39f06..97d2e57fb38 100644 --- a/homeassistant/components/speedtestdotnet/translations/no.json +++ b/homeassistant/components/speedtestdotnet/translations/no.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Deaktiver automatisk oppdatering", - "scan_interval": "Oppdateringsfrekvens (minutter)", "server_name": "Velg testserver" } } diff --git a/homeassistant/components/speedtestdotnet/translations/pl.json b/homeassistant/components/speedtestdotnet/translations/pl.json index b06d7cdd285..feab54cf734 100644 --- a/homeassistant/components/speedtestdotnet/translations/pl.json +++ b/homeassistant/components/speedtestdotnet/translations/pl.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Wy\u0142\u0105cz automatyczne aktualizacje", - "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (w minutach)", "server_name": "Wybierz serwer" } } diff --git a/homeassistant/components/speedtestdotnet/translations/pt-BR.json b/homeassistant/components/speedtestdotnet/translations/pt-BR.json index 739b3b41875..197450b1351 100644 --- a/homeassistant/components/speedtestdotnet/translations/pt-BR.json +++ b/homeassistant/components/speedtestdotnet/translations/pt-BR.json @@ -13,8 +13,6 @@ "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" } } diff --git a/homeassistant/components/speedtestdotnet/translations/ru.json b/homeassistant/components/speedtestdotnet/translations/ru.json index 3ffcd10bf31..2d347eaf088 100644 --- a/homeassistant/components/speedtestdotnet/translations/ru.json +++ b/homeassistant/components/speedtestdotnet/translations/ru.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435", - "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)", "server_name": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f" } } diff --git a/homeassistant/components/speedtestdotnet/translations/sk.json b/homeassistant/components/speedtestdotnet/translations/sk.json index 6356d6c6428..5186d6634cd 100644 --- a/homeassistant/components/speedtestdotnet/translations/sk.json +++ b/homeassistant/components/speedtestdotnet/translations/sk.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Zak\u00e1za\u0165 automatick\u00fa aktualiz\u00e1ciu", - "scan_interval": "Frekvencia aktualiz\u00e1cie (min\u00faty)", "server_name": "Vyberte testovac\u00ed server" } } diff --git a/homeassistant/components/speedtestdotnet/translations/sv.json b/homeassistant/components/speedtestdotnet/translations/sv.json index cde9095fba8..e3c85a5566c 100644 --- a/homeassistant/components/speedtestdotnet/translations/sv.json +++ b/homeassistant/components/speedtestdotnet/translations/sv.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Inaktivera automatisk uppdatering", - "scan_interval": "Uppdateringsfrekvens (minuter)", "server_name": "V\u00e4lj testserver" } } diff --git a/homeassistant/components/speedtestdotnet/translations/tr.json b/homeassistant/components/speedtestdotnet/translations/tr.json index 5becafdf153..ca4a1e167b6 100644 --- a/homeassistant/components/speedtestdotnet/translations/tr.json +++ b/homeassistant/components/speedtestdotnet/translations/tr.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "Otomatik g\u00fcncellemeyi devre d\u0131\u015f\u0131 b\u0131rak\u0131n", - "scan_interval": "G\u00fcncelleme s\u0131kl\u0131\u011f\u0131 (dakika)", "server_name": "Test sunucusunu se\u00e7in" } } diff --git a/homeassistant/components/speedtestdotnet/translations/uk.json b/homeassistant/components/speedtestdotnet/translations/uk.json index 0456d600e59..be9f4403a29 100644 --- a/homeassistant/components/speedtestdotnet/translations/uk.json +++ b/homeassistant/components/speedtestdotnet/translations/uk.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f", - "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0443 \u0445\u0432\u0438\u043b\u0438\u043d\u0430\u0445)", "server_name": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0443\u0432\u0430\u043d\u043d\u044f" } } diff --git a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json index 43d30d4aeb8..78ef4a1254e 100644 --- a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json +++ b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json @@ -13,8 +13,6 @@ "step": { "init": { "data": { - "manual": "\u95dc\u9589\u81ea\u52d5\u66f4\u65b0", - "scan_interval": "\u66f4\u65b0\u983b\u7387\uff08\u5206\u9418\uff09", "server_name": "\u9078\u64c7\u6e2c\u8a66\u4f3a\u670d\u5668" } } diff --git a/homeassistant/components/spider/translations/lt.json b/homeassistant/components/spider/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/spider/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 94be47bed7e..cb6484c5e3e 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -49,7 +49,7 @@ class HomeAssistantSpotifyData: client: Spotify current_user: dict[str, Any] - devices: DataUpdateCoordinator + devices: DataUpdateCoordinator[list[dict[str, Any]]] session: OAuth2Session diff --git a/homeassistant/components/spotify/browse_media.py b/homeassistant/components/spotify/browse_media.py index 12268c8ab56..0f72d459b68 100644 --- a/homeassistant/components/spotify/browse_media.py +++ b/homeassistant/components/spotify/browse_media.py @@ -216,7 +216,7 @@ async def async_browse_media_internal( # Strip prefix if media_content_type: - media_content_type = media_content_type[len(MEDIA_PLAYER_PREFIX) :] + media_content_type = media_content_type.removeprefix(MEDIA_PLAYER_PREFIX) payload = { "media_content_type": media_content_type, diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 79aedc4c866..64bc3ac3e47 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -2,7 +2,7 @@ "domain": "spotify", "name": "Spotify", "documentation": "https://www.home-assistant.io/integrations/spotify", - "requirements": ["spotipy==2.22.0"], + "requirements": ["spotipy==2.22.1"], "zeroconf": ["_spotify-connect._tcp.local."], "dependencies": ["application_credentials"], "codeowners": ["@frenck"], diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index d09b2795e88..1145686efe7 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -300,8 +300,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): @spotify_exception_handler def play_media(self, media_type: str, media_id: str, **kwargs: Any) -> None: """Play media.""" - if media_type.startswith(MEDIA_PLAYER_PREFIX): - media_type = media_type[len(MEDIA_PLAYER_PREFIX) :] + media_type = media_type.removeprefix(MEDIA_PLAYER_PREFIX) kwargs = {} diff --git a/homeassistant/components/spotify/util.py b/homeassistant/components/spotify/util.py index 7f7f682fb9e..e9af305a1d0 100644 --- a/homeassistant/components/spotify/util.py +++ b/homeassistant/components/spotify/util.py @@ -15,7 +15,7 @@ def is_spotify_media_type(media_content_type: str) -> bool: def resolve_spotify_media_type(media_content_type: str) -> str: """Return actual spotify media_content_type.""" - return media_content_type[len(MEDIA_PLAYER_PREFIX) :] + return media_content_type.removeprefix(MEDIA_PLAYER_PREFIX) def fetch_image_url(item: dict[str, Any], key="images") -> str | None: diff --git a/homeassistant/components/sql/__init__.py b/homeassistant/components/sql/__init__.py index 63f81c784be..bba49c415f8 100644 --- a/homeassistant/components/sql/__init__.py +++ b/homeassistant/components/sql/__init__.py @@ -1,10 +1,48 @@ """The sql component.""" from __future__ import annotations -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +import voluptuous as vol -from .const import PLATFORMS +from homeassistant.components.recorder import CONF_DB_URL +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_NAME, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, + Platform, +) +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_COLUMN_NAME, CONF_QUERY, DOMAIN, PLATFORMS + + +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") + return value + + +QUERY_SCHEMA = vol.Schema( + { + vol.Required(CONF_COLUMN_NAME): cv.string, + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_QUERY): vol.All(cv.string, validate_sql_select), + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_VALUE_TEMPLATE): cv.template, + vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_DB_URL): cv.string, + } +) + +CONFIG_SCHEMA = vol.Schema( + {vol.Optional(DOMAIN): vol.All(cv.ensure_list, [QUERY_SCHEMA])}, + extra=vol.ALLOW_EXTRA, +) async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: @@ -12,6 +50,19 @@ async def async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None await hass.config_entries.async_reload(entry.entry_id) +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up SQL from yaml config.""" + if (conf := config.get(DOMAIN)) is None: + return True + + for sensor_conf in conf: + await discovery.async_load_platform( + hass, Platform.SENSOR, DOMAIN, sensor_conf, config + ) + + return True + + async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SQL from a config entry.""" entry.async_on_unload(entry.add_update_listener(async_update_listener)) diff --git a/homeassistant/components/sql/config_flow.py b/homeassistant/components/sql/config_flow.py index d8f4df814fc..cc8bbe672ba 100644 --- a/homeassistant/components/sql/config_flow.py +++ b/homeassistant/components/sql/config_flow.py @@ -80,12 +80,6 @@ class SQLConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return SQLOptionsFlowHandler(config_entry) - async def async_step_import(self, config: dict[str, Any] | None) -> FlowResult: - """Import a configuration from config.yaml.""" - - self._async_abort_entries_match(config) - return await self.async_step_user(user_input=config) - async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/sql/const.py b/homeassistant/components/sql/const.py index 7dfcc3fba81..2443e617395 100644 --- a/homeassistant/components/sql/const.py +++ b/homeassistant/components/sql/const.py @@ -7,6 +7,5 @@ DOMAIN = "sql" PLATFORMS = [Platform.SENSOR] CONF_COLUMN_NAME = "column" -CONF_QUERIES = "queries" CONF_QUERY = "query" DB_URL_RE = re.compile("//.*:.*@") diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 4ee1683a357..db3c20b2fc3 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,7 +2,7 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.4.44"], + "requirements": ["sqlalchemy==1.4.45"], "codeowners": ["@dgomes", "@gjohansson-ST"], "config_flow": true, "iot_class": "local_polling" diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index dfb1e15f052..4469c4c8057 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -9,25 +9,25 @@ import sqlalchemy from sqlalchemy.engine import Result from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import scoped_session, sessionmaker -import voluptuous as vol from homeassistant.components.recorder import CONF_DB_URL, DEFAULT_DB_FILE, DEFAULT_URL -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, - SensorEntity, +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_NAME, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE from homeassistant.core import HomeAssistant from homeassistant.exceptions import TemplateError -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.template import Template from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import CONF_COLUMN_NAME, CONF_QUERIES, CONF_QUERY, DB_URL_RE, DOMAIN +from .const import CONF_COLUMN_NAME, CONF_QUERY, DB_URL_RE, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -37,62 +37,45 @@ def redact_credentials(data: str) -> str: return DB_URL_RE.sub("//****:****@", data) -_QUERY_SCHEME = vol.Schema( - { - vol.Required(CONF_COLUMN_NAME): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_QUERY): cv.string, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_VALUE_TEMPLATE): cv.string, - } -) - -PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( - {vol.Required(CONF_QUERIES): [_QUERY_SCHEME], vol.Optional(CONF_DB_URL): cv.string} -) - - async def async_setup_platform( hass: HomeAssistant, config: ConfigType, async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: - """Set up the SQL sensor platform.""" - _LOGGER.warning( - # SQL config flow added in 2022.4 and should be removed in 2022.6 - "Configuration of the SQL sensor platform in YAML is deprecated and " - "will be removed in Home Assistant 2022.6; Your existing configuration " - "has been imported into the UI automatically and can be safely removed " - "from your configuration.yaml file" - ) + """Set up the SQL sensor from yaml.""" + if (conf := discovery_info) is None: + return - default_db_url = DEFAULT_URL.format( - hass_config_path=hass.config.path(DEFAULT_DB_FILE) - ) + name: str = conf[CONF_NAME] + query_str: str = conf[CONF_QUERY] + unit: str | None = conf.get(CONF_UNIT_OF_MEASUREMENT) + value_template: Template | None = conf.get(CONF_VALUE_TEMPLATE) + column_name: str = conf[CONF_COLUMN_NAME] + unique_id: str | None = conf.get(CONF_UNIQUE_ID) + db_url: str | None = conf.get(CONF_DB_URL) - for query in config[CONF_QUERIES]: - new_config = { - CONF_DB_URL: config.get(CONF_DB_URL, default_db_url), - CONF_NAME: query[CONF_NAME], - CONF_QUERY: query[CONF_QUERY], - CONF_UNIT_OF_MEASUREMENT: query.get(CONF_UNIT_OF_MEASUREMENT), - CONF_VALUE_TEMPLATE: query.get(CONF_VALUE_TEMPLATE), - CONF_COLUMN_NAME: query[CONF_COLUMN_NAME], - } - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=new_config, - ) - ) + if value_template is not None: + value_template.hass = hass + + await async_setup_sensor( + hass, + name, + query_str, + column_name, + unit, + value_template, + unique_id, + db_url, + True, + async_add_entities, + ) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the SQL sensor entry.""" + """Set up the SQL sensor from config entry.""" db_url: str = entry.options[CONF_DB_URL] name: str = entry.options[CONF_NAME] @@ -111,12 +94,56 @@ async def async_setup_entry( if value_template is not None: value_template.hass = hass + await async_setup_sensor( + hass, + name, + query_str, + column_name, + unit, + value_template, + entry.entry_id, + db_url, + False, + async_add_entities, + ) + + +async def async_setup_sensor( + hass: HomeAssistant, + name: str, + query_str: str, + column_name: str, + unit: str | None, + value_template: Template | None, + unique_id: str | None, + db_url: str | None, + yaml: bool, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the SQL sensor.""" + + if not db_url: + db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE)) + + sess: scoped_session | None = None try: engine = sqlalchemy.create_engine(db_url, future=True) sessmaker = scoped_session(sessionmaker(bind=engine, future=True)) + + # Run a dummy query just to test the db_url + sess = sessmaker() + sess.execute("SELECT 1;") + except SQLAlchemyError as err: - _LOGGER.error("Can not open database %s", {redact_credentials(str(err))}) + _LOGGER.error( + "Couldn't connect using %s DB_URL: %s", + redact_credentials(db_url), + redact_credentials(str(err)), + ) return + finally: + if sess: + sess.close() # MSSQL uses TOP and not LIMIT if not ("LIMIT" in query_str.upper() or "SELECT TOP" in query_str.upper()): @@ -134,7 +161,8 @@ async def async_setup_entry( column_name, unit, value_template, - entry.entry_id, + unique_id, + yaml, ) ], True, @@ -155,22 +183,25 @@ class SQLSensor(SensorEntity): column: str, unit: str | None, value_template: Template | None, - entry_id: str, + unique_id: str | None, + yaml: bool, ) -> None: """Initialize the SQL sensor.""" self._query = query + self._attr_name = name if yaml else None self._attr_native_unit_of_measurement = unit self._template = value_template self._column_name = column self.sessionmaker = sessmaker self._attr_extra_state_attributes = {} - self._attr_unique_id = entry_id - self._attr_device_info = DeviceInfo( - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, entry_id)}, - manufacturer="SQL", - name=name, - ) + self._attr_unique_id = unique_id + if not yaml and unique_id: + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, unique_id)}, + manufacturer="SQL", + name=name, + ) def update(self) -> None: """Retrieve sensor data from the query.""" diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 6a171ffe30f..f9d25038674 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -363,6 +363,11 @@ class SqueezeBoxEntity(MediaPlayerEntity): """Title of current playing media.""" return self._player.title + @property + def media_channel(self): + """Channel (e.g. webradio name) of current playing media.""" + return self._player.remote_title + @property def media_artist(self): """Artist of current playing media.""" diff --git a/homeassistant/components/squeezebox/translations/lt.json b/homeassistant/components/squeezebox/translations/lt.json new file mode 100644 index 00000000000..11c1859e0bc --- /dev/null +++ b/homeassistant/components/squeezebox/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "edit": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index 18ed063c8bc..c2f56bb7b4a 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -202,7 +202,9 @@ async def async_build_source_set(hass: HomeAssistant) -> set[IPv4Address | IPv6A return { source_ip for source_ip in await network.async_get_enabled_source_ips(hass) - if not source_ip.is_loopback and not source_ip.is_global + if not source_ip.is_loopback + and not source_ip.is_global + and (source_ip.version == 6 and source_ip.scope_id or source_ip.version == 4) } diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 10f7a8e49a2..4f3c56965c7 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -2,7 +2,7 @@ "domain": "ssdp", "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": ["async-upnp-client==0.33.0"], + "requirements": ["async-upnp-client==0.33.1"], "dependencies": ["network"], "after_dependencies": ["zeroconf"], "codeowners": [], diff --git a/homeassistant/components/starline/translations/lt.json b/homeassistant/components/starline/translations/lt.json new file mode 100644 index 00000000000..c039c6bd4d7 --- /dev/null +++ b/homeassistant/components/starline/translations/lt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "error_auth_user": "Neteisingas vartotojo vardas arba slapta\u017eodis" + }, + "step": { + "auth_user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/__init__.py b/homeassistant/components/starlink/__init__.py new file mode 100644 index 00000000000..ceb962c88cd --- /dev/null +++ b/homeassistant/components/starlink/__init__.py @@ -0,0 +1,40 @@ +"""The Starlink integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_IP_ADDRESS, Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import StarlinkUpdateCoordinator + +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.SENSOR, + Platform.SWITCH, +] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Starlink from a config entry.""" + coordinator = StarlinkUpdateCoordinator( + hass=hass, + url=entry.data[CONF_IP_ADDRESS], + name=entry.title, + ) + + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/starlink/binary_sensor.py b/homeassistant/components/starlink/binary_sensor.py new file mode 100644 index 00000000000..7e20753843b --- /dev/null +++ b/homeassistant/components/starlink/binary_sensor.py @@ -0,0 +1,125 @@ +"""Contains binary sensors exposed by the Starlink integration.""" + +from __future__ import annotations + +from collections.abc import Callable +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 import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import StarlinkData +from .entity import StarlinkEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up all binary sensors for this entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + StarlinkBinarySensorEntity(coordinator, description) + for description in BINARY_SENSORS + ) + + +@dataclass +class StarlinkBinarySensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[StarlinkData], bool | None] + + +@dataclass +class StarlinkBinarySensorEntityDescription( + BinarySensorEntityDescription, StarlinkBinarySensorEntityDescriptionMixin +): + """Describes a Starlink binary sensor entity.""" + + +class StarlinkBinarySensorEntity(StarlinkEntity, BinarySensorEntity): + """A BinarySensorEntity for Starlink devices. Handles creating unique IDs.""" + + entity_description: StarlinkBinarySensorEntityDescription + + @property + def is_on(self) -> bool | None: + """Calculate the binary sensor value from the entity description.""" + return self.entity_description.value_fn(self.coordinator.data) + + +BINARY_SENSORS = [ + StarlinkBinarySensorEntityDescription( + key="update", + name="Update available", + device_class=BinarySensorDeviceClass.UPDATE, + value_fn=lambda data: data.alert["alert_install_pending"], + ), + StarlinkBinarySensorEntityDescription( + key="roaming", + name="Roaming mode", + value_fn=lambda data: data.alert["alert_roaming"], + ), + StarlinkBinarySensorEntityDescription( + key="currently_obstructed", + name="Obstructed", + device_class=BinarySensorDeviceClass.PROBLEM, + value_fn=lambda data: data.status["currently_obstructed"], + ), + StarlinkBinarySensorEntityDescription( + key="heating", + name="Heating", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.alert["alert_is_heating"], + ), + StarlinkBinarySensorEntityDescription( + key="power_save_idle", + name="Idle", + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.alert["alert_is_power_save_idle"], + ), + StarlinkBinarySensorEntityDescription( + key="mast_near_vertical", + name="Mast near vertical", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.alert["alert_mast_not_near_vertical"], + ), + StarlinkBinarySensorEntityDescription( + key="motors_stuck", + name="Motors stuck", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.alert["alert_motors_stuck"], + ), + StarlinkBinarySensorEntityDescription( + key="slow_ethernet", + name="Ethernet speeds", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.alert["alert_slow_ethernet_speeds"], + ), + StarlinkBinarySensorEntityDescription( + key="thermal_throttle", + name="Thermal throttle", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.alert["alert_thermal_throttle"], + ), + StarlinkBinarySensorEntityDescription( + key="unexpected_location", + name="Unexpected location", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: data.alert["alert_unexpected_location"], + ), +] diff --git a/homeassistant/components/starlink/button.py b/homeassistant/components/starlink/button.py new file mode 100644 index 00000000000..4c57bac261d --- /dev/null +++ b/homeassistant/components/starlink/button.py @@ -0,0 +1,66 @@ +"""Contains buttons exposed by the Starlink integration.""" + +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 EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import StarlinkUpdateCoordinator +from .entity import StarlinkEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up all binary sensors for this entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + StarlinkButtonEntity(coordinator, description) for description in BUTTONS + ) + + +@dataclass +class StarlinkButtonEntityDescriptionMixin: + """Mixin for required keys.""" + + press_fn: Callable[[StarlinkUpdateCoordinator], Awaitable[None]] + + +@dataclass +class StarlinkButtonEntityDescription( + ButtonEntityDescription, StarlinkButtonEntityDescriptionMixin +): + """Describes a Starlink button entity.""" + + +class StarlinkButtonEntity(StarlinkEntity, ButtonEntity): + """A ButtonEntity for Starlink devices. Handles creating unique IDs.""" + + entity_description: StarlinkButtonEntityDescription + + async def async_press(self) -> None: + """Press the button.""" + return await self.entity_description.press_fn(self.coordinator) + + +BUTTONS = [ + StarlinkButtonEntityDescription( + key="reboot", + name="Reboot", + device_class=ButtonDeviceClass.RESTART, + entity_category=EntityCategory.DIAGNOSTIC, + press_fn=lambda coordinator: coordinator.async_reboot_starlink(), + ) +] diff --git a/homeassistant/components/starlink/config_flow.py b/homeassistant/components/starlink/config_flow.py new file mode 100644 index 00000000000..4154ef09adf --- /dev/null +++ b/homeassistant/components/starlink/config_flow.py @@ -0,0 +1,52 @@ +"""Config flow for Starlink.""" +from __future__ import annotations + +from typing import Any + +from starlink_grpc import ChannelContext, GrpcError, get_id +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + +CONFIG_SCHEMA = vol.Schema( + {vol.Required(CONF_IP_ADDRESS, default="192.168.100.1:9200"): str} +) + + +class StarlinkConfigFlow(ConfigFlow, domain=DOMAIN): + """The configuration flow for a Starlink system.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Ask the user for a server address and a name for the system.""" + errors = {} + if user_input: + # Input validation. If everything looks good, create the entry + if uid := await self.get_device_id(url=user_input[CONF_IP_ADDRESS]): + # Make sure we're not configuring the same device + await self.async_set_unique_id(uid) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title="Starlink", + data=user_input, + ) + errors[CONF_IP_ADDRESS] = "cannot_connect" + return self.async_show_form( + step_id="user", data_schema=CONFIG_SCHEMA, errors=errors + ) + + async def get_device_id(self, url: str) -> str | None: + """Get the device UID, or None if no device exists at the given URL.""" + context = ChannelContext(target=url) + try: + response = await self.hass.async_add_executor_job(get_id, context) + except GrpcError: + response = None + context.close() + return response diff --git a/homeassistant/components/starlink/const.py b/homeassistant/components/starlink/const.py new file mode 100644 index 00000000000..e2f88c5e442 --- /dev/null +++ b/homeassistant/components/starlink/const.py @@ -0,0 +1,3 @@ +"""Constants for the Starlink integration.""" + +DOMAIN = "starlink" diff --git a/homeassistant/components/starlink/coordinator.py b/homeassistant/components/starlink/coordinator.py new file mode 100644 index 00000000000..56d25bf2d1a --- /dev/null +++ b/homeassistant/components/starlink/coordinator.py @@ -0,0 +1,76 @@ +"""Contains the shared Coordinator for Starlink systems.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta +import logging + +import async_timeout +from starlink_grpc import ( + AlertDict, + ChannelContext, + GrpcError, + ObstructionDict, + StatusDict, + reboot, + set_stow_state, + status_data, +) + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class StarlinkData: + """Contains data pulled from the Starlink system.""" + + status: StatusDict + obstruction: ObstructionDict + alert: AlertDict + + +class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]): + """Coordinates updates between all Starlink sensors defined in this file.""" + + def __init__(self, hass: HomeAssistant, name: str, url: str) -> None: + """Initialize an UpdateCoordinator for a group of sensors.""" + self.channel_context = ChannelContext(target=url) + + super().__init__( + hass, + _LOGGER, + name=name, + update_interval=timedelta(seconds=5), + ) + + async def _async_update_data(self) -> StarlinkData: + async with async_timeout.timeout(4): + try: + status = await self.hass.async_add_executor_job( + status_data, self.channel_context + ) + return StarlinkData(*status) + except GrpcError as exc: + raise UpdateFailed from exc + + async def async_stow_starlink(self, stow: bool): + """Set whether Starlink system tied to this coordinator should be stowed.""" + async with async_timeout.timeout(4): + try: + await self.hass.async_add_executor_job( + set_stow_state, not stow, self.channel_context + ) + except GrpcError as exc: + raise HomeAssistantError from exc + + async def async_reboot_starlink(self): + """Reboot the Starlink system tied to this coordinator.""" + async with async_timeout.timeout(4): + try: + await self.hass.async_add_executor_job(reboot, self.channel_context) + except GrpcError as exc: + raise HomeAssistantError from exc diff --git a/homeassistant/components/starlink/entity.py b/homeassistant/components/starlink/entity.py new file mode 100644 index 00000000000..29ef9ba9f08 --- /dev/null +++ b/homeassistant/components/starlink/entity.py @@ -0,0 +1,33 @@ +"""Contains base entity classes for Starlink entities.""" +from __future__ import annotations + +from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import StarlinkUpdateCoordinator + + +class StarlinkEntity(CoordinatorEntity[StarlinkUpdateCoordinator], Entity): + """A base Entity that is registered under a Starlink device.""" + + _attr_has_entity_name = True + + def __init__( + self, coordinator: StarlinkUpdateCoordinator, description: EntityDescription + ) -> None: + """Initialize the device info and set the update coordinator.""" + super().__init__(coordinator) + self._attr_device_info = DeviceInfo( + identifiers={ + (DOMAIN, self.coordinator.data.status["id"]), + }, + sw_version=self.coordinator.data.status["software_version"], + hw_version=self.coordinator.data.status["hardware_version"], + name="Starlink", + configuration_url=f"http://{self.coordinator.channel_context.target.split(':')[0]}", + manufacturer="SpaceX", + model="Starlink", + ) + self._attr_unique_id = f"{self.coordinator.data.status['id']}_{description.key}" + self.entity_description = description diff --git a/homeassistant/components/starlink/manifest.json b/homeassistant/components/starlink/manifest.json new file mode 100644 index 00000000000..9ab583e3440 --- /dev/null +++ b/homeassistant/components/starlink/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "starlink", + "name": "Starlink", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/starlink", + "requirements": ["starlink-grpc-core==1.1.1"], + "codeowners": ["@boswelja"], + "iot_class": "local_polling", + "quality_scale": "silver" +} diff --git a/homeassistant/components/starlink/sensor.py b/homeassistant/components/starlink/sensor.py new file mode 100644 index 00000000000..a5b9c47ee9b --- /dev/null +++ b/homeassistant/components/starlink/sensor.py @@ -0,0 +1,114 @@ +"""Contains sensors exposed by the Starlink integration.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime, timedelta + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEGREE, UnitOfDataRate, UnitOfTime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.util.dt import now + +from .const import DOMAIN +from .coordinator import StarlinkData +from .entity import StarlinkEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up all sensors for this entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + StarlinkSensorEntity(coordinator, description) for description in SENSORS + ) + + +@dataclass +class StarlinkSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[StarlinkData], datetime | StateType] + + +@dataclass +class StarlinkSensorEntityDescription( + SensorEntityDescription, StarlinkSensorEntityDescriptionMixin +): + """Describes a Starlink sensor entity.""" + + +class StarlinkSensorEntity(StarlinkEntity, SensorEntity): + """A SensorEntity for Starlink devices. Handles creating unique IDs.""" + + entity_description: StarlinkSensorEntityDescription + + @property + def native_value(self) -> StateType | datetime: + """Calculate the sensor value from the entity description.""" + return self.entity_description.value_fn(self.coordinator.data) + + +SENSORS: tuple[StarlinkSensorEntityDescription, ...] = ( + StarlinkSensorEntityDescription( + key="ping", + name="Ping", + icon="mdi:speedometer", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTime.MILLISECONDS, + value_fn=lambda data: round(data.status["pop_ping_latency_ms"]), + ), + StarlinkSensorEntityDescription( + key="azimuth", + name="Azimuth", + icon="mdi:compass", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DEGREE, + value_fn=lambda data: round(data.status["direction_azimuth"]), + ), + StarlinkSensorEntityDescription( + key="elevation", + name="Elevation", + icon="mdi:compass", + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=DEGREE, + value_fn=lambda data: round(data.status["direction_elevation"]), + ), + StarlinkSensorEntityDescription( + key="uplink_throughput", + name="Uplink throughput", + icon="mdi:upload", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND, + value_fn=lambda data: round(data.status["uplink_throughput_bps"]), + ), + StarlinkSensorEntityDescription( + key="downlink_throughput", + name="Downlink throughput", + icon="mdi:download", + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfDataRate.BITS_PER_SECOND, + value_fn=lambda data: round(data.status["downlink_throughput_bps"]), + ), + StarlinkSensorEntityDescription( + key="last_boot_time", + name="Last boot time", + icon="mdi:clock", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + value_fn=lambda data: now() - timedelta(seconds=data.status["uptime"]), + ), +) diff --git a/homeassistant/components/starlink/strings.json b/homeassistant/components/starlink/strings.json new file mode 100644 index 00000000000..dddbada730d --- /dev/null +++ b/homeassistant/components/starlink/strings.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": { + "user": { + "data": { + "ip_address": "[%key:common::config_flow::data::ip%]" + } + } + } + } +} diff --git a/homeassistant/components/starlink/switch.py b/homeassistant/components/starlink/switch.py new file mode 100644 index 00000000000..daa7b45b305 --- /dev/null +++ b/homeassistant/components/starlink/switch.py @@ -0,0 +1,78 @@ +"""Contains switches exposed by the Starlink integration.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any + +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +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 StarlinkData, StarlinkUpdateCoordinator +from .entity import StarlinkEntity + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up all binary sensors for this entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities( + StarlinkSwitchEntity(coordinator, description) for description in SWITCHES + ) + + +@dataclass +class StarlinkSwitchEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[StarlinkData], bool | None] + turn_on_fn: Callable[[StarlinkUpdateCoordinator], Awaitable[None]] + turn_off_fn: Callable[[StarlinkUpdateCoordinator], Awaitable[None]] + + +@dataclass +class StarlinkSwitchEntityDescription( + SwitchEntityDescription, StarlinkSwitchEntityDescriptionMixin +): + """Describes a Starlink switch entity.""" + + +class StarlinkSwitchEntity(StarlinkEntity, SwitchEntity): + """A SwitchEntity for Starlink devices. Handles creating unique IDs.""" + + entity_description: StarlinkSwitchEntityDescription + + @property + def is_on(self) -> bool | None: + """Return True if entity is on.""" + return self.entity_description.value_fn(self.coordinator.data) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + return await self.entity_description.turn_on_fn(self.coordinator) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + return await self.entity_description.turn_off_fn(self.coordinator) + + +SWITCHES = [ + StarlinkSwitchEntityDescription( + key="stowed", + name="Stowed", + device_class=SwitchDeviceClass.SWITCH, + value_fn=lambda data: data.status["state"] == "STOWED", + turn_on_fn=lambda coordinator: coordinator.async_stow_starlink(True), + turn_off_fn=lambda coordinator: coordinator.async_stow_starlink(False), + ) +] diff --git a/homeassistant/components/starlink/translations/bg.json b/homeassistant/components/starlink/translations/bg.json new file mode 100644 index 00000000000..3ca2f8f1d9b --- /dev/null +++ b/homeassistant/components/starlink/translations/bg.json @@ -0,0 +1,17 @@ +{ + "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" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/ca.json b/homeassistant/components/starlink/translations/ca.json new file mode 100644 index 00000000000..63b5168b133 --- /dev/null +++ b/homeassistant/components/starlink/translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "ip_address": "Adre\u00e7a IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/de.json b/homeassistant/components/starlink/translations/de.json new file mode 100644 index 00000000000..214b20ef0ba --- /dev/null +++ b/homeassistant/components/starlink/translations/de.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-Adresse" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/el.json b/homeassistant/components/starlink/translations/el.json new file mode 100644 index 00000000000..1dad5fb0d6d --- /dev/null +++ b/homeassistant/components/starlink/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" + }, + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/en.json b/homeassistant/components/starlink/translations/en.json new file mode 100644 index 00000000000..52d4a77460b --- /dev/null +++ b/homeassistant/components/starlink/translations/en.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect" + }, + "step": { + "user": { + "data": { + "ip_address": "IP Address" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/es.json b/homeassistant/components/starlink/translations/es.json new file mode 100644 index 00000000000..4eb1e485946 --- /dev/null +++ b/homeassistant/components/starlink/translations/es.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "ip_address": "Direcci\u00f3n IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/et.json b/homeassistant/components/starlink/translations/et.json new file mode 100644 index 00000000000..93281ec4ba2 --- /dev/null +++ b/homeassistant/components/starlink/translations/et.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "user": { + "data": { + "ip_address": "IP aadress" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/hu.json b/homeassistant/components/starlink/translations/hu.json new file mode 100644 index 00000000000..f50e5600a2a --- /dev/null +++ b/homeassistant/components/starlink/translations/hu.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "user": { + "data": { + "ip_address": "IP c\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/id.json b/homeassistant/components/starlink/translations/id.json new file mode 100644 index 00000000000..f89c7c7ba4e --- /dev/null +++ b/homeassistant/components/starlink/translations/id.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung" + }, + "step": { + "user": { + "data": { + "ip_address": "Alamat IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/it.json b/homeassistant/components/starlink/translations/it.json new file mode 100644 index 00000000000..7fd490f86b8 --- /dev/null +++ b/homeassistant/components/starlink/translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "ip_address": "Indirizzo IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/lv.json b/homeassistant/components/starlink/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/starlink/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/nl.json b/homeassistant/components/starlink/translations/nl.json new file mode 100644 index 00000000000..6cec631f4cc --- /dev/null +++ b/homeassistant/components/starlink/translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-adres" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/no.json b/homeassistant/components/starlink/translations/no.json new file mode 100644 index 00000000000..8ea8870ffbb --- /dev/null +++ b/homeassistant/components/starlink/translations/no.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresse" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/pl.json b/homeassistant/components/starlink/translations/pl.json new file mode 100644 index 00000000000..435b27b4f0a --- /dev/null +++ b/homeassistant/components/starlink/translations/pl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "user": { + "data": { + "ip_address": "Adres IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/pt-BR.json b/homeassistant/components/starlink/translations/pt-BR.json new file mode 100644 index 00000000000..10a8524e1aa --- /dev/null +++ b/homeassistant/components/starlink/translations/pt-BR.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar" + }, + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o de IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/pt.json b/homeassistant/components/starlink/translations/pt.json new file mode 100644 index 00000000000..28ad8b6c8f3 --- /dev/null +++ b/homeassistant/components/starlink/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/ru.json b/homeassistant/components/starlink/translations/ru.json new file mode 100644 index 00000000000..3f5880516ec --- /dev/null +++ b/homeassistant/components/starlink/translations/ru.json @@ -0,0 +1,17 @@ +{ + "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." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/sk.json b/homeassistant/components/starlink/translations/sk.json new file mode 100644 index 00000000000..0283ec1411d --- /dev/null +++ b/homeassistant/components/starlink/translations/sk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/tr.json b/homeassistant/components/starlink/translations/tr.json new file mode 100644 index 00000000000..c7148df77ef --- /dev/null +++ b/homeassistant/components/starlink/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "ip_address": "IP Adresi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/uk.json b/homeassistant/components/starlink/translations/uk.json new file mode 100644 index 00000000000..25519197704 --- /dev/null +++ b/homeassistant/components/starlink/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u0410\u0434\u0440\u0435\u0441\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starlink/translations/zh-Hant.json b/homeassistant/components/starlink/translations/zh-Hant.json new file mode 100644 index 00000000000..e59404c8c7d --- /dev/null +++ b/homeassistant/components/starlink/translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u4f4d\u5740" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/statistics/translations/hu.json b/homeassistant/components/statistics/translations/hu.json index df6ff936d58..ae7693b72e2 100644 --- a/homeassistant/components/statistics/translations/hu.json +++ b/homeassistant/components/statistics/translations/hu.json @@ -1,11 +1,11 @@ { "issues": { "deprecation_warning_characteristic": { - "description": "A statisztikai integr\u00e1ci\u00f3 `state_characteristic` konfigur\u00e1ci\u00f3s param\u00e9tere k\u00f6telez\u0151v\u00e9 v\u00e1lik.\n\nK\u00e9rj\u00fck, a jelenlegi m\u0171k\u00f6d\u00e9s megtart\u00e1s\u00e1hoz adja hozz\u00e1 a `state_characteristic: {characteristic}` param\u00e9tert a `{entity}` \u00e9rz\u00e9kel\u0151 konfigur\u00e1ci\u00f3j\u00e1hoz.\n\nTov\u00e1bbi r\u00e9szletek\u00e9rt olvassa el a statisztikai integr\u00e1ci\u00f3 dokument\u00e1ci\u00f3j\u00e1t: https://www.home-assistant.io/integrations/statistics/", + "description": "A statisztikai integr\u00e1ci\u00f3 `state_characteristic` konfigur\u00e1ci\u00f3s param\u00e9tere k\u00f6telez\u0151v\u00e9 v\u00e1lik.\n\nK\u00e9rem, a jelenlegi m\u0171k\u00f6d\u00e9s megtart\u00e1s\u00e1hoz adja hozz\u00e1 a `state_characteristic: {characteristic}` param\u00e9tert a `{entity}` \u00e9rz\u00e9kel\u0151 konfigur\u00e1ci\u00f3j\u00e1hoz.\n\nTov\u00e1bbi r\u00e9szletek\u00e9rt olvassa el a statisztikai integr\u00e1ci\u00f3 dokument\u00e1ci\u00f3j\u00e1t: https://www.home-assistant.io/integrations/statistics/", "title": "K\u00f6telez\u0151 'state_characteristic' felt\u00e9telezve egy statisztikai entit\u00e1sn\u00e1l" }, "deprecation_warning_size": { - "description": "A statisztikai integr\u00e1ci\u00f3 `sampling_size` konfigur\u00e1ci\u00f3s param\u00e9tere eddig 20-as \u00e9rt\u00e9kre volt alap\u00e9rtelmezve, ami v\u00e1ltozni fog.\n\nK\u00e9rj\u00fck, ellen\u0151rizze `{entity}` \u00e9rz\u00e9kel\u0151 konfigur\u00e1ci\u00f3j\u00e1t, \u00e9s adjon hozz\u00e1 megfelel\u0151 hat\u00e1rokat, pl. `sampling_size: 20` a jelenlegi m\u0171k\u00f6d\u00e9s megtart\u00e1s\u00e1hoz. A 2022.12.0 verzi\u00f3val a statisztika integr\u00e1ci\u00f3 konfigur\u00e1ci\u00f3ja rugalmasabb\u00e1 v\u00e1lik, \u00e9s elfogadja a `sampling_size` vagy a `max_age`, vagy mindk\u00e9t be\u00e1ll\u00edt\u00e1st. Ezzel k\u00f6vetelm\u00e9nyyel felk\u00e9sz\u00edtj\u00fck a konfigur\u00e1ci\u00f3t erre a k\u00f6telez\u0151 v\u00e1ltoz\u00e1sra.\n\nTov\u00e1bbi r\u00e9szletek\u00e9rt olvassa el a statisztikai integr\u00e1ci\u00f3 dokument\u00e1ci\u00f3j\u00e1t: https://www.home-assistant.io/integrations/statistics/", + "description": "A statisztikai integr\u00e1ci\u00f3 `sampling_size` konfigur\u00e1ci\u00f3s param\u00e9tere eddig 20-as \u00e9rt\u00e9kre volt alap\u00e9rtelmezve, ami v\u00e1ltozni fog.\n\nK\u00e9rem, ellen\u0151rizze `{entity}` \u00e9rz\u00e9kel\u0151 konfigur\u00e1ci\u00f3j\u00e1t, \u00e9s adjon hozz\u00e1 megfelel\u0151 hat\u00e1rokat, pl. `sampling_size: 20` a jelenlegi m\u0171k\u00f6d\u00e9s megtart\u00e1s\u00e1hoz. A 2022.12.0 verzi\u00f3val a statisztika integr\u00e1ci\u00f3 konfigur\u00e1ci\u00f3ja rugalmasabb\u00e1 v\u00e1lik, \u00e9s elfogadja a `sampling_size` vagy a `max_age`, vagy mindk\u00e9t be\u00e1ll\u00edt\u00e1st. Ezzel k\u00f6vetelm\u00e9nyyel felk\u00e9sz\u00edtj\u00fck a konfigur\u00e1ci\u00f3t erre a k\u00f6telez\u0151 v\u00e1ltoz\u00e1sra.\n\nTov\u00e1bbi r\u00e9szletek\u00e9rt olvassa el a statisztikai integr\u00e1ci\u00f3 dokument\u00e1ci\u00f3j\u00e1t: https://www.home-assistant.io/integrations/statistics/", "title": "Implicit 'sampling_size' felt\u00e9telezve egy Statisztikai entit\u00e1shoz" } } diff --git a/homeassistant/components/steam_online/__init__.py b/homeassistant/components/steam_online/__init__.py index b1697e3b794..2629962565b 100644 --- a/homeassistant/components/steam_online/__init__.py +++ b/homeassistant/components/steam_online/__init__.py @@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = SteamDataUpdateCoordinator(hass) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/steam_online/coordinator.py b/homeassistant/components/steam_online/coordinator.py index 30178fa2b82..719acecd1f2 100644 --- a/homeassistant/components/steam_online/coordinator.py +++ b/homeassistant/components/steam_online/coordinator.py @@ -2,7 +2,6 @@ from __future__ import annotations from datetime import timedelta -from typing import Union import steam from steam.api import _interface_method as INTMethod @@ -17,7 +16,7 @@ from .const import CONF_ACCOUNTS, DOMAIN, LOGGER class SteamDataUpdateCoordinator( - DataUpdateCoordinator[dict[str, dict[str, Union[str, int]]]] + DataUpdateCoordinator[dict[str, dict[str, str | int]]] ): """Data update coordinator for the Steam integration.""" diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py index 70225a90a1c..10e507775d0 100644 --- a/homeassistant/components/steam_online/sensor.py +++ b/homeassistant/components/steam_online/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import datetime from time import localtime, mktime -from typing import Optional, cast +from typing import cast from homeassistant.components.sensor import SensorEntity, SensorEntityDescription from homeassistant.config_entries import ConfigEntry @@ -79,7 +79,7 @@ class SteamSensor(SteamEntity, SensorEntity): attrs["game_icon"] = f"{STEAM_ICON_URL}{game_id}/{info}.jpg" self._attr_name = str(player["personaname"]) or None self._attr_entity_picture = str(player["avatarmedium"]) or None - if last_online := cast(Optional[int], player.get("lastlogoff")): + if last_online := cast(int | None, player.get("lastlogoff")): attrs["last_online"] = utc_from_timestamp(mktime(localtime(last_online))) if level := self.coordinator.data[self.entity_description.key]["level"]: attrs["level"] = level diff --git a/homeassistant/components/steam_online/translations/el.json b/homeassistant/components/steam_online/translations/el.json index 33a864e0945..afeaa03018c 100644 --- a/homeassistant/components/steam_online/translations/el.json +++ b/homeassistant/components/steam_online/translations/el.json @@ -12,7 +12,7 @@ }, "step": { "reauth_confirm": { - "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Steam \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03c5\u03c4\u03b5\u03af \u03be\u03b1\u03bd\u03ac \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf \n\n \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c3\u03b1\u03c2 \u03b5\u03b4\u03ce: https://steamcommunity.com/dev/apikey", + "description": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 Steam \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03c0\u03b1\u03bb\u03b7\u03b8\u03b5\u03c5\u03c4\u03b5\u03af \u03be\u03b1\u03bd\u03ac \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf \n\n \u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b2\u03c1\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c3\u03b1\u03c2 \u03b5\u03b4\u03ce: {api_key_url}", "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": { @@ -20,7 +20,7 @@ "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 Steam", "api_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af API" }, - "description": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf https://steamid.io \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 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Steam" + "description": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf {account_id_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 \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03bf\u03cd \u03c3\u03b1\u03c2 \u03c3\u03c4\u03bf Steam" } } }, diff --git a/homeassistant/components/steamist/translations/el.json b/homeassistant/components/steamist/translations/el.json index 0d185423056..ff2e8266575 100644 --- a/homeassistant/components/steamist/translations/el.json +++ b/homeassistant/components/steamist/translations/el.json @@ -14,7 +14,7 @@ "flow_title": "{name} ({ipaddress})", "step": { "discovery_confirm": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ( {ipaddress} );" + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ({ipaddress});" }, "pick_device": { "data": { diff --git a/homeassistant/components/steamist/translations/lv.json b/homeassistant/components/steamist/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/steamist/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/steamist/translations/tr.json b/homeassistant/components/steamist/translations/tr.json index 16f88494597..b36eb5cac12 100644 --- a/homeassistant/components/steamist/translations/tr.json +++ b/homeassistant/components/steamist/translations/tr.json @@ -14,7 +14,7 @@ "flow_title": "{name} ({ipaddress})", "step": { "discovery_confirm": { - "description": "{name} ( {ipaddress} ) kurulumunu yapmak istiyor musunuz?" + "description": "{name} ( {ipaddress} ) kurmak istiyor musunuz?" }, "pick_device": { "data": { diff --git a/homeassistant/components/steamist/translations/uk.json b/homeassistant/components/steamist/translations/uk.json new file mode 100644 index 00000000000..77b00eed718 --- /dev/null +++ b/homeassistant/components/steamist/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "pick_device": { + "data": { + "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/__init__.py b/homeassistant/components/stookwijzer/__init__.py new file mode 100644 index 00000000000..d1950eaf0a3 --- /dev/null +++ b/homeassistant/components/stookwijzer/__init__.py @@ -0,0 +1,29 @@ +"""The Stookwijzer integration.""" +from __future__ import annotations + +from stookwijzer import Stookwijzer + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, Platform +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + +PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Stookwijzer from a config entry.""" + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = Stookwijzer( + entry.data[CONF_LOCATION][CONF_LATITUDE], + entry.data[CONF_LOCATION][CONF_LONGITUDE], + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Stookwijzer config entry.""" + 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/stookwijzer/config_flow.py b/homeassistant/components/stookwijzer/config_flow.py new file mode 100644 index 00000000000..fdf4cc06f37 --- /dev/null +++ b/homeassistant/components/stookwijzer/config_flow.py @@ -0,0 +1,45 @@ +"""Config flow to configure the Stookwijzer integration.""" +from __future__ import annotations + +from typing import Any + +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.selector import LocationSelector + +from .const import DOMAIN + + +class StookwijzerFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow for Stookwijzer.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + + if user_input is not None: + return self.async_create_entry( + title="Stookwijzer", + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_LOCATION, + default={ + CONF_LATITUDE: self.hass.config.latitude, + CONF_LONGITUDE: self.hass.config.longitude, + }, + ): LocationSelector() + } + ), + ) diff --git a/homeassistant/components/stookwijzer/const.py b/homeassistant/components/stookwijzer/const.py new file mode 100644 index 00000000000..cdd5ac2a567 --- /dev/null +++ b/homeassistant/components/stookwijzer/const.py @@ -0,0 +1,16 @@ +"""Constants for the Stookwijzer integration.""" +import logging +from typing import Final + +from homeassistant.backports.enum import StrEnum + +DOMAIN: Final = "stookwijzer" +LOGGER = logging.getLogger(__package__) + + +class StookwijzerState(StrEnum): + """Stookwijzer states for sensor entity.""" + + BLUE = "blauw" + ORANGE = "oranje" + RED = "rood" diff --git a/homeassistant/components/stookwijzer/diagnostics.py b/homeassistant/components/stookwijzer/diagnostics.py new file mode 100644 index 00000000000..e29606cb191 --- /dev/null +++ b/homeassistant/components/stookwijzer/diagnostics.py @@ -0,0 +1,31 @@ +"""Diagnostics support for Stookwijzer.""" +from __future__ import annotations + +from typing import Any + +from stookwijzer import Stookwijzer + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + client: Stookwijzer = hass.data[DOMAIN][entry.entry_id] + + last_updated = None + if client.last_updated: + last_updated = client.last_updated.isoformat() + + return { + "state": client.state, + "last_updated": last_updated, + "lqi": client.lqi, + "windspeed": client.windspeed, + "weather": client.weather, + "concentrations": client.concentrations, + } diff --git a/homeassistant/components/stookwijzer/manifest.json b/homeassistant/components/stookwijzer/manifest.json new file mode 100644 index 00000000000..fc653fd6ebe --- /dev/null +++ b/homeassistant/components/stookwijzer/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "stookwijzer", + "name": "Stookwijzer", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/stookwijzer", + "codeowners": ["@fwestenberg"], + "requirements": ["stookwijzer==1.3.0"], + "integration_type": "service", + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/stookwijzer/sensor.py b/homeassistant/components/stookwijzer/sensor.py new file mode 100644 index 00000000000..9eb70fda7ee --- /dev/null +++ b/homeassistant/components/stookwijzer/sensor.py @@ -0,0 +1,65 @@ +"""This integration provides support for Stookwijzer Sensor.""" +from __future__ import annotations + +from datetime import timedelta + +from stookwijzer import Stookwijzer + +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, StookwijzerState + +SCAN_INTERVAL = timedelta(minutes=60) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Stookwijzer sensor from a config entry.""" + client = hass.data[DOMAIN][entry.entry_id] + async_add_entities([StookwijzerSensor(client, entry)], update_before_add=True) + + +class StookwijzerSensor(SensorEntity): + """Defines a Stookwijzer binary sensor.""" + + _attr_attribution = "Data provided by stookwijzer.nu" + _attr_device_class = SensorDeviceClass.ENUM + _attr_has_entity_name = True + _attr_translation_key = "stookwijzer" + + def __init__(self, client: Stookwijzer, entry: ConfigEntry) -> None: + """Initialize a Stookwijzer device.""" + self._client = client + self._attr_options = [cls.value for cls in StookwijzerState] + self._attr_unique_id = entry.entry_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{entry.entry_id}")}, + name="Stookwijzer", + manufacturer="stookwijzer.nu", + entry_type=DeviceEntryType.SERVICE, + configuration_url="https://www.stookwijzer.nu", + ) + + def update(self) -> None: + """Update the data from the Stookwijzer handler.""" + self._client.update() + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self._client.state is not None + + @property + def native_value(self) -> str | None: + """Return the state of the device.""" + if self._client.state is None: + return None + return StookwijzerState(self._client.state).value diff --git a/homeassistant/components/stookwijzer/strings.json b/homeassistant/components/stookwijzer/strings.json new file mode 100644 index 00000000000..549673165ec --- /dev/null +++ b/homeassistant/components/stookwijzer/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "description": "Select the location you want to recieve the Stookwijzer information for.", + "data": { + "location": "[%key:common::config_flow::data::location%]" + } + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Blue", + "oranje": "Orange", + "rood": "Red" + } + } + } + } +} diff --git a/homeassistant/components/stookwijzer/translations/bg.json b/homeassistant/components/stookwijzer/translations/bg.json new file mode 100644 index 00000000000..3919258acfe --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/bg.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + }, + "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e, \u0437\u0430 \u043a\u043e\u0435\u0442\u043e \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e\u0442 Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "\u0421\u0438\u043d", + "oranje": "\u041e\u0440\u0430\u043d\u0436\u0435\u0432", + "rood": "\u0427\u0435\u0440\u0432\u0435\u043d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/ca.json b/homeassistant/components/stookwijzer/translations/ca.json new file mode 100644 index 00000000000..8945f8007e9 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Ubicaci\u00f3" + }, + "description": "Selecciona la ubicaci\u00f3 per a la qual vulguis rebre la informaci\u00f3 de Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Blau", + "oranje": "Taronja", + "rood": "Vermell" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/de.json b/homeassistant/components/stookwijzer/translations/de.json new file mode 100644 index 00000000000..4383f3a3db9 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Standort" + }, + "description": "W\u00e4hle den Standort aus, f\u00fcr den du die Stookwijzer-Informationen erhalten m\u00f6chtest." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Blau", + "oranje": "Orange", + "rood": "Rot" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/el.json b/homeassistant/components/stookwijzer/translations/el.json new file mode 100644 index 00000000000..0e764e6501c --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/el.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "\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 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03bf\u03c0\u03bf\u03af\u03b1 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03bb\u03ac\u03b2\u03b5\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03c0\u03bb\u03b7\u03c1\u03bf\u03c6\u03bf\u03c1\u03af\u03b5\u03c2 Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "\u039c\u03c0\u03bb\u03b5", + "oranje": "\u03a0\u03bf\u03c1\u03c4\u03bf\u03ba\u03ac\u03bb\u03b9", + "rood": "\u039a\u03cc\u03ba\u03ba\u03b9\u03bd\u03bf" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/en.json b/homeassistant/components/stookwijzer/translations/en.json new file mode 100644 index 00000000000..0aa8a093c69 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Location" + }, + "description": "Select the location you want to recieve the Stookwijzer information for." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Blue", + "oranje": "Orange", + "rood": "Red" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/es.json b/homeassistant/components/stookwijzer/translations/es.json new file mode 100644 index 00000000000..2278fbcb02a --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Ubicaci\u00f3n" + }, + "description": "Selecciona la ubicaci\u00f3n para la que deseas recibir la informaci\u00f3n de Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Azul", + "oranje": "Naranja", + "rood": "Rojo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/et.json b/homeassistant/components/stookwijzer/translations/et.json new file mode 100644 index 00000000000..391eb4f379e --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/et.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Asukoht" + }, + "description": "Vali asukoht, mille kohta soovid Stookwijzeri teavet saada." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Sinine", + "oranje": "Oran\u017e", + "rood": "Punane" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/hu.json b/homeassistant/components/stookwijzer/translations/hu.json new file mode 100644 index 00000000000..14b2289c591 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/hu.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Elhelyezked\u00e9s" + }, + "description": "V\u00e1lassza ki azt a helyet, amelyre vonatkoz\u00f3an a Stookwijzer inform\u00e1ci\u00f3kat szeretn\u00e9 megkapni." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "K\u00e9k", + "oranje": "Narancss\u00e1rga", + "rood": "Piros" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/id.json b/homeassistant/components/stookwijzer/translations/id.json new file mode 100644 index 00000000000..2aa939660f9 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/id.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Lokasi" + }, + "description": "Pilih lokasi yang Anda inginkan untuk menerima informasi Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Biru", + "oranje": "Jingga", + "rood": "Merah" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/it.json b/homeassistant/components/stookwijzer/translations/it.json new file mode 100644 index 00000000000..331b391031a --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Posizione" + }, + "description": "Seleziona la localit\u00e0 per la quale desideri ricevere le informazioni di Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Blu", + "oranje": "Arancione", + "rood": "Rosso" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/no.json b/homeassistant/components/stookwijzer/translations/no.json new file mode 100644 index 00000000000..f89278f62eb --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Plassering" + }, + "description": "Velg stedet du vil motta Stookwijzer-informasjonen for." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Bl\u00e5", + "oranje": "Oransje", + "rood": "R\u00f8d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/pl.json b/homeassistant/components/stookwijzer/translations/pl.json new file mode 100644 index 00000000000..af6bb16f469 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Lokalizacja" + }, + "description": "Wybierz lokalizacj\u0119, dla kt\u00f3rej chcesz otrzymywa\u0107 informacje Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "niebieski", + "oranje": "pomara\u0144czowy", + "rood": "czerwony" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/pt-BR.json b/homeassistant/components/stookwijzer/translations/pt-BR.json new file mode 100644 index 00000000000..d419acc0665 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Localiza\u00e7\u00e3o" + }, + "description": "Selecione o local para o qual deseja receber as informa\u00e7\u00f5es do Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Azul", + "oranje": "Laranja", + "rood": "Vermelho" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/ru.json b/homeassistant/components/stookwijzer/translations/ru.json new file mode 100644 index 00000000000..939177823c6 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043c\u0435\u0441\u0442\u043e, \u0434\u043b\u044f \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "\u0421\u0438\u043d\u0438\u0439", + "oranje": "\u041e\u0440\u0430\u043d\u0436\u0435\u0432\u044b\u0439", + "rood": "\u041a\u0440\u0430\u0441\u043d\u044b\u0439" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/sk.json b/homeassistant/components/stookwijzer/translations/sk.json new file mode 100644 index 00000000000..ed0cbcd176e --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/sk.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "Umiestnenie" + }, + "description": "Vyberte miesto, pre ktor\u00e9 chcete dost\u00e1va\u0165 inform\u00e1cie Stookwijzer." + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Modr\u00e1", + "oranje": "Oran\u017eov\u00e1", + "rood": "\u010cerven\u00e1" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/tr.json b/homeassistant/components/stookwijzer/translations/tr.json new file mode 100644 index 00000000000..dc7675b29e5 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/tr.json @@ -0,0 +1,13 @@ +{ + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "Mavi", + "oranje": "Turuncu", + "rood": "K\u0131rm\u0131z\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/uk.json b/homeassistant/components/stookwijzer/translations/uk.json new file mode 100644 index 00000000000..7a983c3a10c --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/uk.json @@ -0,0 +1,13 @@ +{ + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "\u0421\u0438\u043d\u0456\u0439", + "oranje": "\u041f\u043e\u043c\u0430\u0440\u0430\u043d\u0447\u0435\u0432\u0438\u0439", + "rood": "\u0427\u0435\u0440\u0432\u043e\u043d\u0438\u0439" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stookwijzer/translations/zh-Hant.json b/homeassistant/components/stookwijzer/translations/zh-Hant.json new file mode 100644 index 00000000000..90a58683984 --- /dev/null +++ b/homeassistant/components/stookwijzer/translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "user": { + "data": { + "location": "\u5ea7\u6a19" + }, + "description": "\u9078\u64c7\u6240\u8981\u63a5\u6536 Stookwijzer \u8cc7\u8a0a\u7684\u4f4d\u7f6e\u3002" + } + } + }, + "entity": { + "sensor": { + "stookwijzer": { + "state": { + "blauw": "\u85cd\u8272", + "oranje": "\u6a58\u8272", + "rood": "\u7d05\u8272" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 02aad1126f8..6073494ae1a 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -220,7 +220,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: filter_libav_logging() # Keep import here so that we can import stream integration without installing reqs - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .recorder import async_setup_recorder hass.data[DOMAIN] = {} @@ -405,7 +405,7 @@ class Stream: def _run_worker(self) -> None: """Handle consuming streams and restart keepalive streams.""" # Keep import here so that we can import stream integration without installing reqs - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .worker import StreamState, StreamWorkerError, stream_worker stream_state = StreamState(self.hass, self.outputs, self._diagnostics) @@ -501,7 +501,7 @@ class Stream: """Make a .mp4 recording from a provided stream.""" # Keep import here so that we can import stream integration without installing reqs - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .recorder import RecorderOutput # Check for file access diff --git a/homeassistant/components/stream/const.py b/homeassistant/components/stream/const.py index 35af633435e..eb954a6a8f5 100644 --- a/homeassistant/components/stream/const.py +++ b/homeassistant/components/stream/const.py @@ -31,7 +31,7 @@ EXT_X_START_LL_HLS = 2 PACKETS_TO_WAIT_FOR_AUDIO = 20 # Some streams have an audio stream with no audio -MAX_TIMESTAMP_GAP = 10000 # seconds - anything from 10 to 50000 is probably reasonable +MAX_TIMESTAMP_GAP = 30 # seconds - anything from 10 to 50000 is probably reasonable MAX_MISSING_DTS = 6 # Number of packets missing DTS to allow SOURCE_TIMEOUT = 30 # Timeout for reading stream source diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index a21a9f17d96..a499d0cc114 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -374,7 +374,6 @@ class StreamView(HomeAssistantView): """ requires_auth = False - platform = None async def get( self, request: web.Request, token: str, sequence: str = "", part_num: str = "" @@ -438,7 +437,7 @@ class KeyFrameConverter: """Initialize.""" # Keep import here so that we can import stream integration without installing reqs - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.components.camera.img_util import TurboJPEGSingleton self.packet: Packet = None @@ -461,7 +460,7 @@ class KeyFrameConverter: return # Keep import here so that we can import stream integration without installing reqs - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from av import CodecContext self._codec_context = CodecContext.create(codec_context.name, "r") diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index aefbbf698f1..bd7d90ee653 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -38,6 +38,7 @@ from .fmp4utils import read_init from .hls import HlsStreamOutput _LOGGER = logging.getLogger(__name__) +NEGATIVE_INF = float("-inf") class StreamWorkerError(Exception): @@ -416,14 +417,21 @@ class PeekIterator(Iterator): class TimestampValidator: """Validate ordering of timestamps for packets in a stream.""" - def __init__(self) -> None: + def __init__(self, inv_video_time_base: int, inv_audio_time_base: int) -> None: """Initialize the TimestampValidator.""" # Decompression timestamp of last packet in each stream self._last_dts: dict[av.stream.Stream, int | float] = defaultdict( - lambda: float("-inf") + lambda: NEGATIVE_INF ) # Number of consecutive missing decompression timestamps self._missing_dts = 0 + # For the bounds, just use the larger of the two values. If the error is not flagged + # by one stream, it should just get flagged by the other stream. Either value should + # result in a value which is much less than a 32 bit INT_MAX, which helps avoid the + # assertion error from FFmpeg. + self._max_dts_gap = MAX_TIMESTAMP_GAP * max( + inv_video_time_base, inv_audio_time_base + ) def is_valid(self, packet: av.Packet) -> bool: """Validate the packet timestamp based on ordering within the stream.""" @@ -438,13 +446,12 @@ class TimestampValidator: self._missing_dts = 0 # Discard when dts is not monotonic. Terminate if gap is too wide. prev_dts = self._last_dts[packet.stream] + if abs(prev_dts - packet.dts) > self._max_dts_gap and prev_dts != NEGATIVE_INF: + raise StreamWorkerError( + f"Timestamp discontinuity detected: last dts = {prev_dts}, dts =" + f" {packet.dts}" + ) if packet.dts <= prev_dts: - gap = packet.time_base * (prev_dts - packet.dts) - if gap > MAX_TIMESTAMP_GAP: - raise StreamWorkerError( - f"Timestamp overflow detected: last dts = {prev_dts}, dts =" - f" {packet.dts}" - ) return False self._last_dts[packet.stream] = packet.dts return True @@ -527,7 +534,10 @@ def stream_worker( if audio_stream: stream_state.diagnostics.set_value("audio_codec", audio_stream.name) - dts_validator = TimestampValidator() + dts_validator = TimestampValidator( + int(1 / video_stream.time_base), + 1 / audio_stream.time_base if audio_stream else 1, + ) container_packets = PeekIterator( filter(dts_validator.is_valid, container.demux((video_stream, audio_stream))) ) diff --git a/homeassistant/components/stt/__init__.py b/homeassistant/components/stt/__init__.py index 1d68b0a954b..94e08d25363 100644 --- a/homeassistant/components/stt/__init__.py +++ b/homeassistant/components/stt/__init__.py @@ -231,7 +231,8 @@ class SpeechToTextView(HomeAssistantView): def metadata_from_header(request: web.Request) -> SpeechMetadata: """Extract STT metadata from header. - X-Speech-Content: format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=1; language=de_de + X-Speech-Content: + format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=1; language=de_de """ try: data = request.headers[istr("X-Speech-Content")].split(";") diff --git a/homeassistant/components/subaru/sensor.py b/homeassistant/components/subaru/sensor.py index c5b2b86fda4..e0f8c243c27 100644 --- a/homeassistant/components/subaru/sensor.py +++ b/homeassistant/components/subaru/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, cast import subarulink.const as sc @@ -207,11 +207,11 @@ class SubaruSensor( return None if unit in LENGTH_UNITS: - return round(unit_system.length(current_value, unit), 1) + return round(unit_system.length(current_value, cast(str, unit)), 1) if unit in PRESSURE_UNITS and unit_system == US_CUSTOMARY_SYSTEM: return round( - unit_system.pressure(current_value, unit), + unit_system.pressure(current_value, cast(str, unit)), 1, ) diff --git a/homeassistant/components/subaru/translations/uk.json b/homeassistant/components/subaru/translations/uk.json new file mode 100644 index 00000000000..2aed6be91ba --- /dev/null +++ b/homeassistant/components/subaru/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sun/translations/tr.json b/homeassistant/components/sun/translations/tr.json index cff510523fa..f60c75e2993 100644 --- a/homeassistant/components/sun/translations/tr.json +++ b/homeassistant/components/sun/translations/tr.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } }, diff --git a/homeassistant/components/surepetcare/translations/uk.json b/homeassistant/components/surepetcare/translations/uk.json new file mode 100644 index 00000000000..337e9e7fa20 --- /dev/null +++ b/homeassistant/components/surepetcare/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch_as_x/translations/el.json b/homeassistant/components/switch_as_x/translations/el.json index cf5925abc35..9deb43500e1 100644 --- a/homeassistant/components/switch_as_x/translations/el.json +++ b/homeassistant/components/switch_as_x/translations/el.json @@ -4,11 +4,11 @@ "user": { "data": { "entity_id": "\u0394\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7\u03c2", - "target_domain": "\u03a4\u03cd\u03c0\u03bf\u03c2" + "target_domain": "\u039d\u03ad\u03bf\u03c2 \u03c4\u03cd\u03c0\u03bf\u03c2" }, "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7 \u03c0\u03bf\u03c5 \u03b8\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c4\u03bf Home Assistant \u03c9\u03c2 \u03c6\u03c9\u03c2, \u03ba\u03ac\u03bb\u03c5\u03bc\u03bc\u03b1 \u03ae \u03bf\u03c4\u03b9\u03b4\u03ae\u03c0\u03bf\u03c4\u03b5 \u03ac\u03bb\u03bb\u03bf. \u039f \u03b1\u03c1\u03c7\u03b9\u03ba\u03cc\u03c2 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7\u03c2 \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ba\u03c1\u03c5\u03bc\u03bc\u03ad\u03bd\u03bf\u03c2." } } }, - "title": "Switch as X" + "title": "\u0391\u03bb\u03bb\u03ac\u03be\u03c4\u03b5 \u03c4\u03bf\u03bd \u03c4\u03cd\u03c0\u03bf \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae\u03c2 \u03b5\u03bd\u03cc\u03c2 \u03b4\u03b9\u03b1\u03ba\u03cc\u03c0\u03c4\u03b7" } \ No newline at end of file diff --git a/homeassistant/components/switchbee/__init__.py b/homeassistant/components/switchbee/__init__.py index 79b2e449a14..352f191588a 100644 --- a/homeassistant/components/switchbee/__init__.py +++ b/homeassistant/components/switchbee/__init__.py @@ -2,8 +2,8 @@ from __future__ import annotations +from switchbee.api import CentralUnitPolling, CentralUnitWsRPC, is_wsrpc_api from switchbee.api.central_unit import SwitchBeeError -from switchbee.api.polling import CentralUnitPolling from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform @@ -25,17 +25,27 @@ PLATFORMS: list[Platform] = [ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SwitchBee Smart Home from a config entry.""" + hass.data.setdefault(DOMAIN, {}) central_unit = entry.data[CONF_HOST] user = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] websession = async_get_clientsession(hass, verify_ssl=False) - api = CentralUnitPolling(central_unit, user, password, websession) + api: CentralUnitPolling | CentralUnitWsRPC = CentralUnitPolling( + central_unit, user, password, websession + ) + + # First try to connect and fetch the version try: await api.connect() except SwitchBeeError as exp: raise ConfigEntryNotReady("Failed to connect to the Central Unit") from exp + # Check if websocket version + if is_wsrpc_api(api): + api = CentralUnitWsRPC(central_unit, user, password, websession) + await api.connect() + coordinator = SwitchBeeCoordinator( hass, api, diff --git a/homeassistant/components/switchbee/climate.py b/homeassistant/components/switchbee/climate.py index efc9c25d4bd..3b42287e89f 100644 --- a/homeassistant/components/switchbee/climate.py +++ b/homeassistant/components/switchbee/climate.py @@ -178,5 +178,5 @@ class SwitchBeeClimateEntity(SwitchBeeDeviceEntity[SwitchBeeThermostat], Climate raise HomeAssistantError( f"Failed to set {self.name} state {state}, error: {str(exp)}" ) from exp - else: - await self.coordinator.async_refresh() + + await self.coordinator.async_refresh() diff --git a/homeassistant/components/switchbee/const.py b/homeassistant/components/switchbee/const.py index 12cc5e77a63..c7792dfe645 100644 --- a/homeassistant/components/switchbee/const.py +++ b/homeassistant/components/switchbee/const.py @@ -1,4 +1,6 @@ """Constants for the SwitchBee Smart Home integration.""" +from switchbee.api import CentralUnitPolling, CentralUnitWsRPC + DOMAIN = "switchbee" -SCAN_INTERVAL_SEC = 5 +SCAN_INTERVAL_SEC = {CentralUnitWsRPC: 10, CentralUnitPolling: 5} diff --git a/homeassistant/components/switchbee/coordinator.py b/homeassistant/components/switchbee/coordinator.py index e8453afdbb8..b1b606615dd 100644 --- a/homeassistant/components/switchbee/coordinator.py +++ b/homeassistant/components/switchbee/coordinator.py @@ -6,11 +6,11 @@ from collections.abc import Mapping from datetime import timedelta import logging +from switchbee.api import CentralUnitPolling, CentralUnitWsRPC from switchbee.api.central_unit import SwitchBeeError -from switchbee.api.polling import CentralUnitPolling from switchbee.device import DeviceType, SwitchBeeBaseDevice -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -20,15 +20,15 @@ _LOGGER = logging.getLogger(__name__) class SwitchBeeCoordinator(DataUpdateCoordinator[Mapping[int, SwitchBeeBaseDevice]]): - """Class to manage fetching Freedompro data API.""" + """Class to manage fetching SwitchBee data API.""" def __init__( self, hass: HomeAssistant, - swb_api: CentralUnitPolling, + swb_api: CentralUnitPolling | CentralUnitWsRPC, ) -> None: """Initialize.""" - self.api: CentralUnitPolling = swb_api + self.api: CentralUnitPolling | CentralUnitWsRPC = swb_api self._reconnect_counts: int = 0 self.mac_formatted: str | None = ( None if self.api.mac is None else format_mac(self.api.mac) @@ -38,9 +38,20 @@ class SwitchBeeCoordinator(DataUpdateCoordinator[Mapping[int, SwitchBeeBaseDevic hass, _LOGGER, name=DOMAIN, - update_interval=timedelta(seconds=SCAN_INTERVAL_SEC), + update_interval=timedelta(seconds=SCAN_INTERVAL_SEC[type(self.api)]), ) + # Register callback for notification WsRPC + if isinstance(self.api, CentralUnitWsRPC): + self.api.subscribe_updates(self._async_handle_update) + + @callback + def _async_handle_update(self, push_data: dict) -> None: + """Manually update data and notify listeners.""" + assert isinstance(self.api, CentralUnitWsRPC) + _LOGGER.debug("Received update: %s", push_data) + self.async_set_updated_data(self.api.devices) + async def _async_update_data(self) -> Mapping[int, SwitchBeeBaseDevice]: """Update data via library.""" @@ -72,8 +83,8 @@ class SwitchBeeCoordinator(DataUpdateCoordinator[Mapping[int, SwitchBeeBaseDevic raise UpdateFailed( f"Error communicating with API: {exp}" ) from SwitchBeeError - else: - _LOGGER.debug("Loaded devices") + + _LOGGER.debug("Loaded devices") # Get the state of the devices try: diff --git a/homeassistant/components/switchbee/cover.py b/homeassistant/components/switchbee/cover.py index a9eed00801a..ac0de3622f1 100644 --- a/homeassistant/components/switchbee/cover.py +++ b/homeassistant/components/switchbee/cover.py @@ -81,7 +81,7 @@ class SwitchBeeCoverEntity(SwitchBeeDeviceEntity[SwitchBeeShutter], CoverEntity) | CoverEntityFeature.SET_POSITION | CoverEntityFeature.STOP ) - _attr_is_closed = None + _attr_is_closed: bool | None = None @callback def _handle_coordinator_update(self) -> None: diff --git a/homeassistant/components/switchbee/manifest.json b/homeassistant/components/switchbee/manifest.json index 27201e45090..659034d77ec 100644 --- a/homeassistant/components/switchbee/manifest.json +++ b/homeassistant/components/switchbee/manifest.json @@ -3,7 +3,7 @@ "name": "SwitchBee", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/switchbee", - "requirements": ["pyswitchbee==1.7.3"], + "requirements": ["pyswitchbee==1.7.19"], "codeowners": ["@jafar-atili"], - "iot_class": "local_polling" + "iot_class": "local_push" } diff --git a/homeassistant/components/switchbee/switch.py b/homeassistant/components/switchbee/switch.py index 9ed0e6ea9c0..f8f6e30e368 100644 --- a/homeassistant/components/switchbee/switch.py +++ b/homeassistant/components/switchbee/switch.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, TypeVar, Union +from typing import Any, TypeVar from switchbee.api.central_unit import SwitchBeeDeviceOfflineError, SwitchBeeError from switchbee.device import ( @@ -25,12 +25,12 @@ from .entity import SwitchBeeDeviceEntity _DeviceTypeT = TypeVar( "_DeviceTypeT", - bound=Union[ - SwitchBeeTimedSwitch, - SwitchBeeGroupSwitch, - SwitchBeeSwitch, - SwitchBeeTimerSwitch, - ], + bound=( + SwitchBeeTimedSwitch + | SwitchBeeGroupSwitch + | SwitchBeeSwitch + | SwitchBeeTimerSwitch + ), ) diff --git a/homeassistant/components/switchbee/translations/el.json b/homeassistant/components/switchbee/translations/el.json index e0e460c7823..3199c71599d 100644 --- a/homeassistant/components/switchbee/translations/el.json +++ b/homeassistant/components/switchbee/translations/el.json @@ -15,7 +15,7 @@ "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 \u03c4\u03b7\u03bd \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 SwitchBee \u03bc\u03b5 \u03c4\u03bf Home Assistant." + "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 SwitchBee \u03bc\u03b5 \u03c4\u03bf Home Assistant." } } } diff --git a/homeassistant/components/switchbee/translations/lv.json b/homeassistant/components/switchbee/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/switchbee/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbee/translations/uk.json b/homeassistant/components/switchbee/translations/uk.json new file mode 100644 index 00000000000..337e9e7fa20 --- /dev/null +++ b/homeassistant/components/switchbee/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/coordinator.py b/homeassistant/components/switchbot/coordinator.py index 77586c4202d..c12e8122e52 100644 --- a/homeassistant/components/switchbot/coordinator.py +++ b/homeassistant/components/switchbot/coordinator.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio import contextlib import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING import async_timeout import switchbot @@ -25,9 +25,7 @@ _LOGGER = logging.getLogger(__name__) DEVICE_STARTUP_TIMEOUT = 30 -class SwitchbotDataUpdateCoordinator( - ActiveBluetoothDataUpdateCoordinator[dict[str, Any]] -): +class SwitchbotDataUpdateCoordinator(ActiveBluetoothDataUpdateCoordinator[None]): """Class to manage fetching switchbot data.""" def __init__( @@ -79,9 +77,9 @@ class SwitchbotDataUpdateCoordinator( async def _async_update( self, service_info: bluetooth.BluetoothServiceInfoBleak - ) -> dict[str, Any]: + ) -> None: """Poll the device.""" - return await self.device.update() + await self.device.update() @callback def _async_handle_unavailable( diff --git a/homeassistant/components/switchbot/entity.py b/homeassistant/components/switchbot/entity.py index 60e7528dba6..c0e7a51170a 100644 --- a/homeassistant/components/switchbot/entity.py +++ b/homeassistant/components/switchbot/entity.py @@ -21,10 +21,11 @@ from .coordinator import SwitchbotDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) -class SwitchbotEntity(PassiveBluetoothCoordinatorEntity): +class SwitchbotEntity( + PassiveBluetoothCoordinatorEntity[SwitchbotDataUpdateCoordinator] +): """Generic entity encapsulating common features of Switchbot device.""" - coordinator: SwitchbotDataUpdateCoordinator _device: SwitchbotDevice _attr_has_entity_name = True diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 05dca82b3f9..a95a97d723e 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "requirements": ["PySwitchbot==0.36.4"], "config_flow": true, - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": [ "@bdraco", "@danielhiversen", diff --git a/homeassistant/components/switchbot/translations/bg.json b/homeassistant/components/switchbot/translations/bg.json index 218cc017cc8..a1ae86614b2 100644 --- a/homeassistant/components/switchbot/translations/bg.json +++ b/homeassistant/components/switchbot/translations/bg.json @@ -6,11 +6,20 @@ "no_unconfigured_devices": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u043d\u0435\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, + "error": { + "auth_failed": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f: {error_detail}" + }, "flow_title": "{name}", "step": { "confirm": { "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u0435 {name}?" }, + "lock_auth": { + "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" + } + }, "password": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u0430" diff --git a/homeassistant/components/switchbot/translations/ca.json b/homeassistant/components/switchbot/translations/ca.json index b117dd02c05..e00554484ab 100644 --- a/homeassistant/components/switchbot/translations/ca.json +++ b/homeassistant/components/switchbot/translations/ca.json @@ -7,11 +7,36 @@ "switchbot_unsupported_type": "Tipus de Switchbot no compatible.", "unknown": "Error inesperat" }, + "error": { + "auth_failed": "Ha fallat l'autenticaci\u00f3: {error_detail}", + "encryption_key_invalid": "L'ID de clau o la clau de xifrat no s\u00f3n v\u00e0lids" + }, "flow_title": "{name} ({address})", "step": { "confirm": { "description": "Vols configurar {name}?" }, + "lock_auth": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Proporciona el nom d'usuari i la contrasenya de l'aplicaci\u00f3 SwitchBot. Aquesta informaci\u00f3 no es desar\u00e0 i nom\u00e9s s'utilitzar\u00e0 per recuperar la clau de xifrat dels teus panys. Els noms d'usuari i contrasenyes distingeixen entre maj\u00fascules i min\u00fascules." + }, + "lock_choose_method": { + "description": "Els panys SwitchBot es poden configurar a Home Assistant de dues maneres diferents. \n\nPots introduir l'identificador de clau i la clau de xifrat, o b\u00e9, Home Assistant ho pot importar des del teu compte de SwitchBot.", + "menu_options": { + "lock_auth": "Compte de SwitchBot (recomanat)", + "lock_key": "Introdueix la clau de xifrat del pany manualment" + } + }, + "lock_key": { + "data": { + "encryption_key": "Clau de xifrat", + "key_id": "ID de clau" + }, + "description": "El dispositiu {name} necessita una clau de xifrat, els detalls sobre com obtenir-la es poden trobar a la documentaci\u00f3." + }, "password": { "data": { "password": "Contrasenya" diff --git a/homeassistant/components/switchbot/translations/de.json b/homeassistant/components/switchbot/translations/de.json index b6f637f36df..f3db6af838f 100644 --- a/homeassistant/components/switchbot/translations/de.json +++ b/homeassistant/components/switchbot/translations/de.json @@ -7,11 +7,36 @@ "switchbot_unsupported_type": "Nicht unterst\u00fctzter Switchbot Typ.", "unknown": "Unerwarteter Fehler" }, + "error": { + "auth_failed": "Authentifizierung fehlgeschlagen: {error_detail}", + "encryption_key_invalid": "Schl\u00fcssel-ID oder Verschl\u00fcsselungsschl\u00fcssel ist ung\u00fcltig" + }, "flow_title": "{name} ({address})", "step": { "confirm": { "description": "M\u00f6chtest du {name} einrichten?" }, + "lock_auth": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "description": "Bitte gib deinen Benutzernamen und Passwort f\u00fcr die SwitchBot App an. Diese Daten werden nicht gespeichert und nur zum Abrufen deines Verschl\u00fcsselungsschl\u00fcssels verwendet. Bei Benutzernamen und Passw\u00f6rtern wird zwischen Gro\u00df- und Kleinschreibung unterschieden." + }, + "lock_choose_method": { + "description": "Ein SwitchBot-Schloss kann im Home Assistant auf zwei verschiedene Arten eingerichtet werden.\n\nDu kannst die Schl\u00fcssel-ID und den Verschl\u00fcsselungscode selbst eingeben oder Home Assistant kann sie von deinem SwitchBot-Konto importieren.", + "menu_options": { + "lock_auth": "SwitchBot-Konto (empfohlen)", + "lock_key": "Verschl\u00fcsselungscode manuell eingeben" + } + }, + "lock_key": { + "data": { + "encryption_key": "Verschl\u00fcsselungsschl\u00fcssel", + "key_id": "Schl\u00fcssel-ID" + }, + "description": "Das Ger\u00e4t {name} ben\u00f6tigt einen Verschl\u00fcsselungsschl\u00fcssel. Einzelheiten dazu, wie du diesen erh\u00e4ltst, findest du in der Dokumentation." + }, "password": { "data": { "password": "Passwort" diff --git a/homeassistant/components/switchbot/translations/el.json b/homeassistant/components/switchbot/translations/el.json index 096a6d05fe9..072324c29a3 100644 --- a/homeassistant/components/switchbot/translations/el.json +++ b/homeassistant/components/switchbot/translations/el.json @@ -8,14 +8,37 @@ "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "error": { + "auth_failed": "\u0397 \u03c4\u03b1\u03c5\u03c4\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5", + "encryption_key_invalid": "\u03a4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03ae \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf", "one": "\u03ba\u03b5\u03bd\u03cc", "other": "\u03ba\u03b5\u03bd\u03cc" }, - "flow_title": "{name}", + "flow_title": "{name} ({address})", "step": { "confirm": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" }, + "lock_auth": { + "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": "\u039a\u03b1\u03c4\u03b1\u03c7\u03c9\u03c1\u03af\u03c3\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\u03b7\u03c2 \u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 SwitchBot. \u0391\u03c5\u03c4\u03ac \u03c4\u03b1 \u03b4\u03b5\u03b4\u03bf\u03bc\u03ad\u03bd\u03b1 \u03b4\u03b5\u03bd \u03b8\u03b1 \u03b1\u03c0\u03bf\u03b8\u03b7\u03ba\u03b5\u03c5\u03c4\u03bf\u03cd\u03bd \u03ba\u03b1\u03b9 \u03b8\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03b8\u03bf\u03cd\u03bd \u03bc\u03cc\u03bd\u03bf \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b1\u03c1\u03b9\u03ce\u03bd." + }, + "lock_choose_method": { + "description": "\u039c\u03b9\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b1\u03c1\u03b9\u03ac SwitchBot \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af \u03c3\u03c4\u03bf Home Assistant \u03bc\u03b5 \u03b4\u03cd\u03bf \u03b4\u03b9\u03b1\u03c6\u03bf\u03c1\u03b5\u03c4\u03b9\u03ba\u03bf\u03cd\u03c2 \u03c4\u03c1\u03cc\u03c0\u03bf\u03c5\u03c2.\n\n\u039c\u03c0\u03bf\u03c1\u03b5\u03af\u03c4\u03b5 \u03bd\u03b1 \u03b5\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03bc\u03cc\u03bd\u03bf\u03b9 \u03c3\u03b1\u03c2 \u03c4\u03bf \u03b1\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03b1\u03b9 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03ae \u03c4\u03bf Home Assistant \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c4\u03b1 \u03b5\u03b9\u03c3\u03ac\u03b3\u03b5\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf \u03bb\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc \u03c3\u03b1\u03c2 SwitchBot.", + "menu_options": { + "lock_auth": "\u039b\u03bf\u03b3\u03b1\u03c1\u03b9\u03b1\u03c3\u03bc\u03cc\u03c2 SwitchBot (\u03c3\u03c5\u03bd\u03b9\u03c3\u03c4\u03ac\u03c4\u03b1\u03b9)", + "lock_key": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2 \u03ba\u03bb\u03b5\u03b9\u03b4\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03bc\u03b5 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03bf \u03c4\u03c1\u03cc\u03c0\u03bf" + } + }, + "lock_key": { + "data": { + "encryption_key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2", + "key_id": "\u0391\u03bd\u03b1\u03b3\u03bd\u03c9\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd" + }, + "description": "\u0397 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae {name} \u03b1\u03c0\u03b1\u03b9\u03c4\u03b5\u03af \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2, \u03bb\u03b5\u03c0\u03c4\u03bf\u03bc\u03ad\u03c1\u03b5\u03b9\u03b5\u03c2 \u03c3\u03c7\u03b5\u03c4\u03b9\u03ba\u03ac \u03bc\u03b5 \u03c4\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03b1\u03c0\u03cc\u03ba\u03c4\u03b7\u03c3\u03ae\u03c2 \u03c4\u03bf\u03c5 \u03b2\u03c1\u03af\u03c3\u03ba\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7." + }, "password": { "data": { "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" diff --git a/homeassistant/components/switchbot/translations/en.json b/homeassistant/components/switchbot/translations/en.json index fed02f12a38..7f6aa974d05 100644 --- a/homeassistant/components/switchbot/translations/en.json +++ b/homeassistant/components/switchbot/translations/en.json @@ -59,4 +59,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/es.json b/homeassistant/components/switchbot/translations/es.json index 55a374de412..e4d01f19ebb 100644 --- a/homeassistant/components/switchbot/translations/es.json +++ b/homeassistant/components/switchbot/translations/es.json @@ -7,11 +7,36 @@ "switchbot_unsupported_type": "Tipo de Switchbot no compatible.", "unknown": "Error inesperado" }, + "error": { + "auth_failed": "Error de autenticaci\u00f3n: {error_detail}", + "encryption_key_invalid": "El ID de clave o la clave de cifrado no son v\u00e1lidos" + }, "flow_title": "{name} ({address})", "step": { "confirm": { "description": "\u00bfQuieres configurar {name}?" }, + "lock_auth": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "description": "Por favor, proporciona tu nombre de usuario y contrase\u00f1a de la aplicaci\u00f3n SwitchBot. Estos datos no se guardar\u00e1n y solo se utilizar\u00e1n para recuperar la clave de cifrado de su cerradura. Los nombres de usuario y las contrase\u00f1as distinguen entre may\u00fasculas y min\u00fasculas." + }, + "lock_choose_method": { + "description": "Se puede configurar una cerradura SwitchBot en Home Assistant de dos maneras diferentes. \n\nPuedes introducir la identificaci\u00f3n de la clave y la clave de cifrado t\u00fa mismo, o Home Assistant puede importarlos desde tu cuenta de SwitchBot.", + "menu_options": { + "lock_auth": "Cuenta SwitchBot (recomendado)", + "lock_key": "Introducir la clave de cifrado de la cerradura manualmente" + } + }, + "lock_key": { + "data": { + "encryption_key": "Clave de cifrado", + "key_id": "ID de clave" + }, + "description": "El dispositivo {name} requiere una clave de cifrado; los detalles sobre c\u00f3mo obtenerla se pueden encontrar en la documentaci\u00f3n." + }, "password": { "data": { "password": "Contrase\u00f1a" diff --git a/homeassistant/components/switchbot/translations/et.json b/homeassistant/components/switchbot/translations/et.json index ad76e84f976..35198a87cb3 100644 --- a/homeassistant/components/switchbot/translations/et.json +++ b/homeassistant/components/switchbot/translations/et.json @@ -7,11 +7,36 @@ "switchbot_unsupported_type": "Toetamata Switchboti t\u00fc\u00fcp.", "unknown": "Ootamatu t\u00f5rge" }, + "error": { + "auth_failed": "Tuvastamine nurjus: {error_detail}", + "encryption_key_invalid": "V\u00f5tme ID v\u00f5i kr\u00fcptov\u00f5ti on sobimatu" + }, "flow_title": "{name} ({address})", "step": { "confirm": { "description": "Kas seadistada {name} ?" }, + "lock_auth": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "Sisesta SwitchBot rakenduse kasutajanimi ja sals\u00f5na. Neid andmeid ei salvestata ja neid kasutatakse ainult lukkude kr\u00fcpteerimisv\u00f5tme k\u00e4ttesaamiseks. Kasutajanimi ja salas\u00f5na on t\u00f5stutundlikud." + }, + "lock_choose_method": { + "description": "SwitchBoti lukku saab Home Assistantis seadistada kahel erineval viisil. \n\n V\u00f5tme ID ja kr\u00fcpteerimisv\u00f5tme saad ise sisestada v\u00f5i Home Assistant saab need importida SwitchBoti kontolt.", + "menu_options": { + "lock_auth": "SwitchBoti konto (soovitatav)", + "lock_key": "Sisesta luku kr\u00fcptov\u00f5ti k\u00e4sitsi" + } + }, + "lock_key": { + "data": { + "encryption_key": "Kr\u00fcptimisv\u00f5ti", + "key_id": "V\u00f5tme ID" + }, + "description": "Seade {name} n\u00f5uab kr\u00fcptov\u00f5tit, \u00fcksikasjad selle hankimise kohta leiad dokumentatsioonist." + }, "password": { "data": { "password": "Salas\u00f5na" diff --git a/homeassistant/components/switchbot/translations/he.json b/homeassistant/components/switchbot/translations/he.json index 223bce5b5c7..6f5237614ea 100644 --- a/homeassistant/components/switchbot/translations/he.json +++ b/homeassistant/components/switchbot/translations/he.json @@ -7,6 +7,12 @@ }, "flow_title": "{name} ({address})", "step": { + "lock_auth": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9" + } + }, "password": { "data": { "password": "\u05e1\u05d9\u05e1\u05de\u05d4" diff --git a/homeassistant/components/switchbot/translations/hu.json b/homeassistant/components/switchbot/translations/hu.json index 52f020dcf1e..cd9132118b4 100644 --- a/homeassistant/components/switchbot/translations/hu.json +++ b/homeassistant/components/switchbot/translations/hu.json @@ -8,6 +8,8 @@ "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "error": { + "auth_failed": "Sikertelen volt a hiteles\u00edt\u00e9s: {error_detail}", + "encryption_key_invalid": "A kulcs azonos\u00edt\u00f3ja vagy a titkos\u00edt\u00e1si kulcs \u00e9rv\u00e9nytelen", "one": "\u00dcres", "other": "\u00dcres" }, @@ -16,6 +18,26 @@ "confirm": { "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}?" }, + "lock_auth": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v" + }, + "description": "K\u00e9rem, adja meg a SwitchBot alkalmaz\u00e1s felhaszn\u00e1l\u00f3nev\u00e9t \u00e9s jelszav\u00e1t. Ezek az adatok nem ker\u00fclnek elment\u00e9sre, \u00e9s csak a z\u00e1rak titkos\u00edt\u00e1si kulcs\u00e1nak lek\u00e9rdez\u00e9s\u00e9re haszn\u00e1ljuk fel. A felhaszn\u00e1l\u00f3nevek \u00e9s jelszavak eset\u00e9ben a nagy- \u00e9s kisbet\u0171k \u00e9rz\u00e9kenyek." + }, + "lock_choose_method": { + "menu_options": { + "lock_auth": "SwitchBot fi\u00f3k (aj\u00e1nlott)", + "lock_key": "Z\u00e1r titkos\u00edt\u00e1si kulcs k\u00e9zzel t\u00f6rt\u00e9n\u0151 megad\u00e1sa" + } + }, + "lock_key": { + "data": { + "encryption_key": "Titkos\u00edt\u00e1si kulcs", + "key_id": "Kulcs azonos\u00edt\u00f3" + }, + "description": "A(z) {name} eszk\u00f6z titkos\u00edt\u00e1si kulcsot ig\u00e9nyel, annak beszerz\u00e9s\u00e9vel kapcsolatos r\u00e9szletek a dokument\u00e1ci\u00f3ban tal\u00e1lhat\u00f3k." + }, "password": { "data": { "password": "Jelsz\u00f3" diff --git a/homeassistant/components/switchbot/translations/id.json b/homeassistant/components/switchbot/translations/id.json index c7f4cbcc466..cc84a5cb649 100644 --- a/homeassistant/components/switchbot/translations/id.json +++ b/homeassistant/components/switchbot/translations/id.json @@ -7,11 +7,36 @@ "switchbot_unsupported_type": "Jenis Switchbot yang tidak didukung.", "unknown": "Kesalahan yang tidak diharapkan" }, + "error": { + "auth_failed": "Autentikasi gagal: {error_detail}", + "encryption_key_invalid": "ID Kunci atau Kunci enkripsi tidak valid" + }, "flow_title": "{name} ({address})", "step": { "confirm": { "description": "Ingin menyiapkan {name}?" }, + "lock_auth": { + "data": { + "password": "Kata Sandi", + "username": "Nama Pengguna" + }, + "description": "Berikan nama pengguna dan kata sandi aplikasi SwitchBot Anda. Data ini tidak akan disimpan dan hanya digunakan untuk mengambil kunci enkripsi kunci Anda. Nama pengguna dan kata sandi peka huruf besar-kecil." + }, + "lock_choose_method": { + "description": "Kunci SwitchBot dapat diatur di Home Assistant dengan dua cara berbeda.\n\nAnda dapat memasukkan ID kunci dan kunci enkripsi sendiri, atau Home Assistant dapat mengimpornya dari akun SwitchBot Anda.", + "menu_options": { + "lock_auth": "Akun SwitchBot (disarankan)", + "lock_key": "Masukkan kunci enkripsi kunci secara manual" + } + }, + "lock_key": { + "data": { + "encryption_key": "Kunci enkripsi", + "key_id": "ID Kunci" + }, + "description": "Perangkat {name} memerlukan kunci enkripsi, detail tentang cara mendapatkannya dapat ditemukan di dokumentasi." + }, "password": { "data": { "password": "Kata Sandi" diff --git a/homeassistant/components/switchbot/translations/it.json b/homeassistant/components/switchbot/translations/it.json index 515c4bdd173..867c704fd42 100644 --- a/homeassistant/components/switchbot/translations/it.json +++ b/homeassistant/components/switchbot/translations/it.json @@ -8,6 +8,8 @@ "unknown": "Errore imprevisto" }, "error": { + "auth_failed": "Autenticazione non riuscita: {error_detail}", + "encryption_key_invalid": "L'ID chiave o la chiave crittografica non sono validi", "one": "Vuoto", "other": "Vuoti" }, @@ -16,6 +18,27 @@ "confirm": { "description": "Vuoi configurare {name}?" }, + "lock_auth": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Fornisci il nome utente e la password dell'app SwitchBot. Questi dati non verranno salvati e utilizzati solo per recuperare la chiave crittografica delle serrature. I nomi utente e le password fanno distinzione tra maiuscole e minuscole." + }, + "lock_choose_method": { + "description": "Una serratura SwitchBot pu\u00f2 essere impostata in Home Assistant in due modi diversi.\n\n\u00c8 possibile inserire personalmente l'id della chiave e la chiave di crittografia, oppure Home Assistant pu\u00f2 importarli dall'account SwitchBot.", + "menu_options": { + "lock_auth": "Account SwitchBot (consigliato)", + "lock_key": "Inserire manualmente la chiave di crittografia della serratura" + } + }, + "lock_key": { + "data": { + "encryption_key": "Chiave crittografica", + "key_id": "ID chiave" + }, + "description": "Il dispositivo {name} richiede una chiave di crittografia, i dettagli su come ottenerla sono disponibili nella documentazione." + }, "password": { "data": { "password": "Password" diff --git a/homeassistant/components/switchbot/translations/lv.json b/homeassistant/components/switchbot/translations/lv.json new file mode 100644 index 00000000000..9eea6cd040d --- /dev/null +++ b/homeassistant/components/switchbot/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured_device": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/nl.json b/homeassistant/components/switchbot/translations/nl.json index 66720f89013..2ee1c0165b1 100644 --- a/homeassistant/components/switchbot/translations/nl.json +++ b/homeassistant/components/switchbot/translations/nl.json @@ -7,15 +7,44 @@ "switchbot_unsupported_type": "Niet-ondersteund Switchbot-type.", "unknown": "Onverwachte fout" }, + "error": { + "auth_failed": "Authenticatie mislukt", + "encryption_key_invalid": "Sleutel ID of encryptiesleutel is ongeldig" + }, "flow_title": "{name}", "step": { "confirm": { "description": "Wilt u {name} instellen?" }, + "lock_auth": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "Geef alsjeblieft je SwitchBot App gebruikersnaam en wachtwoord. De data wordt niet opgeslagen en alleen gebruiktom de encryptiesleutels van je sloten op te vragen. Gebruikersnamen en wachtwoorden zijn hoofdlettergevoelig." + }, + "lock_choose_method": { + "description": "Een SwitchBot slot kan op twee manieren worden ingesteld in Home Assistant.\n\nJe kunt sleutel id and encryptiesleutel key zelf invoeren, of deze door Home Assistant laten importeren via je SwitchBot account.", + "menu_options": { + "lock_auth": "SwitchBot-account (aanbevolen)", + "lock_key": "Voer de encryptiesleutelvoor het slot handmatig in" + } + }, + "lock_key": { + "data": { + "encryption_key": "Encryptiesleutel", + "key_id": "Sleutel ID" + } + }, "password": { "data": { "password": "Wachtwoord" } + }, + "user": { + "data": { + "address": "Apparaatadres" + } } } }, diff --git a/homeassistant/components/switchbot/translations/no.json b/homeassistant/components/switchbot/translations/no.json index be6a932f116..65770ab216a 100644 --- a/homeassistant/components/switchbot/translations/no.json +++ b/homeassistant/components/switchbot/translations/no.json @@ -7,11 +7,36 @@ "switchbot_unsupported_type": "Switchbot-type st\u00f8ttes ikke.", "unknown": "Uventet feil" }, + "error": { + "auth_failed": "Autentisering mislyktes: {error_detail}", + "encryption_key_invalid": "N\u00f8kkel-ID eller krypteringsn\u00f8kkel er ugyldig" + }, "flow_title": "{name} ( {address} )", "step": { "confirm": { "description": "Vil du sette opp {name} ?" }, + "lock_auth": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Vennligst oppgi brukernavn og passord for SwitchBot-appen. Disse dataene vil ikke bli lagret og kun brukt til \u00e5 hente krypteringsn\u00f8kkelen for l\u00e5sene dine. Brukernavn og passord skiller mellom store og sm\u00e5 bokstaver." + }, + "lock_choose_method": { + "description": "En SwitchBot-l\u00e5s kan settes opp i Home Assistant p\u00e5 to forskjellige m\u00e5ter. \n\n Du kan angi n\u00f8kkel-ID og krypteringsn\u00f8kkel selv, eller Home Assistant kan importere dem fra SwitchBot-kontoen din.", + "menu_options": { + "lock_auth": "SwitchBot-konto (anbefalt)", + "lock_key": "Skriv inn l\u00e5skrypteringsn\u00f8kkelen manuelt" + } + }, + "lock_key": { + "data": { + "encryption_key": "Krypteringsn\u00f8kkel", + "key_id": "N\u00f8kkel-ID" + }, + "description": "{name} -enheten krever krypteringsn\u00f8kkel, detaljer om hvordan du f\u00e5r tak i den finner du i dokumentasjonen." + }, "password": { "data": { "password": "Passord" diff --git a/homeassistant/components/switchbot/translations/pl.json b/homeassistant/components/switchbot/translations/pl.json index ffe36408692..9f92cfec29d 100644 --- a/homeassistant/components/switchbot/translations/pl.json +++ b/homeassistant/components/switchbot/translations/pl.json @@ -8,6 +8,8 @@ "unknown": "Nieoczekiwany b\u0142\u0105d" }, "error": { + "auth_failed": "Uwierzytelnianie nie powiod\u0142o si\u0119: {error_detail}", + "encryption_key_invalid": "Identyfikator klucza lub klucz szyfruj\u0105cy jest nieprawid\u0142owy", "few": "Puste", "many": "Pustych", "one": "Pusty", @@ -18,6 +20,27 @@ "confirm": { "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, + "lock_auth": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Podaj swoj\u0105 nazw\u0119 u\u017cytkownika i has\u0142o do aplikacji SwitchBot. Te dane nie zostan\u0105 zapisane i zostan\u0105 u\u017cyte tylko do odzyskania klucza szyfrowania zamk\u00f3w. W nazwach u\u017cytkownik\u00f3w i has\u0142ach rozr\u00f3\u017cniana jest wielko\u015b\u0107 liter." + }, + "lock_choose_method": { + "description": "Zamek SwitchBot mo\u017cna skonfigurowa\u0107 w Home Assistant na dwa r\u00f3\u017cne sposoby. \n\nMo\u017cesz samodzielnie wprowadzi\u0107 identyfikator klucza i klucz szyfrowania lub Home Assistant mo\u017ce zaimportowa\u0107 je z konta SwitchBot.", + "menu_options": { + "lock_auth": "Konto SwitchBot (zalecane)", + "lock_key": "Wprowad\u017a klucz szyfrowania zamka r\u0119cznie" + } + }, + "lock_key": { + "data": { + "encryption_key": "Klucz szyfruj\u0105cy", + "key_id": "Identyfikator klucza" + }, + "description": "Urz\u0105dzenie {name} wymaga klucza szyfruj\u0105cego, szczeg\u00f3\u0142y jak go uzyska\u0107 znajdziesz w dokumentacji." + }, "password": { "data": { "password": "Has\u0142o" diff --git a/homeassistant/components/switchbot/translations/pt-BR.json b/homeassistant/components/switchbot/translations/pt-BR.json index 6fe4662469b..046ced89153 100644 --- a/homeassistant/components/switchbot/translations/pt-BR.json +++ b/homeassistant/components/switchbot/translations/pt-BR.json @@ -8,6 +8,8 @@ "unknown": "Erro inesperado" }, "error": { + "auth_failed": "Falha na autentica\u00e7\u00e3o: {error_detail}", + "encryption_key_invalid": "A chave ID ou Chave de Criptografia \u00e9 inv\u00e1lida", "one": "", "other": "" }, @@ -16,6 +18,27 @@ "confirm": { "description": "Deseja configurar {name}?" }, + "lock_auth": { + "data": { + "password": "Senha", + "username": "Usu\u00e1rio" + }, + "description": "Forne\u00e7a seu nome de usu\u00e1rio e senha do aplicativo SwitchBot. Esses dados n\u00e3o ser\u00e3o salvos e usados apenas para recuperar sua chave de criptografia de bloqueios. Nomes de usu\u00e1rio e senhas diferenciam mai\u00fasculas de min\u00fasculas." + }, + "lock_choose_method": { + "description": "Uma fechadura SwitchBot pode ser configurada no Home Assistant de duas maneiras diferentes. \n\n Voc\u00ea mesmo pode inserir o ID da chave e a chave de criptografia ou o Home Assistant pode import\u00e1-los da sua conta do SwitchBot.", + "menu_options": { + "lock_auth": "Conta SwitchBot (recomendado)", + "lock_key": "Insira a chave de criptografia de bloqueio manualmente" + } + }, + "lock_key": { + "data": { + "encryption_key": "Chave de encripta\u00e7\u00e3o", + "key_id": "Chave ID" + }, + "description": "O dispositivo {name} requer chave de criptografia, detalhes sobre como obt\u00ea-la podem ser encontrados na documenta\u00e7\u00e3o." + }, "password": { "data": { "password": "Senha" diff --git a/homeassistant/components/switchbot/translations/ru.json b/homeassistant/components/switchbot/translations/ru.json index 4bd32239c72..401a8efe280 100644 --- a/homeassistant/components/switchbot/translations/ru.json +++ b/homeassistant/components/switchbot/translations/ru.json @@ -7,11 +7,36 @@ "switchbot_unsupported_type": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0439 \u0442\u0438\u043f Switchbot.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, + "error": { + "auth_failed": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438: {error_detail}", + "encryption_key_invalid": "\u041d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f." + }, "flow_title": "{name} ({address})", "step": { "confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" }, + "lock_auth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 \u0438\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f SwitchBot. \u042d\u0442\u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u043d\u0435 \u0431\u0443\u0434\u0443\u0442 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u044b \u0438 \u0431\u0443\u0434\u0443\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0430 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0412\u0430\u0448\u0438\u0445 \u0437\u0430\u043c\u043a\u043e\u0432. \u0418\u043c\u0435\u043d\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439 \u0438 \u043f\u0430\u0440\u043e\u043b\u0438 \u0447\u0443\u0432\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u044c\u043d\u044b \u043a \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0443." + }, + "lock_choose_method": { + "description": "\u0417\u0430\u043c\u043e\u043a SwitchBot \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u0432 Home Assistant \u0434\u0432\u0443\u043c\u044f \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u043c\u0438 \u0441\u043f\u043e\u0441\u043e\u0431\u0430\u043c\u0438.\n\n\u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u0432\u0435\u0441\u0442\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0438 \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e, \u0438\u043b\u0438 Home Assistant \u043c\u043e\u0436\u0435\u0442 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0445 \u0438\u0437 \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 SwitchBot.", + "menu_options": { + "lock_auth": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c SwitchBot (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f)", + "lock_key": "\u0412\u0432\u0435\u0441\u0442\u0438 \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0437\u0430\u043c\u043a\u0430 \u0432\u0440\u0443\u0447\u043d\u0443\u044e" + } + }, + "lock_key": { + "data": { + "encryption_key": "\u041a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f", + "key_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430" + }, + "description": "\u0414\u043b\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 {name} \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f. \u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u0435\u0433\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c, \u043c\u043e\u0436\u043d\u043e \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438." + }, "password": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" diff --git a/homeassistant/components/switchbot/translations/sk.json b/homeassistant/components/switchbot/translations/sk.json index 3c42c629e7b..ed118145984 100644 --- a/homeassistant/components/switchbot/translations/sk.json +++ b/homeassistant/components/switchbot/translations/sk.json @@ -8,6 +8,8 @@ "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "error": { + "auth_failed": "Overenie zlyhalo: {error_detail}", + "encryption_key_invalid": "ID k\u013e\u00fa\u010da alebo \u0161ifrovac\u00ed k\u013e\u00fa\u010d je neplatn\u00fd", "few": "Pr\u00e1zdnych", "many": "Pr\u00e1zdnych", "one": "Pr\u00e1zdny", @@ -18,6 +20,27 @@ "confirm": { "description": "Chcete nastavi\u0165 {name}?" }, + "lock_auth": { + "data": { + "password": "Heslo", + "username": "Pou\u017e\u00edvate\u013esk\u00e9 meno" + }, + "description": "Zadajte svoje pou\u017e\u00edvate\u013esk\u00e9 meno a heslo aplik\u00e1cie SwitchBot. Tieto \u00fadaje sa neulo\u017eia a pou\u017eij\u00fa sa iba na z\u00edskanie \u0161ifrovacieho k\u013e\u00fa\u010da z\u00e1mkov. Pou\u017e\u00edvate\u013esk\u00e9 men\u00e1 a hesl\u00e1 rozli\u0161uj\u00fa ve\u013ek\u00e9 a mal\u00e9 p\u00edsmen\u00e1." + }, + "lock_choose_method": { + "description": "Z\u00e1mok SwitchBot je mo\u017en\u00e9 nastavi\u0165 v aplik\u00e1cii Home Assistant dvoma r\u00f4znymi sp\u00f4sobmi. \n\n ID k\u013e\u00fa\u010da a \u0161ifrovac\u00ed k\u013e\u00fa\u010d m\u00f4\u017eete zada\u0165 sami, alebo ich m\u00f4\u017ee Home Assistant importova\u0165 z v\u00e1\u0161ho \u00fa\u010dtu SwitchBot.", + "menu_options": { + "lock_auth": "\u00da\u010det SwitchBot (odpor\u00fa\u010dan\u00e9)", + "lock_key": "Zadanie \u0161ifrovacieho k\u013e\u00fa\u010da z\u00e1mku manu\u00e1lne" + } + }, + "lock_key": { + "data": { + "encryption_key": "\u0160ifrovac\u00ed k\u013e\u00fa\u010d", + "key_id": "ID k\u013e\u00fa\u010da" + }, + "description": "Zariadenie {name} vy\u017eaduje \u0161ifrovac\u00ed k\u013e\u00fa\u010d, podrobnosti o tom, ako ho z\u00edska\u0165, n\u00e1jdete v dokument\u00e1cii." + }, "password": { "data": { "password": "Heslo" diff --git a/homeassistant/components/switchbot/translations/sv.json b/homeassistant/components/switchbot/translations/sv.json index 7caefebe073..72ae8666939 100644 --- a/homeassistant/components/switchbot/translations/sv.json +++ b/homeassistant/components/switchbot/translations/sv.json @@ -12,6 +12,11 @@ "confirm": { "description": "Vill du konfigurera {name}?" }, + "lock_choose_method": { + "menu_options": { + "lock_key": "Ange krypteringsnyckel manuellt" + } + }, "password": { "data": { "password": "L\u00f6senord" diff --git a/homeassistant/components/switchbot/translations/tr.json b/homeassistant/components/switchbot/translations/tr.json index f135a79b549..17f2be9ce87 100644 --- a/homeassistant/components/switchbot/translations/tr.json +++ b/homeassistant/components/switchbot/translations/tr.json @@ -8,13 +8,36 @@ "unknown": "Beklenmeyen hata" }, "error": { + "auth_failed": "Kimlik do\u011frulama ba\u015far\u0131s\u0131z oldu: {error_detail}", + "encryption_key_invalid": "Anahtar Kimli\u011fi veya \u015eifreleme anahtar\u0131 ge\u00e7ersiz", "one": "Bo\u015f", "other": "Bo\u015f" }, "flow_title": "{name} ({address})", "step": { "confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" + }, + "lock_auth": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "L\u00fctfen SwitchBot uygulamas\u0131 kullan\u0131c\u0131 ad\u0131n\u0131z\u0131 ve \u015fifrenizi girin. Bu veriler kaydedilmeyecek ve yaln\u0131zca kilit \u015fifreleme anahtar\u0131n\u0131z\u0131 almak i\u00e7in kullan\u0131lacakt\u0131r. Kullan\u0131c\u0131 adlar\u0131 ve parolalar b\u00fcy\u00fck/k\u00fc\u00e7\u00fck harfe duyarl\u0131d\u0131r." + }, + "lock_choose_method": { + "description": "Home Asistan\u0131nda bir SwitchBot kilidi iki farkl\u0131 \u015fekilde ayarlanabilir. \n\n Anahtar kimli\u011fini ve \u015fifreleme anahtar\u0131n\u0131 kendiniz girebilir veya Home Assistant bunlar\u0131 SwitchBot hesab\u0131n\u0131zdan i\u00e7e aktarabilir.", + "menu_options": { + "lock_auth": "SwitchBot hesab\u0131 (\u00f6nerilir)", + "lock_key": "Kilit \u015fifreleme anahtar\u0131n\u0131 manuel olarak girin" + } + }, + "lock_key": { + "data": { + "encryption_key": "\u015eifreleme anahtar\u0131", + "key_id": "Anahtar Kimli\u011fi" + }, + "description": "{name} cihaz\u0131, \u015fifreleme anahtar\u0131 gerektirir, bunun nas\u0131l elde edilece\u011fine ili\u015fkin ayr\u0131nt\u0131lar belgelerde bulunabilir." }, "password": { "data": { diff --git a/homeassistant/components/switchbot/translations/uk.json b/homeassistant/components/switchbot/translations/uk.json new file mode 100644 index 00000000000..49e7e016f8c --- /dev/null +++ b/homeassistant/components/switchbot/translations/uk.json @@ -0,0 +1,28 @@ +{ + "config": { + "error": { + "auth_failed": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457: {error_detail}", + "encryption_key_invalid": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430 \u0430\u0431\u043e \u043a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0456" + }, + "step": { + "lock_auth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0423\u043a\u0430\u0436\u0456\u0442\u044c \u0456\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043e\u0434\u0430\u0442\u043a\u0430 SwitchBot. \u0426\u0456 \u0434\u0430\u043d\u0456 \u043d\u0435 \u0437\u0431\u0435\u0440\u0456\u0433\u0430\u0442\u0438\u043c\u0443\u0442\u044c\u0441\u044f \u0439 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u043c\u0443\u0442\u044c\u0441\u044f \u043b\u0438\u0448\u0435 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u043a\u043b\u044e\u0447\u0430 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u0430\u043c\u043a\u0456\u0432. \u0406\u043c\u0435\u043d\u0430 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0456\u0432 \u0456 \u043f\u0430\u0440\u043e\u043b\u0456 \u0447\u0443\u0442\u043b\u0438\u0432\u0456 \u0434\u043e \u0440\u0435\u0433\u0456\u0441\u0442\u0440\u0443." + }, + "lock_key": { + "data": { + "encryption_key": "\u041a\u043b\u044e\u0447 \u0448\u0438\u0444\u0440\u0443\u0432\u0430\u043d\u043d\u044f", + "key_id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u044e\u0447\u0430" + } + }, + "user": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switchbot/translations/zh-Hant.json b/homeassistant/components/switchbot/translations/zh-Hant.json index 43611eeb571..76516174c90 100644 --- a/homeassistant/components/switchbot/translations/zh-Hant.json +++ b/homeassistant/components/switchbot/translations/zh-Hant.json @@ -7,11 +7,36 @@ "switchbot_unsupported_type": "\u4e0d\u652f\u6301\u7684 Switchbot \u985e\u5225\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, + "error": { + "auth_failed": "\u9a57\u8b49\u5931\u6557\uff1a{error_detail}", + "encryption_key_invalid": "\u91d1\u9470 ID \u6216\u52a0\u5bc6\u91d1\u9470\u7121\u6548" + }, "flow_title": "{name} ({address})", "step": { "confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, + "lock_auth": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8acb\u63d0\u4f9b SwitchBot app \u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002\u6b64\u8cc7\u8a0a\u5c07\u4e0d\u6703\u9032\u884c\u5132\u5b58\u3001\u540c\u6642\u50c5\u4f7f\u7528\u65bc\u63a5\u6536\u9580\u9396\u52a0\u5bc6\u91d1\u9470\u3002\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u6709\u5927\u5c0f\u5beb\u4e4b\u5340\u5206\u3002" + }, + "lock_choose_method": { + "description": "\u6709\u5169\u7a2e\u4e0d\u540c\u65b9\u6cd5\u53ef\u4ee5\u5c07 SwitchBot \u88dd\u7f6e\u6574\u5408\u9032 Home Assistant\u3002\n\n\u53ef\u4ee5\u81ea\u884c\u8f38\u5165\u91d1\u9470 ID \u53ca\u52a0\u5bc6\u91d1\u9470\uff0c\u6216\u8005 Home Asssistant \u53ef\u4ee5\u7531 SwitchBot.com \u5e33\u865f\u9032\u884c\u532f\u5165\u3002", + "menu_options": { + "lock_auth": "SwitchBot \u5e33\u865f\uff08\u63a8\u85a6\uff09", + "lock_key": "\u624b\u52d5\u8f38\u5165\u9580\u9396\u52a0\u5bc6\u91d1\u9470" + } + }, + "lock_key": { + "data": { + "encryption_key": "\u52a0\u5bc6\u91d1\u9470", + "key_id": "\u91d1\u9470 ID" + }, + "description": "{name} \u88dd\u7f6e\u9700\u8981\u52a0\u5bc6\u91d1\u9470\u3001\u8acb\u53c3\u95b1\u6587\u4ef6\u4ee5\u4e86\u89e3\u5982\u4f55\u53d6\u5f97\u91d1\u9470\u7684\u8a73\u7d30\u8cc7\u8a0a\u3002" + }, "password": { "data": { "password": "\u5bc6\u78bc" diff --git a/homeassistant/components/switcher_kis/translations/tr.json b/homeassistant/components/switcher_kis/translations/tr.json index 3df15466f03..d8dbccfea8a 100644 --- a/homeassistant/components/switcher_kis/translations/tr.json +++ b/homeassistant/components/switcher_kis/translations/tr.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } } diff --git a/homeassistant/components/syncthing/__init__.py b/homeassistant/components/syncthing/__init__.py index 15f9bc7d307..1f492656166 100644 --- a/homeassistant/components/syncthing/__init__.py +++ b/homeassistant/components/syncthing/__init__.py @@ -172,5 +172,5 @@ class SyncthingClient: await self._client.system.ping() except aiosyncthing.exceptions.SyncthingError: return False - else: - return True + + return True diff --git a/homeassistant/components/syncthing/translations/el.json b/homeassistant/components/syncthing/translations/el.json index d31063f30d3..dc191a1c743 100644 --- a/homeassistant/components/syncthing/translations/el.json +++ b/homeassistant/components/syncthing/translations/el.json @@ -10,7 +10,7 @@ "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": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 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" diff --git a/homeassistant/components/syncthing/translations/hu.json b/homeassistant/components/syncthing/translations/hu.json index 90aca8becea..e17a63a484c 100644 --- a/homeassistant/components/syncthing/translations/hu.json +++ b/homeassistant/components/syncthing/translations/hu.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "title": "A szinkroniz\u00e1l\u00e1s integr\u00e1ci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa", + "title": "A Syncthing integr\u00e1ci\u00f3 be\u00e1ll\u00edt\u00e1sa", "token": "Token", "url": "URL", "verify_ssl": "Ellen\u0151rizze az SSL tan\u00fas\u00edtv\u00e1nyt" diff --git a/homeassistant/components/syncthru/__init__.py b/homeassistant/components/syncthru/__init__.py index 5031f485ab3..f77f68450a4 100644 --- a/homeassistant/components/syncthru/__init__.py +++ b/homeassistant/components/syncthru/__init__.py @@ -42,15 +42,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: exc_info=api_error, ) raise api_error - else: - # if the printer is offline, we raise an UpdateFailed - if printer.is_unknown_state(): - raise UpdateFailed( - f"Configured printer at {printer.url} does not respond." - ) - return printer - coordinator: DataUpdateCoordinator = DataUpdateCoordinator( + # if the printer is offline, we raise an UpdateFailed + if printer.is_unknown_state(): + raise UpdateFailed(f"Configured printer at {printer.url} does not respond.") + return printer + + coordinator = DataUpdateCoordinator[SyncThru]( hass, _LOGGER, name=DOMAIN, diff --git a/homeassistant/components/syncthru/binary_sensor.py b/homeassistant/components/syncthru/binary_sensor.py index 7517c99d2b9..66ed72a3fc9 100644 --- a/homeassistant/components/syncthru/binary_sensor.py +++ b/homeassistant/components/syncthru/binary_sensor.py @@ -38,9 +38,11 @@ async def async_setup_entry( ) -> None: """Set up from config entry.""" - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator: DataUpdateCoordinator[SyncThru] = hass.data[DOMAIN][ + config_entry.entry_id + ] - name = config_entry.data[CONF_NAME] + name: str = config_entry.data[CONF_NAME] entities = [ SyncThruOnlineSensor(coordinator, name), SyncThruProblemSensor(coordinator, name), @@ -49,14 +51,16 @@ async def async_setup_entry( async_add_entities(entities) -class SyncThruBinarySensor(CoordinatorEntity, BinarySensorEntity): +class SyncThruBinarySensor( + CoordinatorEntity[DataUpdateCoordinator[SyncThru]], BinarySensorEntity +): """Implementation of an abstract Samsung Printer binary sensor platform.""" - def __init__(self, coordinator, name): + def __init__(self, coordinator: DataUpdateCoordinator[SyncThru], name: str) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self.syncthru: SyncThru = coordinator.data - self._name = name + self.syncthru = coordinator.data + self._attr_name = name self._id_suffix = "" @property @@ -65,11 +69,6 @@ class SyncThruBinarySensor(CoordinatorEntity, BinarySensorEntity): serial = self.syncthru.serial_number() return f"{serial}{self._id_suffix}" if serial else None - @property - def name(self): - """Return the name of the sensor.""" - return self._name - @property def device_info(self) -> DeviceInfo | None: """Return device information.""" @@ -85,9 +84,9 @@ class SyncThruOnlineSensor(SyncThruBinarySensor): _attr_device_class = BinarySensorDeviceClass.CONNECTIVITY - def __init__(self, syncthru, name): + def __init__(self, coordinator: DataUpdateCoordinator[SyncThru], name: str) -> None: """Initialize the sensor.""" - super().__init__(syncthru, name) + super().__init__(coordinator, name) self._id_suffix = "_online" @property diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index 11e1403816e..58f1a5c2b3a 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -46,7 +46,9 @@ async def async_setup_entry( ) -> None: """Set up from config entry.""" - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + coordinator: DataUpdateCoordinator[SyncThru] = hass.data[DOMAIN][ + config_entry.entry_id + ] printer: SyncThru = coordinator.data supp_toner = printer.toner_status(filter_supported=True) @@ -54,7 +56,7 @@ async def async_setup_entry( supp_tray = printer.input_tray_status(filter_supported=True) supp_output_tray = printer.output_tray_status() - name = config_entry.data[CONF_NAME] + name: str = config_entry.data[CONF_NAME] entities: list[SyncThruSensor] = [ SyncThruMainSensor(coordinator, name), SyncThruActiveAlertSensor(coordinator, name), @@ -72,16 +74,16 @@ async def async_setup_entry( async_add_entities(entities) -class SyncThruSensor(CoordinatorEntity, SensorEntity): +class SyncThruSensor(CoordinatorEntity[DataUpdateCoordinator[SyncThru]], SensorEntity): """Implementation of an abstract Samsung Printer sensor platform.""" - def __init__(self, coordinator, name): + _attr_icon = "mdi:printer" + + def __init__(self, coordinator: DataUpdateCoordinator[SyncThru], name: str) -> None: """Initialize the sensor.""" super().__init__(coordinator) - self.syncthru: SyncThru = coordinator.data - self._name = name - self._icon = "mdi:printer" - self._unit_of_measurement = None + self.syncthru = coordinator.data + self._attr_name = name self._id_suffix = "" @property @@ -90,21 +92,6 @@ class SyncThruSensor(CoordinatorEntity, SensorEntity): serial = self.syncthru.serial_number() return f"{serial}{self._id_suffix}" if serial else None - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): - """Return the icon of the device.""" - return self._icon - - @property - def native_unit_of_measurement(self): - """Return the unit of measuremnt.""" - return self._unit_of_measurement - @property def device_info(self) -> DeviceInfo | None: """Return device information.""" @@ -123,7 +110,7 @@ class SyncThruMainSensor(SyncThruSensor): the displayed current status message. """ - def __init__(self, coordinator, name): + def __init__(self, coordinator: DataUpdateCoordinator[SyncThru], name: str) -> None: """Initialize the sensor.""" super().__init__(coordinator, name) self._id_suffix = "_main" @@ -149,12 +136,15 @@ class SyncThruMainSensor(SyncThruSensor): class SyncThruTonerSensor(SyncThruSensor): """Implementation of a Samsung Printer toner sensor platform.""" - def __init__(self, coordinator, name, color): + _attr_native_unit_of_measurement = PERCENTAGE + + def __init__( + self, coordinator: DataUpdateCoordinator[SyncThru], name: str, color: str + ) -> None: """Initialize the sensor.""" super().__init__(coordinator, name) - self._name = f"{name} Toner {color}" + self._attr_name = f"{name} Toner {color}" self._color = color - self._unit_of_measurement = PERCENTAGE self._id_suffix = f"_toner_{color}" @property @@ -171,12 +161,15 @@ class SyncThruTonerSensor(SyncThruSensor): class SyncThruDrumSensor(SyncThruSensor): """Implementation of a Samsung Printer drum sensor platform.""" - def __init__(self, syncthru, name, color): + _attr_native_unit_of_measurement = PERCENTAGE + + def __init__( + self, coordinator: DataUpdateCoordinator[SyncThru], name: str, color: str + ) -> None: """Initialize the sensor.""" - super().__init__(syncthru, name) - self._name = f"{name} Drum {color}" + super().__init__(coordinator, name) + self._attr_name = f"{name} Drum {color}" self._color = color - self._unit_of_measurement = PERCENTAGE self._id_suffix = f"_drum_{color}" @property @@ -193,10 +186,12 @@ class SyncThruDrumSensor(SyncThruSensor): class SyncThruInputTraySensor(SyncThruSensor): """Implementation of a Samsung Printer input tray sensor platform.""" - def __init__(self, syncthru, name, number): + def __init__( + self, coordinator: DataUpdateCoordinator[SyncThru], name: str, number: str + ) -> None: """Initialize the sensor.""" - super().__init__(syncthru, name) - self._name = f"{name} Tray {number}" + super().__init__(coordinator, name) + self._attr_name = f"{name} Tray {number}" self._number = number self._id_suffix = f"_tray_{number}" @@ -219,10 +214,12 @@ class SyncThruInputTraySensor(SyncThruSensor): class SyncThruOutputTraySensor(SyncThruSensor): """Implementation of a Samsung Printer output tray sensor platform.""" - def __init__(self, syncthru, name, number): + def __init__( + self, coordinator: DataUpdateCoordinator[SyncThru], name: str, number: int + ) -> None: """Initialize the sensor.""" - super().__init__(syncthru, name) - self._name = f"{name} Output Tray {number}" + super().__init__(coordinator, name) + self._attr_name = f"{name} Output Tray {number}" self._number = number self._id_suffix = f"_output_tray_{number}" @@ -245,10 +242,10 @@ class SyncThruOutputTraySensor(SyncThruSensor): class SyncThruActiveAlertSensor(SyncThruSensor): """Implementation of a Samsung Printer active alerts sensor platform.""" - def __init__(self, syncthru, name): + def __init__(self, coordinator: DataUpdateCoordinator[SyncThru], name: str) -> None: """Initialize the sensor.""" - super().__init__(syncthru, name) - self._name = f"{name} Active Alerts" + super().__init__(coordinator, name) + self._attr_name = f"{name} Active Alerts" self._id_suffix = "_active_alerts" @property diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 0b3001215a1..c17a26794df 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -84,12 +84,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # For SSDP compat if not entry.data.get(CONF_MAC): - network = await hass.async_add_executor_job(getattr, api.dsm, "network") hass.config_entries.async_update_entry( - entry, data={**entry.data, CONF_MAC: network.macs} + entry, data={**entry.data, CONF_MAC: api.dsm.network.macs} ) - # These all create executor jobs so we do not gather here coordinator_central = SynologyDSMCentralUpdateCoordinator(hass, entry, api) await coordinator_central.async_config_entry_first_refresh() diff --git a/homeassistant/components/synology_dsm/button.py b/homeassistant/components/synology_dsm/button.py index a1337e672f6..0b64985e7ad 100644 --- a/homeassistant/components/synology_dsm/button.py +++ b/homeassistant/components/synology_dsm/button.py @@ -1,7 +1,7 @@ """Support for Synology DSM buttons.""" from __future__ import annotations -from collections.abc import Callable +from collections.abc import Callable, Coroutine from dataclasses import dataclass import logging from typing import Any, Final @@ -27,7 +27,7 @@ LOGGER = logging.getLogger(__name__) class SynologyDSMbuttonDescriptionMixin: """Mixin to describe a Synology DSM button entity.""" - press_action: Callable[[SynoApi], Any] + press_action: Callable[[SynoApi], Callable[[], Coroutine[Any, Any, None]]] @dataclass @@ -43,14 +43,14 @@ BUTTONS: Final = [ name="Reboot", device_class=ButtonDeviceClass.RESTART, entity_category=EntityCategory.CONFIG, - press_action=lambda syno_api: syno_api.async_reboot(), + press_action=lambda syno_api: syno_api.async_reboot, ), SynologyDSMbuttonDescription( key="shutdown", name="Shutdown", icon="mdi:power", entity_category=EntityCategory.CONFIG, - press_action=lambda syno_api: syno_api.async_shutdown(), + press_action=lambda syno_api: syno_api.async_shutdown, ), ] @@ -92,4 +92,4 @@ class SynologyDSMButton(ButtonEntity): self.entity_description.key, self.syno_api.network.hostname, ) - await self.entity_description.press_action(self.syno_api) + await self.entity_description.press_action(self.syno_api)() diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 65520968e1d..b85ef5f2e3a 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -143,7 +143,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C self._listen_source_updates() await super().async_added_to_hass() - def camera_image( + async def async_camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return bytes of camera image.""" @@ -154,7 +154,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C if not self.available: return None try: - return self._api.surveillance_station.get_camera_image(self.entity_description.key, self.snapshot_quality) # type: ignore[no-any-return] + return await self._api.surveillance_station.get_camera_image(self.entity_description.key, self.snapshot_quality) # type: ignore[no-any-return] except ( SynologyDSMAPIErrorException, SynologyDSMRequestException, @@ -178,22 +178,22 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C return self.camera_data.live_view.rtsp # type: ignore[no-any-return] - def enable_motion_detection(self) -> None: + async def async_enable_motion_detection(self) -> None: """Enable motion detection in the camera.""" _LOGGER.debug( "SynoDSMCamera.enable_motion_detection(%s)", self.camera_data.name, ) - self._api.surveillance_station.enable_motion_detection( + await self._api.surveillance_station.enable_motion_detection( self.entity_description.key ) - def disable_motion_detection(self) -> None: + async def async_disable_motion_detection(self) -> None: """Disable motion detection in camera.""" _LOGGER.debug( "SynoDSMCamera.disable_motion_detection(%s)", self.camera_data.name, ) - self._api.surveillance_station.disable_motion_detection( + await self._api.surveillance_station.disable_motion_detection( self.entity_description.key ) diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 847fc6061c3..d315b3e49c0 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -30,6 +30,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_DEVICE_TOKEN, SYNOLOGY_CONNECTION_EXCEPTIONS @@ -71,21 +72,18 @@ class SynoApi: async def async_setup(self) -> None: """Start interacting with the NAS.""" - await self._hass.async_add_executor_job(self._setup) - - def _setup(self) -> None: - """Start interacting with the NAS in the executor.""" + session = async_get_clientsession(self._hass, self._entry.data[CONF_VERIFY_SSL]) self.dsm = SynologyDSM( + session, self._entry.data[CONF_HOST], self._entry.data[CONF_PORT], self._entry.data[CONF_USERNAME], self._entry.data[CONF_PASSWORD], self._entry.data[CONF_SSL], - self._entry.data[CONF_VERIFY_SSL], timeout=self._entry.options.get(CONF_TIMEOUT), device_token=self._entry.data.get(CONF_DEVICE_TOKEN), ) - self.dsm.login() + await self.dsm.login() # check if surveillance station is used self._with_surveillance_station = bool( @@ -93,7 +91,7 @@ class SynoApi: ) if self._with_surveillance_station: try: - self.dsm.surveillance_station.update() + await self.dsm.surveillance_station.update() except SYNOLOGY_CONNECTION_EXCEPTIONS: self._with_surveillance_station = False self.dsm.reset(SynoSurveillanceStation.API_KEY) @@ -110,16 +108,16 @@ class SynoApi: # check if upgrade is available try: - self.dsm.upgrade.update() + await self.dsm.upgrade.update() except SYNOLOGY_CONNECTION_EXCEPTIONS as ex: self._with_upgrade = False self.dsm.reset(SynoCoreUpgrade.API_KEY) LOGGER.debug("Disabled fetching upgrade data during setup: %s", ex) - self._fetch_device_configuration() + await self._fetch_device_configuration() try: - self._update() + await self._update() except SYNOLOGY_CONNECTION_EXCEPTIONS as err: LOGGER.debug( "Connection error during setup of '%s' with exception: %s", @@ -210,11 +208,11 @@ class SynoApi: self.dsm.reset(self.utilisation) self.utilisation = None - def _fetch_device_configuration(self) -> None: + async def _fetch_device_configuration(self) -> None: """Fetch initial device config.""" self.information = self.dsm.information self.network = self.dsm.network - self.network.update() + await self.network.update() if self._with_security: LOGGER.debug("Enable security api updates for '%s'", self._entry.unique_id) @@ -248,7 +246,7 @@ class SynoApi: async def _syno_api_executer(self, api_call: Callable) -> None: """Synology api call wrapper.""" try: - await self._hass.async_add_executor_job(api_call) + await api_call() except (SynologyDSMAPIErrorException, SynologyDSMRequestException) as err: LOGGER.debug( "Error from '%s': %s", self._entry.unique_id, err, exc_info=True @@ -274,7 +272,7 @@ class SynoApi: async def async_update(self) -> None: """Update function for updating API information.""" try: - await self._hass.async_add_executor_job(self._update) + await self._update() except SYNOLOGY_CONNECTION_EXCEPTIONS as err: LOGGER.debug( "Connection error during update of '%s' with exception: %s", @@ -286,8 +284,8 @@ class SynoApi: ) await self._hass.config_entries.async_reload(self._entry.entry_id) - def _update(self) -> None: + async def _update(self) -> None: """Update function for updating API information.""" LOGGER.debug("Start data update for '%s'", self._entry.unique_id) self._setup_api_requests() - self.dsm.update(self._with_information) + await self.dsm.update(self._with_information) diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index ba332ca7e7d..9342849b2fe 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Mapping from ipaddress import ip_address import logging -from typing import Any +from typing import Any, cast from urllib.parse import urlparse from synology_dsm import SynologyDSM @@ -18,7 +18,7 @@ from synology_dsm.exceptions import ( import voluptuous as vol from homeassistant import exceptions -from homeassistant.components import ssdp +from homeassistant.components import ssdp, zeroconf from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import ( CONF_DISKS, @@ -35,6 +35,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import DiscoveryInfoType @@ -56,6 +57,8 @@ _LOGGER = logging.getLogger(__name__) CONF_OTP_CODE = "otp_code" +HTTP_SUFFIX = "._http._tcp.local." + def _discovery_schema_with_defaults(discovery_info: DiscoveryInfoType) -> vol.Schema: return vol.Schema(_ordered_shared_schema(discovery_info)) @@ -104,6 +107,11 @@ def _is_valid_ip(text: str) -> bool: return True +def format_synology_mac(mac: str) -> str: + """Format a mac address to the format used by Synology DSM.""" + return mac.replace(":", "").replace("-", "").upper() + + class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" @@ -172,15 +180,12 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): else: port = DEFAULT_PORT - api = SynologyDSM( - host, port, username, password, use_ssl, verify_ssl, timeout=30 - ) + session = async_get_clientsession(self.hass, verify_ssl) + api = SynologyDSM(session, host, port, username, password, use_ssl, timeout=30) errors = {} try: - serial = await self.hass.async_add_executor_job( - _login_and_fetch_syno_info, api, otp_code - ) + serial = await _login_and_fetch_syno_info(api, otp_code) except SynologyDSMLogin2SARequiredException: return await self.async_step_2sa(user_input) except SynologyDSMLogin2SAFailedException: @@ -241,21 +246,42 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): return self._show_form(step) return await self.async_validate_input_create_entry(user_input, step_id=step) - async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: - """Handle a discovered synology_dsm.""" - parsed_url = urlparse(discovery_info.ssdp_location) - friendly_name = ( - discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME].split("(", 1)[0].strip() - ) + async def async_step_zeroconf( + self, discovery_info: zeroconf.ZeroconfServiceInfo + ) -> FlowResult: + """Handle a discovered synology_dsm via zeroconf.""" + discovered_macs = [ + format_synology_mac(mac) + for mac in discovery_info.properties.get("mac_address", "").split("|") + if mac + ] + if not discovered_macs: + return self.async_abort(reason="no_mac_address") + host = discovery_info.host + friendly_name = discovery_info.name.removesuffix(HTTP_SUFFIX) + return await self._async_from_discovery(host, friendly_name, discovered_macs) - discovered_mac = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL].upper() + async def async_step_ssdp(self, discovery_info: ssdp.SsdpServiceInfo) -> FlowResult: + """Handle a discovered synology_dsm via ssdp.""" + parsed_url = urlparse(discovery_info.ssdp_location) + upnp_friendly_name: str = discovery_info.upnp[ssdp.ATTR_UPNP_FRIENDLY_NAME] + friendly_name = upnp_friendly_name.split("(", 1)[0].strip() + mac_address = discovery_info.upnp[ssdp.ATTR_UPNP_SERIAL] + discovered_macs = [format_synology_mac(mac_address)] # Synology NAS can broadcast on multiple IP addresses, since they can be connected to multiple ethernets. # The serial of the NAS is actually its MAC address. + host = cast(str, parsed_url.hostname) + return await self._async_from_discovery(host, friendly_name, discovered_macs) - await self.async_set_unique_id(discovered_mac) - existing_entry = self._async_get_existing_entry(discovered_mac) - - if not existing_entry: + async def _async_from_discovery( + self, host: str, friendly_name: str, discovered_macs: list[str] + ) -> FlowResult: + """Handle a discovered synology_dsm via zeroconf or ssdp.""" + existing_entry = None + for discovered_mac in discovered_macs: + await self.async_set_unique_id(discovered_mac) + if existing_entry := self._async_get_existing_entry(discovered_mac): + break self._abort_if_unique_id_configured() fqdn_with_ssl_verification = ( @@ -266,18 +292,18 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): if ( existing_entry - and existing_entry.data[CONF_HOST] != parsed_url.hostname + and existing_entry.data[CONF_HOST] != host and not fqdn_with_ssl_verification ): _LOGGER.info( - "Update host from '%s' to '%s' for NAS '%s' via SSDP discovery", + "Update host from '%s' to '%s' for NAS '%s' via discovery", existing_entry.data[CONF_HOST], - parsed_url.hostname, + host, existing_entry.unique_id, ) self.hass.config_entries.async_update_entry( existing_entry, - data={**existing_entry.data, CONF_HOST: parsed_url.hostname}, + data={**existing_entry.data, CONF_HOST: host}, ) return self.async_abort(reason="reconfigure_successful") @@ -286,7 +312,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): self.discovered_conf = { CONF_NAME: friendly_name, - CONF_HOST: parsed_url.hostname, + CONF_HOST: host, } self.context["title_placeholders"] = self.discovered_conf return await self.async_step_link() @@ -341,7 +367,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN): """See if we already have a configured NAS with this MAC address.""" for entry in self._async_current_entries(): if discovered_mac in [ - mac.replace("-", "") for mac in entry.data.get(CONF_MAC, []) + format_synology_mac(mac) for mac in entry.data.get(CONF_MAC, []) ]: return entry return None @@ -386,13 +412,13 @@ class SynologyDSMOptionsFlowHandler(OptionsFlow): return self.async_show_form(step_id="init", data_schema=data_schema) -def _login_and_fetch_syno_info(api: SynologyDSM, otp_code: str | None) -> str: +async def _login_and_fetch_syno_info(api: SynologyDSM, otp_code: str | None) -> str: """Login to the NAS and fetch basic data.""" # These do i/o - api.login(otp_code) - api.utilisation.update() - api.storage.update() - api.network.update() + await api.login(otp_code) + await api.utilisation.update() + await api.storage.update() + await api.network.update() if ( not api.information.serial diff --git a/homeassistant/components/synology_dsm/coordinator.py b/homeassistant/components/synology_dsm/coordinator.py index 9090b945d44..3b25d93154a 100644 --- a/homeassistant/components/synology_dsm/coordinator.py +++ b/homeassistant/components/synology_dsm/coordinator.py @@ -5,7 +5,6 @@ from datetime import timedelta import logging from typing import Any, TypeVar -import async_timeout from synology_dsm.api.surveillance_station.camera import SynoCamera from synology_dsm.exceptions import SynologyDSMAPIErrorException @@ -64,20 +63,14 @@ class SynologyDSMSwitchUpdateCoordinator( async def async_setup(self) -> None: """Set up the coordinator initial data.""" - info = await self.hass.async_add_executor_job( - self.api.dsm.surveillance_station.get_info - ) + info = await self.api.dsm.surveillance_station.get_info() self.version = info["data"]["CMSMinVersion"] async def _async_update_data(self) -> dict[str, dict[str, Any]]: """Fetch all data from api.""" surveillance_station = self.api.surveillance_station return { - "switches": { - "home_mode": await self.hass.async_add_executor_job( - surveillance_station.get_home_mode_status - ) - } + "switches": {"home_mode": await surveillance_station.get_home_mode_status()} } @@ -131,8 +124,7 @@ class SynologyDSMCameraUpdateCoordinator( } try: - async with async_timeout.timeout(30): - await self.hass.async_add_executor_job(surveillance_station.update) + await surveillance_station.update() except SynologyDSMAPIErrorException as err: raise UpdateFailed(f"Error communicating with API: {err}") from err diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 7dcf7cf702a..6d0012156db 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["py-synologydsm-api==1.0.8"], + "requirements": ["py-synologydsm-api==2.0.2"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"], "config_flow": true, "ssdp": [ @@ -11,6 +11,9 @@ "deviceType": "urn:schemas-upnp-org:device:Basic:1" } ], + "zeroconf": [ + { "type": "_http._tcp.local.", "properties": { "vendor": "synology*" } } + ], "iot_class": "local_polling", "loggers": ["synology_dsm"] } diff --git a/homeassistant/components/synology_dsm/strings.json b/homeassistant/components/synology_dsm/strings.json index 09574f82f9e..f571b9c5326 100644 --- a/homeassistant/components/synology_dsm/strings.json +++ b/homeassistant/components/synology_dsm/strings.json @@ -44,6 +44,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { + "no_mac_address": "The MAC address is missing from the zeroconf record", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reconfigure_successful": "Re-configuration was successful" diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index 245b8804488..e44c578f4d2 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -87,9 +87,7 @@ class SynoDSMSurveillanceHomeModeToggle( "SynoDSMSurveillanceHomeModeToggle.turn_on(%s)", self._api.information.serial, ) - await self.hass.async_add_executor_job( - self._api.dsm.surveillance_station.set_home_mode, True - ) + await self._api.dsm.surveillance_station.set_home_mode(True) await self.coordinator.async_request_refresh() async def async_turn_off(self, **kwargs: Any) -> None: @@ -98,9 +96,7 @@ class SynoDSMSurveillanceHomeModeToggle( "SynoDSMSurveillanceHomeModeToggle.turn_off(%s)", self._api.information.serial, ) - await self.hass.async_add_executor_job( - self._api.dsm.surveillance_station.set_home_mode, False - ) + await self._api.dsm.surveillance_station.set_home_mode(False) await self.coordinator.async_request_refresh() @property diff --git a/homeassistant/components/synology_dsm/translations/ca.json b/homeassistant/components/synology_dsm/translations/ca.json index 110e1cabdae..b2da07856e8 100644 --- a/homeassistant/components/synology_dsm/translations/ca.json +++ b/homeassistant/components/synology_dsm/translations/ca.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", + "no_mac_address": "Falta l'adre\u00e7a MAC al registre zeroconf", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "reconfigure_successful": "Re-configuraci\u00f3 realitzada correctament" }, diff --git a/homeassistant/components/synology_dsm/translations/de.json b/homeassistant/components/synology_dsm/translations/de.json index e4b9041441d..902382507d3 100644 --- a/homeassistant/components/synology_dsm/translations/de.json +++ b/homeassistant/components/synology_dsm/translations/de.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "no_mac_address": "Die MAC-Adresse fehlt im Zeroconf-Eintrag", "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "reconfigure_successful": "Die Neukonfiguration war erfolgreich" }, diff --git a/homeassistant/components/synology_dsm/translations/el.json b/homeassistant/components/synology_dsm/translations/el.json index fc4e0124056..3975ee252d4 100644 --- a/homeassistant/components/synology_dsm/translations/el.json +++ b/homeassistant/components/synology_dsm/translations/el.json @@ -2,6 +2,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", + "no_mac_address": "\u0397 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 MAC \u03bb\u03b5\u03af\u03c0\u03b5\u03b9 \u03b1\u03c0\u03cc \u03c4\u03b7\u03bd \u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae zeroconf", "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" }, diff --git a/homeassistant/components/synology_dsm/translations/en.json b/homeassistant/components/synology_dsm/translations/en.json index 72eec8ff461..08d13e36d1c 100644 --- a/homeassistant/components/synology_dsm/translations/en.json +++ b/homeassistant/components/synology_dsm/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Device is already configured", + "no_mac_address": "The MAC address is missing from the zeroconf record", "reauth_successful": "Re-authentication was successful", "reconfigure_successful": "Re-configuration was successful" }, diff --git a/homeassistant/components/synology_dsm/translations/es.json b/homeassistant/components/synology_dsm/translations/es.json index e6c45511109..743ff5aee5e 100644 --- a/homeassistant/components/synology_dsm/translations/es.json +++ b/homeassistant/components/synology_dsm/translations/es.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", + "no_mac_address": "Falta la direcci\u00f3n MAC del registro zeroconf", "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", "reconfigure_successful": "La reconfiguraci\u00f3n se realiz\u00f3 correctamente" }, diff --git a/homeassistant/components/synology_dsm/translations/et.json b/homeassistant/components/synology_dsm/translations/et.json index 69341cab488..e13d498ce30 100644 --- a/homeassistant/components/synology_dsm/translations/et.json +++ b/homeassistant/components/synology_dsm/translations/et.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "no_mac_address": "Zeroconfi kirjest puudub MAC-aadress", "reauth_successful": "Taastuvastamine \u00f5nnestus", "reconfigure_successful": "\u00dcmberseadistamine \u00f5nnestus" }, diff --git a/homeassistant/components/synology_dsm/translations/id.json b/homeassistant/components/synology_dsm/translations/id.json index 204b7b372fa..d362ffbd71a 100644 --- a/homeassistant/components/synology_dsm/translations/id.json +++ b/homeassistant/components/synology_dsm/translations/id.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Perangkat sudah dikonfigurasi", + "no_mac_address": "Alamat MAC tidak ada dalam data zeroconf", "reauth_successful": "Autentikasi ulang berhasil", "reconfigure_successful": "Konfigurasi ulang berhasil" }, diff --git a/homeassistant/components/synology_dsm/translations/lv.json b/homeassistant/components/synology_dsm/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/no.json b/homeassistant/components/synology_dsm/translations/no.json index 3e0b77d51f9..efb9a4cb436 100644 --- a/homeassistant/components/synology_dsm/translations/no.json +++ b/homeassistant/components/synology_dsm/translations/no.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", + "no_mac_address": "MAC-adressen mangler fra zeroconf-posten", "reauth_successful": "Re-autentisering var vellykket", "reconfigure_successful": "Omkonfigurasjonen var vellykket" }, diff --git a/homeassistant/components/synology_dsm/translations/pt-BR.json b/homeassistant/components/synology_dsm/translations/pt-BR.json index 3410658fafe..f76dfa067a5 100644 --- a/homeassistant/components/synology_dsm/translations/pt-BR.json +++ b/homeassistant/components/synology_dsm/translations/pt-BR.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado", + "no_mac_address": "O endere\u00e7o MAC est\u00e1 faltando no registro zeroconf", "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", "reconfigure_successful": "A reconfigura\u00e7\u00e3o foi bem-sucedida" }, diff --git a/homeassistant/components/synology_dsm/translations/ru.json b/homeassistant/components/synology_dsm/translations/ru.json index 007504c4828..148beec7f84 100644 --- a/homeassistant/components/synology_dsm/translations/ru.json +++ b/homeassistant/components/synology_dsm/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.", + "no_mac_address": "MAC-\u0430\u0434\u0440\u0435\u0441 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0432 \u0437\u0430\u043f\u0438\u0441\u0438 zeroconf", "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.", "reconfigure_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, diff --git a/homeassistant/components/synology_dsm/translations/sk.json b/homeassistant/components/synology_dsm/translations/sk.json index b50e70872b5..d7ab2127ec3 100644 --- a/homeassistant/components/synology_dsm/translations/sk.json +++ b/homeassistant/components/synology_dsm/translations/sk.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Zariadenie je u\u017e nakonfigurovan\u00e9", + "no_mac_address": "V z\u00e1zname zeroconf ch\u00fdba adresa MAC", "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", "reconfigure_successful": "Rekonfigur\u00e1cia bola \u00faspe\u0161n\u00e1" }, diff --git a/homeassistant/components/synology_dsm/translations/tr.json b/homeassistant/components/synology_dsm/translations/tr.json index bcd2085218f..f986d1055c5 100644 --- a/homeassistant/components/synology_dsm/translations/tr.json +++ b/homeassistant/components/synology_dsm/translations/tr.json @@ -28,7 +28,7 @@ "username": "Kullan\u0131c\u0131 Ad\u0131", "verify_ssl": "SSL sertifikalar\u0131n\u0131 do\u011frula" }, - "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?" + "description": "{name} ( {host} ) kurmak istiyor musunuz?" }, "reauth_confirm": { "data": { diff --git a/homeassistant/components/synology_dsm/translations/uk.json b/homeassistant/components/synology_dsm/translations/uk.json index 21b0f4a68f0..bac1aa267d9 100644 --- a/homeassistant/components/synology_dsm/translations/uk.json +++ b/homeassistant/components/synology_dsm/translations/uk.json @@ -28,6 +28,12 @@ }, "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name} ({host})?" }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/synology_dsm/translations/zh-Hant.json b/homeassistant/components/synology_dsm/translations/zh-Hant.json index 504e1bec32d..b6986e1dc69 100644 --- a/homeassistant/components/synology_dsm/translations/zh-Hant.json +++ b/homeassistant/components/synology_dsm/translations/zh-Hant.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "no_mac_address": "Zeroconf \u7d00\u9304\u4e2d\u7f3a\u5c11 Mac \u4f4d\u5740", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", "reconfigure_successful": "\u91cd\u65b0\u8a2d\u5b9a\u6210\u529f" }, diff --git a/homeassistant/components/system_bridge/translations/lv.json b/homeassistant/components/system_bridge/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/system_bridge/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index b913993a4e1..a72451b0023 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -42,6 +42,7 @@ from .const import ( HA_TERMINATION_TYPE, HA_TO_TADO_FAN_MODE_MAP, HA_TO_TADO_HVAC_MODE_MAP, + HA_TO_TADO_SWING_MODE_MAP, ORDERED_KNOWN_TADO_MODES, SIGNAL_TADO_UPDATE_RECEIVED, SUPPORT_PRESET, @@ -52,6 +53,7 @@ from .const import ( TADO_TO_HA_FAN_MODE_MAP, TADO_TO_HA_HVAC_MODE_MAP, TADO_TO_HA_OFFSET_MAP, + TADO_TO_HA_SWING_MODE_MAP, TEMP_OFFSET, TYPE_AIR_CONDITIONING, TYPE_HEATING, @@ -456,13 +458,16 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): @property def swing_mode(self): """Active swing mode for the device.""" - return self._current_tado_swing_mode + return TADO_TO_HA_SWING_MODE_MAP[self._current_tado_swing_mode] @property def swing_modes(self): """Swing modes for the device.""" if self.supported_features & ClimateEntityFeature.SWING_MODE: - return [TADO_SWING_ON, TADO_SWING_OFF] + return [ + TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_ON], + TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_OFF], + ] return None @property @@ -479,7 +484,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): def set_swing_mode(self, swing_mode: str) -> None: """Set swing modes for the device.""" - self._control_hvac(swing_mode=swing_mode) + self._control_hvac(swing_mode=HA_TO_TADO_SWING_MODE_MAP[swing_mode]) @callback def _async_update_zone_data(self): diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index c547179f4e9..94d074c4066 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -18,6 +18,8 @@ from homeassistant.components.climate import ( FAN_OFF, PRESET_AWAY, PRESET_HOME, + SWING_OFF, + SWING_ON, HVACAction, HVACMode, ) @@ -157,6 +159,15 @@ SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME] TADO_SWING_OFF = "OFF" TADO_SWING_ON = "ON" +HA_TO_TADO_SWING_MODE_MAP = { + SWING_OFF: TADO_SWING_OFF, + SWING_ON: TADO_SWING_ON, +} + +TADO_TO_HA_SWING_MODE_MAP = { + value: key for key, value in HA_TO_TADO_SWING_MODE_MAP.items() +} + DOMAIN = "tado" SIGNAL_TADO_UPDATE_RECEIVED = "tado_update_received_{}_{}_{}" diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index fdcc9c7db3e..4289813494a 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -268,7 +268,7 @@ class TadoZoneSensor(TadoZoneEntity, SensorEntity): @property def state_class(self): """Return the state class.""" - if self.zone_variable in ["ac", "heating", "humidity", "temperature"]: + if self.zone_variable in ["heating", "humidity", "temperature"]: return SensorStateClass.MEASUREMENT return None diff --git a/homeassistant/components/tado/translations/el.json b/homeassistant/components/tado/translations/el.json index 7fca0f12f44..ef949e79edd 100644 --- a/homeassistant/components/tado/translations/el.json +++ b/homeassistant/components/tado/translations/el.json @@ -23,7 +23,7 @@ "step": { "init": { "data": { - "fallback": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b5\u03bd\u03b1\u03bb\u03bb\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2." + "fallback": "\u0395\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03b5\u03bd\u03b1\u03bb\u03bb\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2." }, "description": "\u0397 \u03b5\u03bd\u03b1\u03bb\u03bb\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b8\u03b1 \u03b1\u03bb\u03bb\u03ac\u03be\u03b5\u03b9 \u03c3\u03b5 \u0388\u03be\u03c5\u03c0\u03bd\u03bf \u03c7\u03c1\u03bf\u03bd\u03bf\u03b4\u03b9\u03ac\u03b3\u03c1\u03b1\u03bc\u03bc\u03b1 \u03c3\u03c4\u03b7\u03bd \u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03b7 \u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03c7\u03c1\u03bf\u03bd\u03bf\u03b4\u03b9\u03b1\u03b3\u03c1\u03ac\u03bc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03bc\u03b5\u03c4\u03ac \u03c4\u03b7 \u03bc\u03b7 \u03b1\u03c5\u03c4\u03cc\u03bc\u03b1\u03c4\u03b7 \u03c0\u03c1\u03bf\u03c3\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03bc\u03b9\u03b1\u03c2 \u03b6\u03ce\u03bd\u03b7\u03c2.", "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03b9\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2 Tado." diff --git a/homeassistant/components/tado/translations/lv.json b/homeassistant/components/tado/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/tado/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tailscale/strings.json b/homeassistant/components/tailscale/strings.json index 0ac0db0ef08..c03b5a3f841 100644 --- a/homeassistant/components/tailscale/strings.json +++ b/homeassistant/components/tailscale/strings.json @@ -20,6 +20,7 @@ "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" }, "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } diff --git a/homeassistant/components/tailscale/translations/bg.json b/homeassistant/components/tailscale/translations/bg.json index 8ec410d2f18..d9adb73b534 100644 --- a/homeassistant/components/tailscale/translations/bg.json +++ b/homeassistant/components/tailscale/translations/bg.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430" }, "error": { diff --git a/homeassistant/components/tailscale/translations/ca.json b/homeassistant/components/tailscale/translations/ca.json index a339ce6a22b..503451277f7 100644 --- a/homeassistant/components/tailscale/translations/ca.json +++ b/homeassistant/components/tailscale/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El servei ja est\u00e0 configurat", "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { diff --git a/homeassistant/components/tailscale/translations/de.json b/homeassistant/components/tailscale/translations/de.json index fca05d46a6e..c2b0ec85fb0 100644 --- a/homeassistant/components/tailscale/translations/de.json +++ b/homeassistant/components/tailscale/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { diff --git a/homeassistant/components/tailscale/translations/el.json b/homeassistant/components/tailscale/translations/el.json index 0c08e11eee8..8074ae53232 100644 --- a/homeassistant/components/tailscale/translations/el.json +++ b/homeassistant/components/tailscale/translations/el.json @@ -19,7 +19,7 @@ "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)." + "description": "\u0391\u03c5\u03c4\u03ae \u03b7 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af \u03c4\u03bf \u03b4\u03af\u03ba\u03c4\u03c5\u03cc \u03c3\u03b1\u03c2 Tailscale, **\u0394\u0395\u039d** \u03ba\u03ac\u03bd\u03b5\u03b9 \u03c4\u03bf Home Assistant \u03c3\u03b1\u03c2 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03bf \u03bc\u03ad\u03c3\u03c9 \u03c4\u03bf\u03c5 Tailscale VPN. \n\n \u0393\u03b9\u03b1 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03bc\u03b5 \u03c4\u03bf Tailscale, \u03b8\u03b1 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \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\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {authkeys_url} . \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 Tailscale (\u03b4\u03af\u03c0\u03bb\u03b1 \u03b1\u03c0\u03cc \u03c4\u03bf \u03bb\u03bf\u03b3\u03cc\u03c4\u03c5\u03c0\u03bf Tailscale)." } } } diff --git a/homeassistant/components/tailscale/translations/en.json b/homeassistant/components/tailscale/translations/en.json index dd607f6360d..1a7379290f8 100644 --- a/homeassistant/components/tailscale/translations/en.json +++ b/homeassistant/components/tailscale/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Service is already configured", "reauth_successful": "Re-authentication was successful" }, "error": { diff --git a/homeassistant/components/tailscale/translations/et.json b/homeassistant/components/tailscale/translations/et.json index 1f8a677f2af..c0ce2f23760 100644 --- a/homeassistant/components/tailscale/translations/et.json +++ b/homeassistant/components/tailscale/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Teenus on juba seadistatud", "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { diff --git a/homeassistant/components/tailscale/translations/no.json b/homeassistant/components/tailscale/translations/no.json index 609589aec6e..73ed14ed157 100644 --- a/homeassistant/components/tailscale/translations/no.json +++ b/homeassistant/components/tailscale/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Tjenesten er allerede konfigurert", "reauth_successful": "Re-autentisering var vellykket" }, "error": { diff --git a/homeassistant/components/tailscale/translations/ru.json b/homeassistant/components/tailscale/translations/ru.json index c05e3a09eac..338d1120e6d 100644 --- a/homeassistant/components/tailscale/translations/ru.json +++ b/homeassistant/components/tailscale/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant.", "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { diff --git a/homeassistant/components/tailscale/translations/zh-Hant.json b/homeassistant/components/tailscale/translations/zh-Hant.json index 316168fa67a..d29562d0819 100644 --- a/homeassistant/components/tailscale/translations/zh-Hant.json +++ b/homeassistant/components/tailscale/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { diff --git a/homeassistant/components/tellstick/__init__.py b/homeassistant/components/tellstick/__init__.py index 1007867362a..42685f03e03 100644 --- a/homeassistant/components/tellstick/__init__.py +++ b/homeassistant/components/tellstick/__init__.py @@ -40,10 +40,6 @@ SIGNAL_TELLCORE_CALLBACK = "tellstick_callback" # calling concurrently. TELLSTICK_LOCK = threading.RLock() -# A TellstickRegistry that keeps a map from tellcore_id to the corresponding -# tellcore_device and HA device (entity). -TELLCORE_REGISTRY = None - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index fd867527153..3b79ed62237 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -33,7 +33,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -TEMPER_SENSORS = [] +TEMPER_SENSORS: list[TemperSensor] = [] def get_temper_devices(): diff --git a/homeassistant/components/template/trigger.py b/homeassistant/components/template/trigger.py index e6266a07077..0cc53d5fb2d 100644 --- a/homeassistant/components/template/trigger.py +++ b/homeassistant/components/template/trigger.py @@ -73,7 +73,7 @@ async def async_attach_trigger( return if delay_cancel: - # pylint: disable=not-callable + # pylint: disable-next=not-callable delay_cancel() delay_cancel = None @@ -149,7 +149,7 @@ async def async_attach_trigger( """Remove state listeners async.""" unsub() if delay_cancel: - # pylint: disable=not-callable + # pylint: disable-next=not-callable delay_cancel() return async_remove diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index ef02f208e8c..90cdc979262 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -7,7 +7,7 @@ "tf-models-official==2.5.0", "pycocotools==2.0.1", "numpy==1.23.2", - "pillow==9.3.0" + "pillow==9.4.0" ], "codeowners": [], "iot_class": "local_polling", diff --git a/homeassistant/components/tesla_wall_connector/translations/lv.json b/homeassistant/components/tesla_wall_connector/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/tesla_wall_connector/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/text/translations/tr.json b/homeassistant/components/text/translations/tr.json new file mode 100644 index 00000000000..09e61f02845 --- /dev/null +++ b/homeassistant/components/text/translations/tr.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "{entity_name} i\u00e7in de\u011fer belirleyin" + } + }, + "title": "Metin" +} \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/manifest.json b/homeassistant/components/thermobeacon/manifest.json index 01e66bbbb94..fbd53c8ab57 100644 --- a/homeassistant/components/thermobeacon/manifest.json +++ b/homeassistant/components/thermobeacon/manifest.json @@ -37,7 +37,7 @@ { "local_name": "ThermoBeacon", "connectable": false } ], "requirements": ["thermobeacon-ble==0.6.0"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "iot_class": "local_push" } diff --git a/homeassistant/components/thermobeacon/sensor.py b/homeassistant/components/thermobeacon/sensor.py index b651fb2cea0..6cd80bad7a3 100644 --- a/homeassistant/components/thermobeacon/sensor.py +++ b/homeassistant/components/thermobeacon/sensor.py @@ -1,8 +1,6 @@ """Support for ThermoBeacon sensors.""" from __future__ import annotations -from typing import Optional, Union - from thermobeacon_ble import ( SensorDeviceClass as ThermoBeaconSensorDeviceClass, SensorUpdate, @@ -128,9 +126,7 @@ async def async_setup_entry( class ThermoBeaconBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a ThermoBeacon sensor.""" diff --git a/homeassistant/components/thermobeacon/translations/lv.json b/homeassistant/components/thermobeacon/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermobeacon/translations/tr.json b/homeassistant/components/thermobeacon/translations/tr.json index f0ddbc274c9..36347c44f7f 100644 --- a/homeassistant/components/thermobeacon/translations/tr.json +++ b/homeassistant/components/thermobeacon/translations/tr.json @@ -9,13 +9,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/thermobeacon/translations/uk.json b/homeassistant/components/thermobeacon/translations/uk.json new file mode 100644 index 00000000000..e58b49d4c9e --- /dev/null +++ b/homeassistant/components/thermobeacon/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/manifest.json b/homeassistant/components/thermopro/manifest.json index dca643a28cf..e9135a44324 100644 --- a/homeassistant/components/thermopro/manifest.json +++ b/homeassistant/components/thermopro/manifest.json @@ -7,7 +7,7 @@ { "local_name": "TP35*", "connectable": false }, { "local_name": "TP39*", "connectable": false } ], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "requirements": ["thermopro-ble==0.4.3"], "codeowners": ["@bdraco"], "iot_class": "local_push" diff --git a/homeassistant/components/thermopro/sensor.py b/homeassistant/components/thermopro/sensor.py index 8a2e68f1625..107385615f8 100644 --- a/homeassistant/components/thermopro/sensor.py +++ b/homeassistant/components/thermopro/sensor.py @@ -1,8 +1,6 @@ """Support for thermopro ble sensors.""" from __future__ import annotations -from typing import Optional, Union - from thermopro_ble import ( DeviceKey, SensorDeviceClass as ThermoProSensorDeviceClass, @@ -117,9 +115,7 @@ async def async_setup_entry( class ThermoProBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a thermopro ble sensor.""" diff --git a/homeassistant/components/thermopro/translations/lv.json b/homeassistant/components/thermopro/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/thermopro/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/thermopro/translations/tr.json b/homeassistant/components/thermopro/translations/tr.json index f63cee3493c..66d94aa9414 100644 --- a/homeassistant/components/thermopro/translations/tr.json +++ b/homeassistant/components/thermopro/translations/tr.json @@ -8,13 +8,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/thread/__init__.py b/homeassistant/components/thread/__init__.py new file mode 100644 index 00000000000..4da54e2c88a --- /dev/null +++ b/homeassistant/components/thread/__init__.py @@ -0,0 +1,30 @@ +"""The Thread integration.""" +from __future__ import annotations + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType + +from .const import DOMAIN + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Thread integration.""" + if not hass.config_entries.async_entries(DOMAIN): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT} + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return True diff --git a/homeassistant/components/thread/config_flow.py b/homeassistant/components/thread/config_flow.py new file mode 100644 index 00000000000..978b4c10779 --- /dev/null +++ b/homeassistant/components/thread/config_flow.py @@ -0,0 +1,19 @@ +"""Config flow for the Thread integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + + +class ThreadConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Thread.""" + + VERSION = 1 + + async def async_step_import( + self, import_data: dict[str, str] | None = None + ) -> FlowResult: + """Set up by import from async_setup.""" + return self.async_create_entry(title="Thread", data={}) diff --git a/homeassistant/components/thread/const.py b/homeassistant/components/thread/const.py new file mode 100644 index 00000000000..e8fe950ca6d --- /dev/null +++ b/homeassistant/components/thread/const.py @@ -0,0 +1,3 @@ +"""Constants for the Thread integration.""" + +DOMAIN = "thread" diff --git a/homeassistant/components/thread/manifest.json b/homeassistant/components/thread/manifest.json new file mode 100644 index 00000000000..5eec75e6223 --- /dev/null +++ b/homeassistant/components/thread/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "thread", + "name": "Thread", + "codeowners": ["@home-assistant/core"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/thread", + "integration_type": "service", + "iot_class": "local_polling" +} diff --git a/homeassistant/components/threshold/translations/el.json b/homeassistant/components/threshold/translations/el.json index 11735c53908..9ca372214cd 100644 --- a/homeassistant/components/threshold/translations/el.json +++ b/homeassistant/components/threshold/translations/el.json @@ -12,8 +12,8 @@ "name": "\u038c\u03bd\u03bf\u03bc\u03b1", "upper": "\u0386\u03bd\u03c9 \u03cc\u03c1\u03b9\u03bf" }, - "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03c0\u03cc\u03c4\u03b5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03bf \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2. \n\n \u039c\u03cc\u03bd\u03bf \u03c4\u03bf \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af - \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03ba\u03c1\u03cc\u03c4\u03b5\u03c1\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf.\n \u039c\u03cc\u03bd\u03bf \u03c4\u03bf \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af - \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b5\u03b3\u03b1\u03bb\u03cd\u03c4\u03b5\u03c1\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf.\n \u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf\u03c5 \u03ba\u03b1\u03b9 \u03ac\u03bd\u03c9 \u03bf\u03c1\u03af\u03bf\u03c5 - \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c4\u03bf \u03b5\u03cd\u03c1\u03bf\u03c2 [\u03ba\u03ac\u03c4\u03c9 \u03cc\u03c1\u03b9\u03bf .. \u03ac\u03bd\u03c9 \u03cc\u03c1\u03b9\u03bf].", - "title": "\u039d\u03ad\u03bf\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03bf\u03c1\u03af\u03bf\u03c5" + "description": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b4\u03c5\u03b1\u03b4\u03b9\u03ba\u03cc \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03b1\u03b9 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03bd\u03ac\u03bb\u03bf\u03b3\u03b1 \u03bc\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b9\u03bc\u03ae \u03b5\u03bd\u03cc\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \n\n \u039c\u03cc\u03bd\u03bf \u03c4\u03bf \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af - \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03ba\u03c1\u03cc\u03c4\u03b5\u03c1\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf.\n \u039c\u03cc\u03bd\u03bf \u03c4\u03bf \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af - \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b5\u03b3\u03b1\u03bb\u03cd\u03c4\u03b5\u03c1\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf.\n \u0388\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af \u03c4\u03cc\u03c3\u03bf \u03c4\u03bf \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c3\u03bf \u03ba\u03b1\u03b9 \u03c4\u03bf \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf - \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c4\u03bf \u03b5\u03cd\u03c1\u03bf\u03c2 [\u03ba\u03ac\u03c4\u03c9 \u03cc\u03c1\u03b9\u03bf .. \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf].", + "title": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03ba\u03b1\u03c4\u03c9\u03c6\u03bb\u03af\u03bf\u03c5" } } }, @@ -30,7 +30,7 @@ "name": "\u038c\u03bd\u03bf\u03bc\u03b1", "upper": "\u0386\u03bd\u03c9 \u03cc\u03c1\u03b9\u03bf" }, - "description": "\u0394\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03ce\u03c3\u03c4\u03b5 \u03c0\u03cc\u03c4\u03b5 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03bf \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2. \n\n \u039c\u03cc\u03bd\u03bf \u03c4\u03bf \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af - \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03ba\u03c1\u03cc\u03c4\u03b5\u03c1\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf.\n \u039c\u03cc\u03bd\u03bf \u03c4\u03bf \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af - \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b5\u03b3\u03b1\u03bb\u03cd\u03c4\u03b5\u03c1\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf.\n \u0394\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf\u03c5 \u03ba\u03b1\u03b9 \u03ac\u03bd\u03c9 \u03bf\u03c1\u03af\u03bf\u03c5 - \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c4\u03bf \u03b5\u03cd\u03c1\u03bf\u03c2 [\u03ba\u03ac\u03c4\u03c9 \u03cc\u03c1\u03b9\u03bf .. \u03ac\u03bd\u03c9 \u03cc\u03c1\u03b9\u03bf]." + "description": "\u039c\u03cc\u03bd\u03bf \u03c4\u03bf \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af - \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03ba\u03c1\u03cc\u03c4\u03b5\u03c1\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf.\n \u039c\u03cc\u03bd\u03bf \u03c4\u03bf \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf \u03ad\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af - \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b5\u03b3\u03b1\u03bb\u03cd\u03c4\u03b5\u03c1\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf.\n \u0388\u03c7\u03b5\u03b9 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03b8\u03b5\u03af \u03c4\u03cc\u03c3\u03bf \u03c4\u03bf \u03ba\u03b1\u03c4\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c3\u03bf \u03ba\u03b1\u03b9 \u03c4\u03bf \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf - \u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b1\u03bd \u03b7 \u03c4\u03b9\u03bc\u03ae \u03c4\u03bf\u03c5 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03b5\u03b9\u03c3\u03cc\u03b4\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c3\u03c4\u03bf \u03b5\u03cd\u03c1\u03bf\u03c2 [\u03ba\u03ac\u03c4\u03c9 \u03cc\u03c1\u03b9\u03bf .. \u03b1\u03bd\u03ce\u03c4\u03b5\u03c1\u03bf \u03cc\u03c1\u03b9\u03bf]." } } }, diff --git a/homeassistant/components/threshold/translations/uk.json b/homeassistant/components/threshold/translations/uk.json new file mode 100644 index 00000000000..fe3fc997183 --- /dev/null +++ b/homeassistant/components/threshold/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tibber/diagnostics.py b/homeassistant/components/tibber/diagnostics.py index 75c76636f5f..991d20e9e2d 100644 --- a/homeassistant/components/tibber/diagnostics.py +++ b/homeassistant/components/tibber/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Tibber.""" from __future__ import annotations +from typing import Any + import tibber from homeassistant.config_entries import ConfigEntry @@ -11,7 +13,7 @@ from .const import DOMAIN async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" tibber_connection: tibber.Tibber = hass.data[DOMAIN] diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 1636c5da4bd..cb3c88532d9 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -3,7 +3,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.26.11"], + "requirements": ["pyTibber==0.26.12"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 38f52a067fa..3b00ea8421b 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -33,7 +33,7 @@ CONF_SHOW_INACTIVE = "show_inactive" class TileData: """Define an object to be stored in `hass.data`.""" - coordinators: dict[str, DataUpdateCoordinator] + coordinators: dict[str, DataUpdateCoordinator[None]] tiles: dict[str, Tile] @@ -94,7 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except TileError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err - coordinators = {} + coordinators: dict[str, DataUpdateCoordinator[None]] = {} coordinator_init_tasks = [] for tile_uuid, tile in tiles.items(): diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 7931bbb8797..8dba892de83 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -30,6 +30,7 @@ ATTR_CONNECTION_STATE = "connection_state" ATTR_IS_DEAD = "is_dead" ATTR_IS_LOST = "is_lost" ATTR_LAST_LOST_TIMESTAMP = "last_lost_timestamp" +ATTR_LAST_TIMESTAMP = "last_timestamp" ATTR_RING_STATE = "ring_state" ATTR_TILE_NAME = "tile_name" ATTR_VOIP_STATE = "voip_state" @@ -77,13 +78,13 @@ async def async_setup_scanner( return True -class TileDeviceTracker(CoordinatorEntity, TrackerEntity): +class TileDeviceTracker(CoordinatorEntity[DataUpdateCoordinator[None]], TrackerEntity): """Representation of a network infrastructure device.""" _attr_icon = DEFAULT_ICON def __init__( - self, entry: ConfigEntry, coordinator: DataUpdateCoordinator, tile: Tile + self, entry: ConfigEntry, coordinator: DataUpdateCoordinator[None], tile: Tile ) -> None: """Initialize.""" super().__init__(coordinator) @@ -142,6 +143,7 @@ class TileDeviceTracker(CoordinatorEntity, TrackerEntity): ATTR_ALTITUDE: self._tile.altitude, ATTR_IS_LOST: self._tile.lost, ATTR_LAST_LOST_TIMESTAMP: self._tile.lost_timestamp, + ATTR_LAST_TIMESTAMP: self._tile.last_timestamp, ATTR_RING_STATE: self._tile.ring_state, ATTR_VOIP_STATE: self._tile.voip_state, } diff --git a/homeassistant/components/tile/translations/lt.json b/homeassistant/components/tile/translations/lt.json new file mode 100644 index 00000000000..883b5c03e2c --- /dev/null +++ b/homeassistant/components/tile/translations/lt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis", + "username": "El. pa\u0161tas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tilt_ble/manifest.json b/homeassistant/components/tilt_ble/manifest.json index b201628a7f5..98649dbab28 100644 --- a/homeassistant/components/tilt_ble/manifest.json +++ b/homeassistant/components/tilt_ble/manifest.json @@ -10,7 +10,7 @@ } ], "requirements": ["tilt-ble==0.2.3"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@apt-itude"], "iot_class": "local_push" } diff --git a/homeassistant/components/tilt_ble/sensor.py b/homeassistant/components/tilt_ble/sensor.py index be0e9e66037..7edfec3643f 100644 --- a/homeassistant/components/tilt_ble/sensor.py +++ b/homeassistant/components/tilt_ble/sensor.py @@ -1,8 +1,6 @@ """Support for Tilt Hydrometers.""" from __future__ import annotations -from typing import Optional, Union - from tilt_ble import DeviceClass, DeviceKey, SensorUpdate, Units from homeassistant import config_entries @@ -103,9 +101,7 @@ async def async_setup_entry( class TiltBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a Tilt Hydrometer BLE sensor.""" diff --git a/homeassistant/components/tilt_ble/translations/lv.json b/homeassistant/components/tilt_ble/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/tilt_ble/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tilt_ble/translations/tr.json b/homeassistant/components/tilt_ble/translations/tr.json index f63cee3493c..66d94aa9414 100644 --- a/homeassistant/components/tilt_ble/translations/tr.json +++ b/homeassistant/components/tilt_ble/translations/tr.json @@ -8,13 +8,13 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "user": { "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index e3a40be16c0..f72aa742f56 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -2,12 +2,16 @@ from __future__ import annotations from collections.abc import Callable -from datetime import datetime, timedelta +from datetime import datetime, time, timedelta import logging +from typing import TYPE_CHECKING, Any 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.config_entries import ConfigEntry from homeassistant.const import ( CONF_AFTER, @@ -37,7 +41,7 @@ ATTR_AFTER = "after" ATTR_BEFORE = "before" ATTR_NEXT_UPDATE = "next_update" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_AFTER): vol.Any(cv.time, vol.All(vol.Lower, cv.sun_event)), vol.Required(CONF_BEFORE): vol.Any(cv.time, vol.All(vol.Lower, cv.sun_event)), @@ -103,40 +107,53 @@ class TodSensor(BinarySensorEntity): _attr_should_poll = False - def __init__(self, name, after, after_offset, before, before_offset, unique_id): + def __init__( + self, + name: str, + after: time, + after_offset: timedelta, + before: time, + before_offset: timedelta, + unique_id: str | None, + ) -> None: """Init the ToD Sensor...""" self._attr_unique_id = unique_id - self._name = name - self._time_before = self._time_after = self._next_update = None + self._attr_name = name + self._time_before: datetime | None = None + self._time_after: datetime | None = None + self._next_update: datetime | None = None self._after_offset = after_offset self._before_offset = before_offset self._before = before self._after = after - self._unsub_update: Callable[[], None] = None + self._unsub_update: Callable[[], None] | None = None @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def is_on(self): + def is_on(self) -> bool: """Return True is sensor is on.""" + if TYPE_CHECKING: + assert self._time_after is not None + assert self._time_before is not None if self._time_after < self._time_before: return self._time_after <= dt_util.utcnow() < self._time_before return False @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the sensor.""" - time_zone = dt_util.get_time_zone(self.hass.config.time_zone) - return { - ATTR_AFTER: self._time_after.astimezone(time_zone).isoformat(), - ATTR_BEFORE: self._time_before.astimezone(time_zone).isoformat(), - ATTR_NEXT_UPDATE: self._next_update.astimezone(time_zone).isoformat(), - } + if TYPE_CHECKING: + assert self._time_after is not None + assert self._time_before is not None + assert self._next_update is not None + if time_zone := dt_util.get_time_zone(self.hass.config.time_zone): + return { + ATTR_AFTER: self._time_after.astimezone(time_zone).isoformat(), + ATTR_BEFORE: self._time_before.astimezone(time_zone).isoformat(), + ATTR_NEXT_UPDATE: self._next_update.astimezone(time_zone).isoformat(), + } + return None - def _naive_time_to_utc_datetime(self, naive_time): + def _naive_time_to_utc_datetime(self, naive_time: time) -> datetime: """Convert naive time from config to utc_datetime with current day.""" # get the current local date from utc time current_local_date = ( @@ -147,7 +164,7 @@ class TodSensor(BinarySensorEntity): # calculate utc datetime corresponding to local time return dt_util.as_utc(datetime.combine(current_local_date, naive_time)) - def _calculate_boundary_time(self): + def _calculate_boundary_time(self) -> None: """Calculate internal absolute time boundaries.""" nowutc = dt_util.utcnow() # If after value is a sun event instead of absolute time @@ -155,8 +172,8 @@ class TodSensor(BinarySensorEntity): # Calculate the today's event utc time or # if not available take next after_event_date = get_astral_event_date( - self.hass, self._after, nowutc - ) or get_astral_event_next(self.hass, self._after, nowutc) + self.hass, str(self._after), nowutc + ) or get_astral_event_next(self.hass, str(self._after), nowutc) else: # Convert local time provided to UTC today # datetime.combine(date, time, tzinfo) is not supported @@ -171,13 +188,13 @@ class TodSensor(BinarySensorEntity): # Calculate the today's event utc time or if not available take # next before_event_date = get_astral_event_date( - self.hass, self._before, nowutc - ) or get_astral_event_next(self.hass, self._before, nowutc) + self.hass, str(self._before), nowutc + ) or get_astral_event_next(self.hass, str(self._before), nowutc) # Before is earlier than after if before_event_date < after_event_date: # Take next day for before before_event_date = get_astral_event_next( - self.hass, self._before, after_event_date + self.hass, str(self._before), after_event_date ) else: # Convert local time provided to UTC today, see above @@ -195,6 +212,7 @@ class TodSensor(BinarySensorEntity): # _time_before is set to 12:00 next day # _time_after is set to 23:00 today # nowutc is set to 10:00 today + if ( not _is_sun_event(self._after) and self._time_after > nowutc @@ -208,11 +226,14 @@ class TodSensor(BinarySensorEntity): self._time_after += self._after_offset self._time_before += self._before_offset - def _turn_to_next_day(self): + def _turn_to_next_day(self) -> None: """Turn to to the next day.""" + if TYPE_CHECKING: + assert self._time_after is not None + assert self._time_before is not None if _is_sun_event(self._after): self._time_after = get_astral_event_next( - self.hass, self._after, self._time_after - self._after_offset + self.hass, str(self._after), self._time_after - self._after_offset ) self._time_after += self._after_offset else: @@ -221,7 +242,7 @@ class TodSensor(BinarySensorEntity): if _is_sun_event(self._before): self._time_before = get_astral_event_next( - self.hass, self._before, self._time_before - self._before_offset + self.hass, str(self._before), self._time_before - self._before_offset ) self._time_before += self._before_offset else: @@ -241,12 +262,17 @@ class TodSensor(BinarySensorEntity): self.async_on_remove(_clean_up_listener) + if TYPE_CHECKING: + assert self._next_update is not None self._unsub_update = event.async_track_point_in_utc_time( self.hass, self._point_in_time_listener, self._next_update ) - def _calculate_next_update(self): + def _calculate_next_update(self) -> None: """Datetime when the next update to the state.""" + if TYPE_CHECKING: + assert self._time_after is not None + assert self._time_before is not None now = dt_util.utcnow() if now < self._time_after: self._next_update = self._time_after @@ -258,11 +284,14 @@ class TodSensor(BinarySensorEntity): self._next_update = self._time_after @callback - def _point_in_time_listener(self, now): + def _point_in_time_listener(self, now: datetime) -> None: """Run when the state of the sensor should be updated.""" self._calculate_next_update() self.async_write_ha_state() + if TYPE_CHECKING: + assert self._next_update is not None + self._unsub_update = event.async_track_point_in_utc_time( self.hass, self._point_in_time_listener, self._next_update ) diff --git a/homeassistant/components/tod/translations/el.json b/homeassistant/components/tod/translations/el.json index 453e36c4020..c6cd852a4a3 100644 --- a/homeassistant/components/tod/translations/el.json +++ b/homeassistant/components/tod/translations/el.json @@ -7,8 +7,8 @@ "before_time": "\u0395\u03ba\u03c4\u03cc\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c5", "name": "\u038c\u03bd\u03bf\u03bc\u03b1" }, - "description": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c0\u03cc\u03c4\u03b5 \u03bf \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b8\u03b1 \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9.", - "title": "\u0391\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 New Times of the Day" + "description": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b4\u03c5\u03b1\u03b4\u03b9\u03ba\u03cc \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03ae \u03b1\u03c0\u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03b1\u03bd\u03ac\u03bb\u03bf\u03b3\u03b1 \u03bc\u03b5 \u03c4\u03b7\u03bd \u03ce\u03c1\u03b1.", + "title": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 Times of the Day" } } }, diff --git a/homeassistant/components/tod/translations/uk.json b/homeassistant/components/tod/translations/uk.json new file mode 100644 index 00000000000..fe3fc997183 --- /dev/null +++ b/homeassistant/components/tod/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 9e14a87c82d..3e7026970e6 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -1,11 +1,17 @@ """Support for Todoist task management (https://todoist.com).""" from __future__ import annotations -from datetime import date, datetime, timedelta, timezone +import asyncio +from datetime import date, datetime, timedelta +from itertools import chain import logging from typing import Any +import uuid -from todoist.api import TodoistAPI +from todoist_api_python.api_async import TodoistAPIAsync +from todoist_api_python.endpoints import get_sync_url +from todoist_api_python.headers import create_headers +from todoist_api_python.models import Label, Task import voluptuous as vol from homeassistant.components.calendar import ( @@ -15,6 +21,7 @@ from homeassistant.components.calendar import ( ) from homeassistant.const import CONF_ID, CONF_NAME, CONF_TOKEN from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -24,8 +31,6 @@ from .const import ( ALL_DAY, ALL_TASKS, ASSIGNEE, - CHECKED, - COLLABORATORS, COMPLETED, CONF_EXTRA_PROJECTS, CONF_PROJECT_DUE_DATE, @@ -34,31 +39,24 @@ from .const import ( CONTENT, DESCRIPTION, DOMAIN, - DUE, DUE_DATE, DUE_DATE_LANG, DUE_DATE_STRING, DUE_DATE_VALID_LANGS, DUE_TODAY, END, - FULL_NAME, - ID, LABELS, - NAME, OVERDUE, PRIORITY, - PROJECT_ID, PROJECT_NAME, - PROJECTS, REMINDER_DATE, REMINDER_DATE_LANG, REMINDER_DATE_STRING, SERVICE_NEW_TASK, START, SUMMARY, - TASKS, ) -from .types import CalData, CustomProject, DueDate, ProjectData, TodoistEvent +from .types import CalData, CustomProject, ProjectData, TodoistEvent _LOGGER = logging.getLogger(__name__) @@ -108,109 +106,115 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( SCAN_INTERVAL = timedelta(minutes=1) -def setup_platform( +async def async_setup_platform( hass: HomeAssistant, config: ConfigType, - add_entities: AddEntitiesCallback, + async_add_entities: AddEntitiesCallback, discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the Todoist platform.""" - token = config.get(CONF_TOKEN) + token = config[CONF_TOKEN] # Look up IDs based on (lowercase) names. project_id_lookup = {} label_id_lookup = {} collaborator_id_lookup = {} - api = TodoistAPI(token) - api.sync() + api = TodoistAPIAsync(token) # Setup devices: # Grab all projects. - projects = api.state[PROJECTS] + projects = await api.get_projects() + + collaborator_tasks = (api.get_collaborators(project.id) for project in projects) + collaborators = list(chain.from_iterable(await asyncio.gather(*collaborator_tasks))) - collaborators = api.state[COLLABORATORS] # Grab all labels - labels = api.state[LABELS] + labels = await api.get_labels() # Add all Todoist-defined projects. project_devices = [] for project in projects: # Project is an object, not a dict! # Because of that, we convert what we need to a dict. - project_data: ProjectData = {CONF_NAME: project[NAME], CONF_ID: project[ID]} + project_data: ProjectData = {CONF_NAME: project.name, CONF_ID: project.id} project_devices.append(TodoistProjectEntity(project_data, labels, api)) # Cache the names so we can easily look up name->ID. - project_id_lookup[project[NAME].lower()] = project[ID] + project_id_lookup[project.name.lower()] = project.id # Cache all label names - for label in labels: - label_id_lookup[label[NAME].lower()] = label[ID] + label_id_lookup = {label.name.lower(): label.id for label in labels} - for collaborator in collaborators: - collaborator_id_lookup[collaborator[FULL_NAME].lower()] = collaborator[ID] + collaborator_id_lookup = { + collab.name.lower(): collab.id for collab in collaborators + } # Check config for more projects. extra_projects: list[CustomProject] = config[CONF_EXTRA_PROJECTS] - for project in extra_projects: + for extra_project in extra_projects: # Special filter: By date - project_due_date = project.get(CONF_PROJECT_DUE_DATE) + project_due_date = extra_project.get(CONF_PROJECT_DUE_DATE) # Special filter: By label - project_label_filter = project[CONF_PROJECT_LABEL_WHITELIST] + project_label_filter = extra_project[CONF_PROJECT_LABEL_WHITELIST] # Special filter: By name # Names must be converted into IDs. - project_name_filter = project[CONF_PROJECT_WHITELIST] - project_id_filter = [ - project_id_lookup[project_name.lower()] - for project_name in project_name_filter - ] + project_name_filter = extra_project[CONF_PROJECT_WHITELIST] + project_id_filter: list[str] | None = None + if project_name_filter is not None: + project_id_filter = [ + project_id_lookup[project_name.lower()] + for project_name in project_name_filter + ] # Create the custom project and add it to the devices array. project_devices.append( TodoistProjectEntity( - project, + {"id": None, "name": extra_project["name"]}, labels, api, - project_due_date, - project_label_filter, - project_id_filter, + due_date_days=project_due_date, + whitelisted_labels=project_label_filter, + whitelisted_projects=project_id_filter, ) ) - add_entities(project_devices) - def handle_new_task(call: ServiceCall) -> None: + async_add_entities(project_devices) + + session = async_get_clientsession(hass) + + async def handle_new_task(call: ServiceCall) -> None: """Call when a user creates a new Todoist Task from Home Assistant.""" project_name = call.data[PROJECT_NAME] project_id = project_id_lookup[project_name] # Create the task - item = api.items.add(call.data[CONTENT], project_id=project_id) + content = call.data[CONTENT] + data: dict[str, Any] = {"project_id": project_id} - if LABELS in call.data: - task_labels = call.data[LABELS] - label_ids = [label_id_lookup[label.lower()] for label in task_labels] - item.update(labels=label_ids) + if task_labels := call.data.get(LABELS): + data["label_ids"] = [ + label_id_lookup[label.lower()] for label in task_labels + ] if ASSIGNEE in call.data: task_assignee = call.data[ASSIGNEE].lower() if task_assignee in collaborator_id_lookup: - item.update(responsible_uid=collaborator_id_lookup[task_assignee]) + data["assignee"] = collaborator_id_lookup[task_assignee] else: raise ValueError( f"User is not part of the shared project. user: {task_assignee}" ) if PRIORITY in call.data: - item.update(priority=call.data[PRIORITY]) + data["priority"] = call.data[PRIORITY] - _due: dict = {} if DUE_DATE_STRING in call.data: - _due["string"] = call.data[DUE_DATE_STRING] + data["due_string"] = call.data[DUE_DATE_STRING] if DUE_DATE_LANG in call.data: - _due["lang"] = call.data[DUE_DATE_LANG] + data["due_lang"] = call.data[DUE_DATE_LANG] if DUE_DATE in call.data: due_date = dt.parse_datetime(call.data[DUE_DATE]) @@ -222,11 +226,14 @@ def setup_platform( # Format it in the manner Todoist expects due_date = dt.as_utc(due_date) date_format = "%Y-%m-%dT%H:%M:%S" - _due["date"] = datetime.strftime(due_date, date_format) + data["due_datetime"] = datetime.strftime(due_date, date_format) - if _due: - item.update(due=_due) + api_task = await api.add_task(content, **data) + # @NOTE: The rest-api doesn't support reminders, this works manually using + # the sync api, in order to keep functional parity with the component. + # https://developer.todoist.com/sync/v9/#reminders + sync_url = get_sync_url("sync") _reminder_due: dict = {} if REMINDER_DATE_STRING in call.data: _reminder_due["string"] = call.data[REMINDER_DATE_STRING] @@ -248,50 +255,50 @@ def setup_platform( date_format = "%Y-%m-%dT%H:%M:%S" _reminder_due["date"] = datetime.strftime(due_date, date_format) - if _reminder_due: - api.reminders.add(item["id"], due=_reminder_due) + async def add_reminder(reminder_due: dict): + reminder_data = { + "commands": [ + { + "type": "reminder_add", + "temp_id": str(uuid.uuid1()), + "uuid": str(uuid.uuid1()), + "args": {"item_id": api_task.id, "due": reminder_due}, + } + ] + } + headers = create_headers(token=token, with_content=True) + return await session.post(sync_url, headers=headers, json=reminder_data) + + if _reminder_due: + await add_reminder(_reminder_due) - # Commit changes - api.commit() _LOGGER.debug("Created Todoist task: %s", call.data[CONTENT]) - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_NEW_TASK, handle_new_task, schema=NEW_TASK_SERVICE_SCHEMA ) -def _parse_due_date(data: DueDate, timezone_offset: int) -> datetime | None: - """Parse the due date dict into a datetime object in UTC. - - This function will always return a timezone aware datetime if it can be parsed. - """ - if not (nowtime := dt.parse_datetime(data["date"])): - return None - if nowtime.tzinfo is None: - nowtime = nowtime.replace(tzinfo=timezone(timedelta(hours=timezone_offset))) - return dt.as_utc(nowtime) - - class TodoistProjectEntity(CalendarEntity): """A device for getting the next Task from a Todoist Project.""" def __init__( self, data: ProjectData, - labels: list[str], - token: TodoistAPI, + labels: list[Label], + api: TodoistAPIAsync, due_date_days: int | None = None, whitelisted_labels: list[str] | None = None, - whitelisted_projects: list[int] | None = None, + whitelisted_projects: list[str] | None = None, ) -> None: """Create the Todoist Calendar Entity.""" self.data = TodoistProjectData( data, labels, - token, - due_date_days, - whitelisted_labels, - whitelisted_projects, + api, + due_date_days=due_date_days, + whitelisted_labels=whitelisted_labels, + whitelisted_projects=whitelisted_projects, ) self._cal_data: CalData = {} self._name = data[CONF_NAME] @@ -309,11 +316,11 @@ class TodoistProjectEntity(CalendarEntity): """Return the name of the entity.""" return self._name - def update(self) -> None: + async def async_update(self) -> None: """Update all Todoist Calendars.""" - self.data.update() + await self.data.async_update() # Set Todoist-specific data that can't easily be grabbed - self._cal_data[ALL_TASKS] = [ + self._cal_data["all_tasks"] = [ task[SUMMARY] for task in self.data.all_project_tasks ] @@ -324,7 +331,7 @@ class TodoistProjectEntity(CalendarEntity): end_date: datetime, ) -> list[CalendarEvent]: """Get all events in a specific time frame.""" - return await self.data.async_get_events(hass, start_date, end_date) + return await self.data.async_get_events(start_date, end_date) @property def extra_state_attributes(self) -> dict[str, Any] | None: @@ -378,14 +385,14 @@ class TodoistProjectData: def __init__( self, project_data: ProjectData, - labels: list[str], - api: TodoistAPI, + labels: list[Label], + api: TodoistAPIAsync, due_date_days: int | None = None, whitelisted_labels: list[str] | None = None, - whitelisted_projects: list[int] | None = None, + whitelisted_projects: list[str] | None = None, ) -> None: """Initialize a Todoist Project.""" - self.event = None + self.event: TodoistEvent | None = None self._api = api self._name = project_data[CONF_NAME] @@ -410,7 +417,7 @@ class TodoistProjectData: self._label_whitelist = whitelisted_labels # This project includes only projects with these names. - self._project_id_whitelist: list[int] = [] + self._project_id_whitelist: list[str] = [] if whitelisted_projects is not None: self._project_id_whitelist = whitelisted_projects @@ -419,33 +426,41 @@ class TodoistProjectData: """Return the next upcoming calendar event.""" if not self.event: return None - if not self.event.get(END) or self.event.get(ALL_DAY): - start = self.event[START].date() + + start = self.event[START] + if self.event.get(ALL_DAY) or self.event[END] is None: return CalendarEvent( summary=self.event[SUMMARY], - start=start, - end=start + timedelta(days=1), + start=start.date(), + end=start.date() + timedelta(days=1), ) + return CalendarEvent( - summary=self.event[SUMMARY], start=self.event[START], end=self.event[END] + summary=self.event[SUMMARY], start=start, end=self.event[END] ) - def create_todoist_task(self, data): + def create_todoist_task(self, data: Task): """ Create a dictionary based on a Task passed from the Todoist API. Will return 'None' if the task is to be filtered out. """ - task = {} - # Fields are required to be in all returned task objects. - task[SUMMARY] = data[CONTENT] - task[COMPLETED] = data[CHECKED] == 1 - task[PRIORITY] = data[PRIORITY] - task[DESCRIPTION] = f"https://todoist.com/showTask?id={data[ID]}" + task: TodoistEvent = { + ALL_DAY: False, + COMPLETED: data.is_completed, + DESCRIPTION: f"https://todoist.com/showTask?id={data.id}", + DUE_TODAY: False, + END: None, + LABELS: [], + OVERDUE: False, + PRIORITY: data.priority, + START: dt.utcnow(), + SUMMARY: data.content, + } # All task Labels (optional parameter). task[LABELS] = [ - label[NAME].lower() for label in self._labels if label[ID] in data[LABELS] + label.name.lower() for label in self._labels if label.id in data.labels ] if self._label_whitelist and ( @@ -460,30 +475,30 @@ class TodoistProjectData: # That means that the START date is the earliest time one can # complete the task. # Generally speaking, that means right now. - task[START] = dt.utcnow() - if data[DUE] is not None: - task[END] = _parse_due_date( - data[DUE], self._api.state["user"]["tz_info"]["hours"] + if data.due is not None: + end = dt.parse_datetime( + data.due.datetime if data.due.datetime else data.due.date ) + task[END] = dt.as_utc(end) if end is not None else end + if task[END] is not None: + if self._due_date_days is not None and ( + task[END] > dt.utcnow() + self._due_date_days + ): + # This task is out of range of our due date; + # it shouldn't be counted. + return None - if self._due_date_days is not None and ( - task[END] > dt.utcnow() + self._due_date_days - ): - # This task is out of range of our due date; - # it shouldn't be counted. - return None + task[DUE_TODAY] = task[END].date() == dt.utcnow().date() - task[DUE_TODAY] = task[END].date() == dt.utcnow().date() - - # Special case: Task is overdue. - if task[END] <= task[START]: - task[OVERDUE] = True - # Set end time to the current time plus 1 hour. - # We're pretty much guaranteed to update within that 1 hour, - # so it should be fine. - task[END] = task[START] + timedelta(hours=1) - else: - task[OVERDUE] = False + # Special case: Task is overdue. + if task[END] <= task[START]: + task[OVERDUE] = True + # Set end time to the current time plus 1 hour. + # We're pretty much guaranteed to update within that 1 hour, + # so it should be fine. + task[END] = task[START] + timedelta(hours=1) + else: + task[OVERDUE] = False else: # If we ask for everything due before a certain date, don't count # things which have no due dates. @@ -564,72 +579,60 @@ class TodoistProjectData: ): event = proposed_event continue - return event async def async_get_events( - self, hass: HomeAssistant, start_date: datetime, end_date: datetime + self, start_date: datetime, end_date: datetime ) -> list[CalendarEvent]: """Get all tasks in a specific time frame.""" if self._id is None: + tasks = await self._api.get_tasks() project_task_data = [ task - for task in self._api.state[TASKS] + for task in tasks if not self._project_id_whitelist - or task[PROJECT_ID] in self._project_id_whitelist + or task.project_id in self._project_id_whitelist ] else: - project_data = await hass.async_add_executor_job( - self._api.projects.get_data, self._id - ) - project_task_data = project_data[TASKS] + project_task_data = await self._api.get_tasks(project_id=self._id) events = [] for task in project_task_data: - if task["due"] is None: + if task.due is None: continue - # @NOTE: _parse_due_date always returns the date in UTC time. - due_date: datetime | None = _parse_due_date( - task["due"], self._api.state["user"]["tz_info"]["hours"] + due_date = dt.parse_datetime( + task.due.datetime if task.due.datetime else task.due.date ) if not due_date: continue - gmt_string = self._api.state["user"]["tz_info"]["gmt_string"] - local_midnight = dt.parse_datetime( - due_date.strftime(f"%Y-%m-%dT00:00:00{gmt_string}") - ) - if local_midnight is not None: - midnight = dt.as_utc(local_midnight) - else: - midnight = due_date.replace(hour=0, minute=0, second=0, microsecond=0) - + due_date = dt.as_utc(due_date) if start_date < due_date < end_date: due_date_value: datetime | date = due_date + midnight = dt.start_of_local_day(due_date) if due_date == midnight: # If the due date has no time data, return just the date so that it # will render correctly as an all day event on a calendar. due_date_value = due_date.date() event = CalendarEvent( - summary=task["content"], + summary=task.content, start=due_date_value, end=due_date_value, ) events.append(event) return events - def update(self): + async def async_update(self) -> None: """Get the latest data.""" if self._id is None: - self._api.reset_state() - self._api.sync() + tasks = await self._api.get_tasks() project_task_data = [ task - for task in self._api.state[TASKS] + for task in tasks if not self._project_id_whitelist - or task[PROJECT_ID] in self._project_id_whitelist + or task.project_id in self._project_id_whitelist ] else: - project_task_data = self._api.projects.get_data(self._id)[TASKS] + project_task_data = await self._api.get_tasks(project_id=self._id) # If we have no data, we can just return right away. if not project_task_data: @@ -639,7 +642,6 @@ class TodoistProjectData: # Keep an updated list of all tasks in this project. project_tasks = [] - for task in project_task_data: todoist_task = self.create_todoist_task(task) if todoist_task is not None: diff --git a/homeassistant/components/todoist/manifest.json b/homeassistant/components/todoist/manifest.json index a00819638f3..e5f0de5e485 100644 --- a/homeassistant/components/todoist/manifest.json +++ b/homeassistant/components/todoist/manifest.json @@ -2,7 +2,7 @@ "domain": "todoist", "name": "Todoist", "documentation": "https://www.home-assistant.io/integrations/todoist", - "requirements": ["todoist-python==8.0.0"], + "requirements": ["todoist-api-python==2.0.2"], "codeowners": ["@boralyl"], "iot_class": "cloud_polling", "loggers": ["todoist"] diff --git a/homeassistant/components/todoist/types.py b/homeassistant/components/todoist/types.py index fd3b2e889ca..e6b4d55fce2 100644 --- a/homeassistant/components/todoist/types.py +++ b/homeassistant/components/todoist/types.py @@ -19,7 +19,7 @@ class ProjectData(TypedDict): """Dict representing project data.""" name: str - id: int | None + id: str | None class CustomProject(TypedDict): diff --git a/homeassistant/components/tolo/climate.py b/homeassistant/components/tolo/climate.py index 3afc641ba22..849a9f5b3ed 100644 --- a/homeassistant/components/tolo/climate.py +++ b/homeassistant/components/tolo/climate.py @@ -130,9 +130,9 @@ class SaunaClimate(ToloSaunaCoordinatorEntity, ClimateEntity): """Set fan mode.""" self.coordinator.client.set_fan_on(fan_mode == FAN_ON) - def set_humidity(self, humidity: float) -> None: + def set_humidity(self, humidity: int) -> None: """Set desired target humidity.""" - self.coordinator.client.set_target_humidity(round(humidity)) + self.coordinator.client.set_target_humidity(humidity) def set_temperature(self, **kwargs: Any) -> None: """Set desired target temperature.""" diff --git a/homeassistant/components/tolo/manifest.json b/homeassistant/components/tolo/manifest.json index ba208a050a5..b1656094ab3 100644 --- a/homeassistant/components/tolo/manifest.json +++ b/homeassistant/components/tolo/manifest.json @@ -3,7 +3,7 @@ "name": "TOLO Sauna", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tolo", - "requirements": ["tololib==0.1.0b3"], + "requirements": ["tololib==0.1.0b4"], "codeowners": ["@MatthiasLohr"], "iot_class": "local_polling", "dhcp": [{ "hostname": "usr-tcp232-ed2" }], diff --git a/homeassistant/components/tolo/translations/lv.json b/homeassistant/components/tolo/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/tolo/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/nl.json b/homeassistant/components/tolo/translations/nl.json index 31d417dd9e7..51c7b2834f0 100644 --- a/homeassistant/components/tolo/translations/nl.json +++ b/homeassistant/components/tolo/translations/nl.json @@ -18,5 +18,15 @@ "description": "Voer de hostnaam of het IP-adres van uw TOLO Sauna-apparaat in." } } + }, + "entity": { + "select": { + "lamp_mode": { + "state": { + "automatic": "Automatisch", + "manual": "Handmatig" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tolo/translations/tr.json b/homeassistant/components/tolo/translations/tr.json index 8f2b5ee1939..95ba87a38e6 100644 --- a/homeassistant/components/tolo/translations/tr.json +++ b/homeassistant/components/tolo/translations/tr.json @@ -9,7 +9,7 @@ "flow_title": "{name}", "step": { "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" }, "user": { "data": { @@ -18,5 +18,15 @@ "description": "TOLO Sauna cihaz\u0131n\u0131z\u0131n ana bilgisayar ad\u0131n\u0131 veya IP adresini girin." } } + }, + "entity": { + "select": { + "lamp_mode": { + "state": { + "automatic": "Otomatik", + "manual": "Manuel" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tomorrowio/__init__.py b/homeassistant/components/tomorrowio/__init__.py index e2aaba6cdff..bf43b0aa10d 100644 --- a/homeassistant/components/tomorrowio/__init__.py +++ b/homeassistant/components/tomorrowio/__init__.py @@ -140,7 +140,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await coordinator.async_setup_entry(entry) - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -162,7 +162,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return unload_ok -class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator): +class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Define an object to hold Tomorrow.io data.""" def __init__(self, hass: HomeAssistant, api: TomorrowioV4) -> None: @@ -235,7 +235,7 @@ class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> dict[str, Any]: """Update data via library.""" - data = {} + data: dict[str, Any] = {} # If we are refreshing because of a new config entry that's not already in our # data, we do a partial refresh to avoid wasted API calls. if self.data and any( diff --git a/homeassistant/components/tomorrowio/strings.json b/homeassistant/components/tomorrowio/strings.json index 3ae70f214bb..1057477b0ac 100644 --- a/homeassistant/components/tomorrowio/strings.json +++ b/homeassistant/components/tomorrowio/strings.json @@ -10,6 +10,9 @@ } } }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_service%]" + }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", diff --git a/homeassistant/components/tomorrowio/translations/bg.json b/homeassistant/components/tomorrowio/translations/bg.json index 261841d75d1..34436ecbffe 100644 --- a/homeassistant/components/tomorrowio/translations/bg.json +++ b/homeassistant/components/tomorrowio/translations/bg.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u0423\u0441\u043b\u0443\u0433\u0430\u0442\u0430 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" + }, "error": { "cannot_connect": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_api_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447", diff --git a/homeassistant/components/tomorrowio/translations/ca.json b/homeassistant/components/tomorrowio/translations/ca.json index 64a3457b044..5040937a0c6 100644 --- a/homeassistant/components/tomorrowio/translations/ca.json +++ b/homeassistant/components/tomorrowio/translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_api_key": "Clau API inv\u00e0lida", diff --git a/homeassistant/components/tomorrowio/translations/cs.json b/homeassistant/components/tomorrowio/translations/cs.json index 0a6cfc5cf12..aa36b3cf7cb 100644 --- a/homeassistant/components/tomorrowio/translations/cs.json +++ b/homeassistant/components/tomorrowio/translations/cs.json @@ -2,7 +2,8 @@ "config": { "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", - "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API" + "invalid_api_key": "Neplatn\u00fd kl\u00ed\u010d API", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { "user": { diff --git a/homeassistant/components/tomorrowio/translations/de.json b/homeassistant/components/tomorrowio/translations/de.json index ad5c14dd4e0..0c0b07b51b9 100644 --- a/homeassistant/components/tomorrowio/translations/de.json +++ b/homeassistant/components/tomorrowio/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", diff --git a/homeassistant/components/tomorrowio/translations/en.json b/homeassistant/components/tomorrowio/translations/en.json index 15d1aadeaaf..088f8a1774c 100644 --- a/homeassistant/components/tomorrowio/translations/en.json +++ b/homeassistant/components/tomorrowio/translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Service is already configured" + }, "error": { "cannot_connect": "Failed to connect", "invalid_api_key": "Invalid API key", diff --git a/homeassistant/components/tomorrowio/translations/et.json b/homeassistant/components/tomorrowio/translations/et.json index 3270fbe7e14..3c6a902dd21 100644 --- a/homeassistant/components/tomorrowio/translations/et.json +++ b/homeassistant/components/tomorrowio/translations/et.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Teenus on juba seadistatud" + }, "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_api_key": "Vigane API v\u00f5ti", diff --git a/homeassistant/components/tomorrowio/translations/nl.json b/homeassistant/components/tomorrowio/translations/nl.json index aa2141c202c..74c8cbbf918 100644 --- a/homeassistant/components/tomorrowio/translations/nl.json +++ b/homeassistant/components/tomorrowio/translations/nl.json @@ -21,13 +21,16 @@ "sensor": { "health_concern": { "state": { - "good": "Goed" + "good": "Goed", + "hazardous": "Gevaarlijk", + "unhealthy": "Ongezond" } }, "pollen_index": { "state": { "high": "Hoog", "low": "Laag", + "medium": "Medium", "none": "Geen" } }, diff --git a/homeassistant/components/tomorrowio/translations/no.json b/homeassistant/components/tomorrowio/translations/no.json index 32bef37e41c..f192791a30d 100644 --- a/homeassistant/components/tomorrowio/translations/no.json +++ b/homeassistant/components/tomorrowio/translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Tjenesten er allerede konfigurert" + }, "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_api_key": "Ugyldig API-n\u00f8kkel", diff --git a/homeassistant/components/tomorrowio/translations/ru.json b/homeassistant/components/tomorrowio/translations/ru.json index 1354f159d82..73b7ced9c31 100644 --- a/homeassistant/components/tomorrowio/translations/ru.json +++ b/homeassistant/components/tomorrowio/translations/ru.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0441\u043b\u0443\u0436\u0431\u0430 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", diff --git a/homeassistant/components/tomorrowio/translations/sk.json b/homeassistant/components/tomorrowio/translations/sk.json index f15b7bc34ec..62e96493f84 100644 --- a/homeassistant/components/tomorrowio/translations/sk.json +++ b/homeassistant/components/tomorrowio/translations/sk.json @@ -56,7 +56,7 @@ "data": { "timestep": "Min. medzi predpove\u010fami NowCast" }, - "description": "Ak sa rozhodnete povoli\u0165 entitu progn\u00f3zy \u201enowcast\u201c, m\u00f4\u017eete nakonfigurova\u0165 po\u010det min\u00fat medzi jednotliv\u00fdmi progn\u00f3zami. Po\u010det poskytnut\u00fdch predpoved\u00ed z\u00e1vis\u00ed od po\u010dtu min\u00fat vybrat\u00fdch medzi predpove\u010fami.", + "description": "Ak sa rozhodnete povoli\u0165 entitu progn\u00f3zy `nowcast`, m\u00f4\u017eete nakonfigurova\u0165 po\u010det min\u00fat medzi jednotliv\u00fdmi progn\u00f3zami. Po\u010det poskytnut\u00fdch predpoved\u00ed z\u00e1vis\u00ed od po\u010dtu min\u00fat vybrat\u00fdch medzi predpove\u010fami.", "title": "Aktualizujte mo\u017enosti Tomorrow.io" } } diff --git a/homeassistant/components/tomorrowio/translations/tr.json b/homeassistant/components/tomorrowio/translations/tr.json index 61802d7f328..b650c065e9f 100644 --- a/homeassistant/components/tomorrowio/translations/tr.json +++ b/homeassistant/components/tomorrowio/translations/tr.json @@ -17,6 +17,39 @@ } } }, + "entity": { + "sensor": { + "health_concern": { + "state": { + "good": "\u0130yi", + "hazardous": "Tehlikeli", + "moderate": "Il\u0131ml\u0131", + "unhealthy": "Sa\u011fl\u0131ks\u0131z", + "unhealthy_for_sensitive_groups": "Hassas Gruplar \u0130\u00e7in Sa\u011fl\u0131ks\u0131z", + "very_unhealthy": "\u00c7ok Sa\u011fl\u0131ks\u0131z" + } + }, + "pollen_index": { + "state": { + "high": "Y\u00fcksek", + "low": "D\u00fc\u015f\u00fck", + "medium": "Orta", + "none": "Hi\u00e7biri", + "very_high": "\u00c7ok Y\u00fcksek", + "very_low": "\u00c7ok D\u00fc\u015f\u00fck" + } + }, + "precipitation_type": { + "state": { + "freezing_rain": "Dondurucu Ya\u011fmur", + "ice_pellets": "Buz Peletleri", + "none": "Hi\u00e7biri", + "rain": "Ya\u011fmur", + "snow": "Kar" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/tomorrowio/translations/zh-Hant.json b/homeassistant/components/tomorrowio/translations/zh-Hant.json index cb21ab23ebb..bbb478e8a2d 100644 --- a/homeassistant/components/tomorrowio/translations/zh-Hant.json +++ b/homeassistant/components/tomorrowio/translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u670d\u52d9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_api_key": "API \u91d1\u9470\u7121\u6548", diff --git a/homeassistant/components/torque/manifest.json b/homeassistant/components/torque/manifest.json index 39b01ba712e..07d91299b4a 100644 --- a/homeassistant/components/torque/manifest.json +++ b/homeassistant/components/torque/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/torque", "dependencies": ["http"], "codeowners": [], - "iot_class": "cloud_polling" + "iot_class": "local_push" } diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index 858ed3121d7..967cbfa7e73 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -54,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = coordinator - hass.config_entries.async_setup_platforms(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) entry.async_on_unload(entry.add_update_listener(update_listener)) diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index 6f9e579ef2c..c6433fb71a4 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -51,7 +51,7 @@ async def async_setup_entry( ) ) - async_add_entities(alarms, True) + async_add_entities(alarms) # Set up services platform = entity_platform.async_get_current_platform() diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index 71c11958d40..0866428c460 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -2,7 +2,7 @@ "domain": "totalconnect", "name": "Total Connect", "documentation": "https://www.home-assistant.io/integrations/totalconnect", - "requirements": ["total_connect_client==2022.10"], + "requirements": ["total_connect_client==2023.1"], "dependencies": [], "codeowners": ["@austinmroczek"], "config_flow": true, diff --git a/homeassistant/components/tplink/entity.py b/homeassistant/components/tplink/entity.py index 471d32631c4..01e124dea1a 100644 --- a/homeassistant/components/tplink/entity.py +++ b/homeassistant/components/tplink/entity.py @@ -2,10 +2,9 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar from kasa import SmartDevice -from typing_extensions import Concatenate, ParamSpec from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity import DeviceInfo diff --git a/homeassistant/components/tplink/translations/lv.json b/homeassistant/components/tplink/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/tplink/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/tr.json b/homeassistant/components/tplink/translations/tr.json index 616997a0976..39c7b5b8c5c 100644 --- a/homeassistant/components/tplink/translations/tr.json +++ b/homeassistant/components/tplink/translations/tr.json @@ -10,7 +10,7 @@ "flow_title": "{name} {model} ({host})", "step": { "discovery_confirm": { - "description": "{name} {model} ( {host} ) kurulumu yapmak istiyor musunuz?" + "description": "{name} {model} ( {host} ) kurmak istiyor musunuz?" }, "pick_device": { "data": { diff --git a/homeassistant/components/tplink/translations/uk.json b/homeassistant/components/tplink/translations/uk.json index 1efd10692f9..787e5440af9 100644 --- a/homeassistant/components/tplink/translations/uk.json +++ b/homeassistant/components/tplink/translations/uk.json @@ -2,6 +2,13 @@ "config": { "abort": { "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456." + }, + "step": { + "pick_device": { + "data": { + "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/traccar/translations/el.json b/homeassistant/components/traccar/translations/el.json index f4760538f23..a14d2254715 100644 --- a/homeassistant/components/traccar/translations/el.json +++ b/homeassistant/components/traccar/translations/el.json @@ -6,7 +6,7 @@ "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." + "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 \u03c7\u03c1\u03b5\u03b9\u03b1\u03c3\u03c4\u03b5\u03af \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 \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": { diff --git a/homeassistant/components/traccar/translations/hu.json b/homeassistant/components/traccar/translations/hu.json index 6b80f58bc97..d8c6bab88b7 100644 --- a/homeassistant/components/traccar/translations/hu.json +++ b/homeassistant/components/traccar/translations/hu.json @@ -6,7 +6,7 @@ "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." }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a webhook funkci\u00f3t a Traccar-ban. \n\nHaszn\u00e1lja a k\u00f6vetkez\u0151 URL-t: `{webhook_url}`\n\nTov\u00e1bbi r\u00e9szletek\u00e9rt l\u00e1ssa a [dokument\u00e1ci\u00f3t]({docs_url})." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni Home Assistantba, be kell \u00e1ll\u00edtania a Traccar webhook funkci\u00f3j\u00e1t. \n\nHaszn\u00e1lja az URL-t: `{webhook_url}`\n \nB\u0151vebb inform\u00e1ci\u00f3 [a dokument\u00e1ci\u00f3ban]({docs_url}) olvashat\u00f3." }, "step": { "user": { diff --git a/homeassistant/components/traccar/translations/tr.json b/homeassistant/components/traccar/translations/tr.json index e0657d5fddf..fbdde3f796f 100644 --- a/homeassistant/components/traccar/translations/tr.json +++ b/homeassistant/components/traccar/translations/tr.json @@ -6,7 +6,7 @@ "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." }, "create_entry": { - "default": "Olaylar\u0131 Home Assistant'a g\u00f6ndermek i\u00e7in Traccar'da webhook \u00f6zelli\u011fini kurman\u0131z gerekir. \n\n \u015eu URL'yi kullan\u0131n: ` {webhook_url} ` \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url}" + "default": "Olaylar\u0131 Home Assistant'a g\u00f6ndermek i\u00e7in Traccar'da webhook \u00f6zelli\u011fini kurman\u0131z gerekir. \n\n \u015eu URL'yi kullan\u0131n: ` {webhook_url} ` \n\n Daha fazla ayr\u0131nt\u0131 i\u00e7in [belgelere]( {docs_url} ) bak\u0131n." }, "step": { "user": { diff --git a/homeassistant/components/tractive/diagnostics.py b/homeassistant/components/tractive/diagnostics.py index 879e9d82e7b..6defd91c0fb 100644 --- a/homeassistant/components/tractive/diagnostics.py +++ b/homeassistant/components/tractive/diagnostics.py @@ -1,6 +1,8 @@ """Diagnostics support for Tractive.""" 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_EMAIL, CONF_PASSWORD @@ -13,7 +15,7 @@ TO_REDACT = {CONF_PASSWORD, CONF_EMAIL, "title", "_id"} async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" trackables = hass.data[DOMAIN][config_entry.entry_id][TRACKABLES] diff --git a/homeassistant/components/tractive/translations/lv.json b/homeassistant/components/tractive/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/tractive/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/tr.json b/homeassistant/components/tractive/translations/tr.json index 270c80ca0a4..f43dfa99719 100644 --- a/homeassistant/components/tractive/translations/tr.json +++ b/homeassistant/components/tractive/translations/tr.json @@ -17,5 +17,17 @@ } } } + }, + "entity": { + "sensor": { + "tracker_state": { + "state": { + "not_reporting": "Rapor edilmiyor", + "operational": "Operasyonel", + "system_shutdown_user": "Sistem kapatma kullan\u0131c\u0131s\u0131", + "system_startup": "Sistem ba\u015flatma" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tractive/translations/uk.json b/homeassistant/components/tractive/translations/uk.json new file mode 100644 index 00000000000..5c722c2a338 --- /dev/null +++ b/homeassistant/components/tractive/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/diagnostics.py b/homeassistant/components/tradfri/diagnostics.py index c415632faa4..271e2a226fe 100644 --- a/homeassistant/components/tradfri/diagnostics.py +++ b/homeassistant/components/tradfri/diagnostics.py @@ -1,7 +1,7 @@ """Diagnostics support for IKEA Tradfri.""" from __future__ import annotations -from typing import cast +from typing import Any, cast from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -12,7 +12,7 @@ from .const import CONF_GATEWAY_ID, COORDINATOR, COORDINATOR_LIST, DOMAIN async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics the Tradfri platform.""" entry_data = hass.data[DOMAIN][entry.entry_id] coordinator_data = entry_data[COORDINATOR] diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index ae4eb460c6c..a12149ef7ed 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,7 +3,7 @@ "name": "IKEA TR\u00c5DFRI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": ["pytradfri[async]==9.0.0"], + "requirements": ["pytradfri[async]==9.0.1"], "homekit": { "models": ["TRADFRI"] }, diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 3b92abfad77..689964cb151 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -89,9 +89,9 @@ SENSOR_DESCRIPTIONS_FAN: tuple[TradfriSensorEntityDescription, ...] = ( TradfriSensorEntityDescription( key="aqi", name="air quality", - device_class=SensorDeviceClass.AQI, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + icon="mdi:air-filter", value=_get_air_quality, ), TradfriSensorEntityDescription( diff --git a/homeassistant/components/tradfri/translations/id.json b/homeassistant/components/tradfri/translations/id.json index f24fdac1980..0d70ec114fc 100644 --- a/homeassistant/components/tradfri/translations/id.json +++ b/homeassistant/components/tradfri/translations/id.json @@ -8,7 +8,7 @@ "cannot_authenticate": "Tidak dapat mengautentikasi, apakah Gateway dipasangkan dengan server lain seperti misalnya Homekit?", "cannot_connect": "Gagal terhubung", "invalid_key": "Gagal mendaftar dengan kunci yang disediakan. Jika ini terus terjadi, coba mulai ulang gateway.", - "timeout": "Waktu tunggu memvalidasi kode telah habis." + "timeout": "Waktu tunggu validasi kode telah habis." }, "step": { "auth": { diff --git a/homeassistant/components/tradfri/translations/lt.json b/homeassistant/components/tradfri/translations/lt.json index 2dff6a15f18..09757832ed3 100644 --- a/homeassistant/components/tradfri/translations/lt.json +++ b/homeassistant/components/tradfri/translations/lt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u012erenginys jau sukonfig\u016bruotas" + }, "step": { "auth": { "data": { diff --git a/homeassistant/components/tradfri/translations/lv.json b/homeassistant/components/tradfri/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/tradfri/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/config_flow.py b/homeassistant/components/trafikverket_ferry/config_flow.py index 1f5d19118eb..a1f984e5556 100644 --- a/homeassistant/components/trafikverket_ferry/config_flow.py +++ b/homeassistant/components/trafikverket_ferry/config_flow.py @@ -32,6 +32,7 @@ DATA_SCHEMA = vol.Schema( options=WEEKDAYS, multiple=True, mode=selector.SelectSelectorMode.DROPDOWN, + translation_key=CONF_WEEKDAY, ) ), } diff --git a/homeassistant/components/trafikverket_ferry/manifest.json b/homeassistant/components/trafikverket_ferry/manifest.json index 26f395debb9..a4c921df564 100644 --- a/homeassistant/components/trafikverket_ferry/manifest.json +++ b/homeassistant/components/trafikverket_ferry/manifest.json @@ -2,7 +2,7 @@ "domain": "trafikverket_ferry", "name": "Trafikverket Ferry", "documentation": "https://www.home-assistant.io/integrations/trafikverket_ferry", - "requirements": ["pytrafikverket==0.2.2"], + "requirements": ["pytrafikverket==0.2.3"], "codeowners": ["@gjohansson-ST"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/trafikverket_ferry/strings.json b/homeassistant/components/trafikverket_ferry/strings.json index 67200eae135..86ce87c92e4 100644 --- a/homeassistant/components/trafikverket_ferry/strings.json +++ b/homeassistant/components/trafikverket_ferry/strings.json @@ -26,5 +26,18 @@ } } } + }, + "selector": { + "weekday": { + "options": { + "mon": "Monday", + "tue": "Tuesday", + "wed": "Wednesday", + "thu": "Thursday", + "fri": "Friday", + "sat": "Saturday", + "sun": "Sunday" + } + } } } diff --git a/homeassistant/components/trafikverket_ferry/translations/bg.json b/homeassistant/components/trafikverket_ferry/translations/bg.json index 05da54a248e..7a6db61503b 100644 --- a/homeassistant/components/trafikverket_ferry/translations/bg.json +++ b/homeassistant/components/trafikverket_ferry/translations/bg.json @@ -22,5 +22,18 @@ } } } + }, + "selector": { + "weekday": { + "options": { + "fri": "\u041f\u0435\u0442\u044a\u043a", + "mon": "\u041f\u043e\u043d\u0435\u0434\u0435\u043b\u043d\u0438\u043a", + "sat": "\u0421\u044a\u0431\u043e\u0442\u0430", + "sun": "\u041d\u0435\u0434\u0435\u043b\u044f", + "thu": "\u0427\u0435\u0442\u0432\u044a\u0440\u0442\u044a\u043a", + "tue": "\u0412\u0442\u043e\u0440\u043d\u0438\u043a", + "wed": "\u0421\u0440\u044f\u0434\u0430" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/ca.json b/homeassistant/components/trafikverket_ferry/translations/ca.json index 0af4ba34155..b9fd595a73a 100644 --- a/homeassistant/components/trafikverket_ferry/translations/ca.json +++ b/homeassistant/components/trafikverket_ferry/translations/ca.json @@ -26,5 +26,18 @@ } } } + }, + "selector": { + "weekday": { + "options": { + "fri": "Divendres", + "mon": "Dilluns", + "sat": "Dissabte", + "sun": "Diumenge", + "thu": "Dijous", + "tue": "Dimarts", + "wed": "Dimecres" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/de.json b/homeassistant/components/trafikverket_ferry/translations/de.json index 298a0e1711c..35660f47785 100644 --- a/homeassistant/components/trafikverket_ferry/translations/de.json +++ b/homeassistant/components/trafikverket_ferry/translations/de.json @@ -26,5 +26,18 @@ } } } + }, + "selector": { + "weekday": { + "options": { + "fri": "Freitag", + "mon": "Montag", + "sat": "Samstag", + "sun": "Sonntag", + "thu": "Donnerstag", + "tue": "Dienstag", + "wed": "Mittwoch" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/en.json b/homeassistant/components/trafikverket_ferry/translations/en.json index 651f3476710..053805d00eb 100644 --- a/homeassistant/components/trafikverket_ferry/translations/en.json +++ b/homeassistant/components/trafikverket_ferry/translations/en.json @@ -26,5 +26,18 @@ } } } + }, + "selector": { + "weekday": { + "options": { + "fri": "Friday", + "mon": "Monday", + "sat": "Saturday", + "sun": "Sunday", + "thu": "Thursday", + "tue": "Tuesday", + "wed": "Wednesday" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/et.json b/homeassistant/components/trafikverket_ferry/translations/et.json index ec791013788..8751eb907d2 100644 --- a/homeassistant/components/trafikverket_ferry/translations/et.json +++ b/homeassistant/components/trafikverket_ferry/translations/et.json @@ -26,5 +26,18 @@ } } } + }, + "selector": { + "weekday": { + "options": { + "fri": "Reede", + "mon": "Esmasp\u00e4ev", + "sat": "Laup\u00e4ev", + "sun": "P\u00fchap\u00e4ev", + "thu": "Neljap\u00e4ev", + "tue": "Teisip\u00e4ev", + "wed": "Kolmap\u00e4ev" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/pl.json b/homeassistant/components/trafikverket_ferry/translations/pl.json index 5e7d133b1e5..a497f1ebd78 100644 --- a/homeassistant/components/trafikverket_ferry/translations/pl.json +++ b/homeassistant/components/trafikverket_ferry/translations/pl.json @@ -26,5 +26,18 @@ } } } + }, + "selector": { + "weekday": { + "options": { + "fri": "pi\u0105tek", + "mon": "poniedzia\u0142ek", + "sat": "sobota", + "sun": "niedziela", + "thu": "czwartek", + "tue": "wtorek", + "wed": "\u015broda" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/ru.json b/homeassistant/components/trafikverket_ferry/translations/ru.json index c87b6e941f3..69411756f0d 100644 --- a/homeassistant/components/trafikverket_ferry/translations/ru.json +++ b/homeassistant/components/trafikverket_ferry/translations/ru.json @@ -26,5 +26,18 @@ } } } + }, + "selector": { + "weekday": { + "options": { + "fri": "\u041f\u044f\u0442\u043d\u0438\u0446\u0430", + "mon": "\u041f\u043e\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u0438\u043a", + "sat": "\u0421\u0443\u0431\u0431\u043e\u0442\u0430", + "sun": "\u0412\u043e\u0441\u043a\u0440\u0435\u0441\u0435\u043d\u044c\u0435", + "thu": "\u0427\u0435\u0442\u0432\u0435\u0440\u0433", + "tue": "\u0412\u0442\u043e\u0440\u043d\u0438\u043a", + "wed": "\u0421\u0440\u0435\u0434\u0430" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_ferry/translations/zh-Hant.json b/homeassistant/components/trafikverket_ferry/translations/zh-Hant.json index 9acd15fdfb1..d18c4565c37 100644 --- a/homeassistant/components/trafikverket_ferry/translations/zh-Hant.json +++ b/homeassistant/components/trafikverket_ferry/translations/zh-Hant.json @@ -26,5 +26,18 @@ } } } + }, + "selector": { + "weekday": { + "options": { + "fri": "\u661f\u671f\u4e94", + "mon": "\u661f\u671f\u4e00", + "sat": "\u661f\u671f\u516d", + "sun": "\u661f\u671f\u5929", + "thu": "\u661f\u671f\u56db", + "tue": "\u661f\u671f\u4e8c", + "wed": "\u661f\u671f\u4e09" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/trafikverket_train/manifest.json b/homeassistant/components/trafikverket_train/manifest.json index 16a125f9561..b123d5cb747 100644 --- a/homeassistant/components/trafikverket_train/manifest.json +++ b/homeassistant/components/trafikverket_train/manifest.json @@ -2,7 +2,7 @@ "domain": "trafikverket_train", "name": "Trafikverket Train", "documentation": "https://www.home-assistant.io/integrations/trafikverket_train", - "requirements": ["pytrafikverket==0.2.2"], + "requirements": ["pytrafikverket==0.2.3"], "codeowners": ["@endor-force", "@gjohansson-ST"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/trafikverket_weatherstation/manifest.json b/homeassistant/components/trafikverket_weatherstation/manifest.json index a1d9ca21c02..444688d559f 100644 --- a/homeassistant/components/trafikverket_weatherstation/manifest.json +++ b/homeassistant/components/trafikverket_weatherstation/manifest.json @@ -2,7 +2,7 @@ "domain": "trafikverket_weatherstation", "name": "Trafikverket Weather Station", "documentation": "https://www.home-assistant.io/integrations/trafikverket_weatherstation", - "requirements": ["pytrafikverket==0.2.2"], + "requirements": ["pytrafikverket==0.2.3"], "codeowners": ["@endor-force", "@gjohansson-ST"], "config_flow": true, "iot_class": "cloud_polling", diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 7f8128e060c..b1ff20627e1 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -9,7 +9,9 @@ from homeassistant.components.sensor import SensorDeviceClass, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, STATE_IDLE, UnitOfDataRate from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import TransmissionClient @@ -58,6 +60,12 @@ class TransmissionSensor(SensorEntity): self._name = sensor_name self._sub_type = sub_type self._state = None + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, tm_client.config_entry.entry_id)}, + manufacturer="Transmission", + name=client_name, + ) @property def name(self): diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index 0fd9ffee51e..ed771d24581 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -6,7 +6,9 @@ from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN, SWITCH_TYPES @@ -36,15 +38,21 @@ class TransmissionSwitch(SwitchEntity): _attr_should_poll = False - def __init__(self, switch_type, switch_name, tm_client, name): + def __init__(self, switch_type, switch_name, tm_client, client_name): """Initialize the Transmission switch.""" self._name = switch_name - self.client_name = name + self.client_name = client_name self.type = switch_type self._tm_client = tm_client self._state = STATE_OFF self._data = None self.unsub_update = None + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, tm_client.config_entry.entry_id)}, + manufacturer="Transmission", + name=client_name, + ) @property def name(self): diff --git a/homeassistant/components/transmission/translations/ca.json b/homeassistant/components/transmission/translations/ca.json index 29c4c0a35d2..ee3bd3b55b0 100644 --- a/homeassistant/components/transmission/translations/ca.json +++ b/homeassistant/components/transmission/translations/ca.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Actualitza totes les automatitzacions o 'scripts' que utilitzin aquest servei. S'han de substituir totes les claus o entrades anomenades 'name' per 'entry_id'.", - "title": "S'est\u00e0 eliminant la clau 'name' del servei Transmission" - } - } - }, - "title": "S'est\u00e0 eliminant la clau 'name' del servei Transmission" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/de.json b/homeassistant/components/transmission/translations/de.json index 1d04d0674a7..04274f2c1cb 100644 --- a/homeassistant/components/transmission/translations/de.json +++ b/homeassistant/components/transmission/translations/de.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Aktualisiere alle Automatisierungen oder Skripte, die diesen Dienst verwenden, und ersetze den Namensschl\u00fcssel durch den entry_id Schl\u00fcssel.", - "title": "Der Namensschl\u00fcssel in den \u00dcbertragungsdiensten wird entfernt" - } - } - }, - "title": "Der Namensschl\u00fcssel in den \u00dcbertragungsdiensten wird entfernt" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/el.json b/homeassistant/components/transmission/translations/el.json index e18065b34bf..2eab6fea0e6 100644 --- a/homeassistant/components/transmission/translations/el.json +++ b/homeassistant/components/transmission/translations/el.json @@ -25,23 +25,10 @@ "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" + "title": "\u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b5 \u03c4\u03bf Transmission Client" } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "\u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03c4\u03c5\u03c7\u03cc\u03bd \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03ba\u03b1\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03bf\u03bd\u03cc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03bc\u03b5 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af entry_id.", - "title": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03bf\u03bd\u03cc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c3\u03c4\u03bf Transmission \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" - } - } - }, - "title": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03bf\u03bd\u03cc\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c3\u03c4\u03bf Transmission \u03bc\u03b5\u03c4\u03ac\u03b4\u03bf\u03c3\u03b7\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/en.json b/homeassistant/components/transmission/translations/en.json index ff2a6b779e7..c31aa573b9d 100644 --- a/homeassistant/components/transmission/translations/en.json +++ b/homeassistant/components/transmission/translations/en.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Update any automations or scripts that use this service and replace the name key with the entry_id key.", - "title": "The name key in Transmission services is being removed" - } - } - }, - "title": "The name key in Transmission services is being removed" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/es.json b/homeassistant/components/transmission/translations/es.json index 69242bda413..30180811cb4 100644 --- a/homeassistant/components/transmission/translations/es.json +++ b/homeassistant/components/transmission/translations/es.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Actualiza cualquier automatizaci\u00f3n o script que use este servicio y sustituye la clave nombre por la clave entry_id.", - "title": "Se va a eliminar la clave nombre en los servicios de Transmission" - } - } - }, - "title": "Se va a eliminar la clave nombre en los servicios de Transmission" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/et.json b/homeassistant/components/transmission/translations/et.json index 3fab9d169db..745ef1030af 100644 --- a/homeassistant/components/transmission/translations/et.json +++ b/homeassistant/components/transmission/translations/et.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "V\u00e4rskenda k\u00f5iki seda teenust kasutavaid automatiseerimisi v\u00f5i skripte ja asenda nimev\u00f5ti v\u00f5tmega entry_id-ga.", - "title": "Transmission teenuste nimev\u00f5ti eemaldatakse" - } - } - }, - "title": "Transmission teenuste nimev\u00f5ti eemaldatakse" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/he.json b/homeassistant/components/transmission/translations/he.json index 31a8887945c..73c5f6c7384 100644 --- a/homeassistant/components/transmission/translations/he.json +++ b/homeassistant/components/transmission/translations/he.json @@ -25,10 +25,5 @@ } } } - }, - "issues": { - "deprecated_key": { - "title": "\u05de\u05e4\u05ea\u05d7 \u05d4\u05e9\u05dd \u05d1\u05e9\u05d9\u05e8\u05d5\u05ea\u05d9 \u05e9\u05d9\u05d3\u05d5\u05e8 \u05de\u05d5\u05e1\u05e8" - } } } \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/hu.json b/homeassistant/components/transmission/translations/hu.json index b9eae6b7f6f..46c11736f4d 100644 --- a/homeassistant/components/transmission/translations/hu.json +++ b/homeassistant/components/transmission/translations/hu.json @@ -25,23 +25,10 @@ "port": "Port", "username": "Felhaszn\u00e1l\u00f3n\u00e9v" }, - "title": "\u00c1tviteli \u00fcgyf\u00e9l be\u00e1ll\u00edt\u00e1sa" + "title": "Transmission kliens be\u00e1ll\u00edt\u00e1sa" } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Friss\u00edtsen minden olyan automatiz\u00e1l\u00e1st vagy szkriptet, amely ezt a szolg\u00e1ltat\u00e1st haszn\u00e1lja, \u00e9s cser\u00e9lje ki a name kulcsot a entry_id kulcsra.", - "title": "A n\u00e9vkulcs a Transmission szolg\u00e1ltat\u00e1sokban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" - } - } - }, - "title": "A n\u00e9vkulcs a Transmission szolg\u00e1ltat\u00e1sokban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/id.json b/homeassistant/components/transmission/translations/id.json index 98a5e918740..7b5fa3a703f 100644 --- a/homeassistant/components/transmission/translations/id.json +++ b/homeassistant/components/transmission/translations/id.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Perbarui semua otomasi atau skrip yang menggunakan layanan ini dan ganti kunci name dengan kunci entry_id.", - "title": "Kunci name dalam layanan Transmission sedang dihapus" - } - } - }, - "title": "Kunci name dalam layanan Transmission sedang dihapus" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/it.json b/homeassistant/components/transmission/translations/it.json index f920833f56e..c3624486032 100644 --- a/homeassistant/components/transmission/translations/it.json +++ b/homeassistant/components/transmission/translations/it.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Aggiorna eventuali automazioni o script che utilizzano questo servizio e sostituisci la chiave del nome con la chiave entry_id.", - "title": "La chiave del nome nei servizi di trasmissione \u00e8 stata rimossa" - } - } - }, - "title": "La chiave del nome nei servizi di trasmissione \u00e8 stata rimossa" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/lt.json b/homeassistant/components/transmission/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/transmission/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/lv.json b/homeassistant/components/transmission/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/transmission/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/no.json b/homeassistant/components/transmission/translations/no.json index 89e3b1ce9c5..4aec146a9e0 100644 --- a/homeassistant/components/transmission/translations/no.json +++ b/homeassistant/components/transmission/translations/no.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Oppdater eventuelle automatiseringer eller skript som bruker denne tjenesten og erstatt navnen\u00f8kkelen med entry_id-n\u00f8kkelen.", - "title": "Navnen\u00f8kkelen i overf\u00f8ringstjenester fjernes" - } - } - }, - "title": "Navnen\u00f8kkelen i overf\u00f8ringstjenester fjernes" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/pl.json b/homeassistant/components/transmission/translations/pl.json index 7ae84ea4f4e..994744a3547 100644 --- a/homeassistant/components/transmission/translations/pl.json +++ b/homeassistant/components/transmission/translations/pl.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Zaktualizuj wszystkie automatyzacje lub skrypty korzystaj\u0105ce z tej us\u0142ugi i zast\u0105p klucz nazwy kluczem entry_id.", - "title": "Klucz nazwy w us\u0142ugach Transmission zostanie usuni\u0119ty" - } - } - }, - "title": "Klucz nazwy w us\u0142ugach Transmission zostanie usuni\u0119ty" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/pt-BR.json b/homeassistant/components/transmission/translations/pt-BR.json index 878e911564d..5579b64e2d9 100644 --- a/homeassistant/components/transmission/translations/pt-BR.json +++ b/homeassistant/components/transmission/translations/pt-BR.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Atualize quaisquer automa\u00e7\u00f5es ou scripts que usam esse servi\u00e7o e substitua a chave de nome pela chave entry_id.", - "title": "A chave de nome nos servi\u00e7os de transmiss\u00e3o est\u00e1 sendo removida" - } - } - }, - "title": "A chave de nome nos servi\u00e7os de transmiss\u00e3o est\u00e1 sendo removida" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/ru.json b/homeassistant/components/transmission/translations/ru.json index cfd1c7e0e84..ba6787eed7d 100644 --- a/homeassistant/components/transmission/translations/ru.json +++ b/homeassistant/components/transmission/translations/ru.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "\u0412 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u044f\u0445 \u0438 \u0441\u043a\u0440\u0438\u043f\u0442\u0430\u0445, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0445 \u044d\u0442\u0443 \u0441\u043b\u0443\u0436\u0431\u0443, \u0441\u043b\u0435\u0434\u0443\u0435\u0442 \u0437\u0430\u043c\u0435\u043d\u0438\u0442\u044c \u043a\u043b\u044e\u0447 name \u043d\u0430 \u043a\u043b\u044e\u0447 entry_id.", - "title": "\u0412 \u0441\u043b\u0443\u0436\u0431\u0430\u0445 Transmission \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0451\u043d \u043a\u043b\u044e\u0447 name" - } - } - }, - "title": "\u0412 \u0441\u043b\u0443\u0436\u0431\u0430\u0445 Transmission \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0451\u043d \u043a\u043b\u044e\u0447 name" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/sk.json b/homeassistant/components/transmission/translations/sk.json index c688d9a4906..486db20b55a 100644 --- a/homeassistant/components/transmission/translations/sk.json +++ b/homeassistant/components/transmission/translations/sk.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "Aktualizujte v\u0161etky automatiz\u00e1cie alebo skripty, ktor\u00e9 pou\u017e\u00edvaj\u00fa t\u00fato slu\u017ebu, a nahra\u010fte k\u013e\u00fa\u010d n\u00e1zvu k\u013e\u00fa\u010dom entry_id.", - "title": "K\u013e\u00fa\u010d s n\u00e1zvom v slu\u017eb\u00e1ch prenosu sa odstra\u0148uje" - } - } - }, - "title": "K\u013e\u00fa\u010d s n\u00e1zvom v slu\u017eb\u00e1ch prenosu sa odstra\u0148uje" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/transmission/translations/tr.json b/homeassistant/components/transmission/translations/tr.json index bef2fd47ff7..e1b5e132046 100644 --- a/homeassistant/components/transmission/translations/tr.json +++ b/homeassistant/components/transmission/translations/tr.json @@ -25,7 +25,7 @@ "port": "Port", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "title": "\u0130letim \u0130stemcisi Kurulumu" + "title": "\u0130letim \u0130stemcisini Kur" } } }, diff --git a/homeassistant/components/transmission/translations/zh-Hant.json b/homeassistant/components/transmission/translations/zh-Hant.json index 235a13f2c01..fd3d3a909aa 100644 --- a/homeassistant/components/transmission/translations/zh-Hant.json +++ b/homeassistant/components/transmission/translations/zh-Hant.json @@ -29,19 +29,6 @@ } } }, - "issues": { - "deprecated_key": { - "fix_flow": { - "step": { - "confirm": { - "description": "\u4f7f\u7528\u6b64\u670d\u52d9\u4ee5\u66f4\u65b0\u4efb\u4f55\u81ea\u52d5\u5316\u6216\u8173\u672c\u3001\u4ee5\u53d6\u4ee3 name key \u70ba entry_id key\u3002", - "title": "Transmission \u4e2d\u7684 name key \u670d\u52d9\u5373\u5c07\u79fb\u9664" - } - } - }, - "title": "Transmission \u4e2d\u7684 name key \u670d\u52d9\u5373\u5c07\u79fb\u9664" - } - }, "options": { "step": { "init": { diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index c07df07ae4f..38aa6a4706e 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -11,7 +11,7 @@ import mimetypes import os from pathlib import Path import re -from typing import TYPE_CHECKING, Any, Optional, TypedDict, cast +from typing import TYPE_CHECKING, Any, TypedDict, cast from aiohttp import web import mutagen @@ -51,7 +51,7 @@ from .media_source import generate_media_source_id, media_source_id_to_kwargs _LOGGER = logging.getLogger(__name__) -TtsAudioType = tuple[Optional[str], Optional[bytes]] +TtsAudioType = tuple[str | None, bytes | None] ATTR_CACHE = "cache" ATTR_LANGUAGE = "language" @@ -621,9 +621,18 @@ class SpeechManager: if not tts_file.tags: tts_file.add_tags() if isinstance(tts_file.tags, ID3): - tts_file["artist"] = ID3Text(encoding=3, text=artist) # type: ignore[no-untyped-call] - tts_file["album"] = ID3Text(encoding=3, text=album) # type: ignore[no-untyped-call] - tts_file["title"] = ID3Text(encoding=3, text=message) # type: ignore[no-untyped-call] + tts_file["artist"] = ID3Text( + encoding=3, + text=artist, # type: ignore[no-untyped-call] + ) + tts_file["album"] = ID3Text( + encoding=3, + text=album, # type: ignore[no-untyped-call] + ) + tts_file["title"] = ID3Text( + encoding=3, + text=message, # type: ignore[no-untyped-call] + ) else: tts_file["artist"] = artist tts_file["album"] = album diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 64151195b01..564bfab8b14 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -162,10 +162,10 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): celsius_type = self.find_dpcode( (DPCode.TEMP_CURRENT, DPCode.UPPER_TEMP), dptype=DPType.INTEGER ) - farhenheit_type = self.find_dpcode( + fahrenheit_type = self.find_dpcode( (DPCode.TEMP_CURRENT_F, DPCode.UPPER_TEMP_F), dptype=DPType.INTEGER ) - if farhenheit_type and ( + if fahrenheit_type and ( prefered_temperature_unit == UnitOfTemperature.FAHRENHEIT or ( prefered_temperature_unit == UnitOfTemperature.CELSIUS @@ -173,7 +173,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): ) ): self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT - self._current_temperature = farhenheit_type + self._current_temperature = fahrenheit_type elif celsius_type: self._attr_temperature_unit = UnitOfTemperature.CELSIUS self._current_temperature = celsius_type @@ -182,17 +182,17 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): celsius_type = self.find_dpcode( DPCode.TEMP_SET, dptype=DPType.INTEGER, prefer_function=True ) - farhenheit_type = self.find_dpcode( + fahrenheit_type = self.find_dpcode( DPCode.TEMP_SET_F, dptype=DPType.INTEGER, prefer_function=True ) - if farhenheit_type and ( + if fahrenheit_type and ( prefered_temperature_unit == UnitOfTemperature.FAHRENHEIT or ( prefered_temperature_unit == UnitOfTemperature.CELSIUS and not celsius_type ) ): - self._set_temperature = farhenheit_type + self._set_temperature = fahrenheit_type elif celsius_type: self._set_temperature = celsius_type @@ -296,7 +296,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity): """Set new target fan mode.""" self._send_command([{"code": DPCode.FAN_SPEED_ENUM, "value": fan_mode}]) - def set_humidity(self, humidity: float) -> None: + def set_humidity(self, humidity: int) -> None: """Set new target humidity.""" if self._set_humidity is None: raise RuntimeError( diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index 75ccffce685..20dc724deb9 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -173,7 +173,9 @@ class DPCode(StrEnum): CUR_VOLTAGE = "cur_voltage" # Actual voltage DECIBEL_SENSITIVITY = "decibel_sensitivity" DECIBEL_SWITCH = "decibel_switch" + DEHUMIDITY_SET_ENUM = "dehumidify_set_enum" DEHUMIDITY_SET_VALUE = "dehumidify_set_value" + DISINFECTION = "disinfection" DO_NOT_DISTURB = "do_not_disturb" DOORCONTACT_STATE = "doorcontact_state" # Status of door window sensor DOORCONTACT_STATE_2 = "doorcontact_state_2" @@ -208,6 +210,7 @@ class DPCode(StrEnum): HUMIDIFIER = "humidifier" # Humidification HUMIDITY = "humidity" # Humidity HUMIDITY_CURRENT = "humidity_current" # Current humidity + HUMIDITY_INDOOR = "humidity_indoor" # Indoor humidity HUMIDITY_SET = "humidity_set" # Humidity setting HUMIDITY_VALUE = "humidity_value" # Humidity IPC_WORK_MODE = "ipc_work_mode" @@ -327,6 +330,7 @@ class DPCode(StrEnum): TEMP_CONTROLLER = "temp_controller" TEMP_CURRENT = "temp_current" # Current temperature in °C TEMP_CURRENT_F = "temp_current_f" # Current temperature in °F + TEMP_INDOOR = "temp_indoor" # Indoor temperature in °C TEMP_SET = "temp_set" # Set the temperature in °C TEMP_SET_F = "temp_set_f" # Set the temperature in °F TEMP_UNIT_CONVERT = "temp_unit_convert" # Temperature unit switching @@ -354,6 +358,7 @@ class DPCode(StrEnum): VOLUME_SET = "volume_set" WARM = "warm" # Heat preservation WARM_TIME = "warm_time" # Heat preservation time + WATER = "water" WATER_RESET = "water_reset" # Resetting of water usage days WATER_SET = "water_set" # Water level WATERSENSOR_STATE = "watersensor_state" diff --git a/homeassistant/components/tuya/fan.py b/homeassistant/components/tuya/fan.py index 021745f4c81..59cfee3506c 100644 --- a/homeassistant/components/tuya/fan.py +++ b/homeassistant/components/tuya/fan.py @@ -29,6 +29,7 @@ TUYA_SUPPORT_TYPE = { "fsd", # Fan with Light "fskg", # Fan wall switch "kj", # Air Purifier + "cs", # Dehumidifier } diff --git a/homeassistant/components/tuya/humidifier.py b/homeassistant/components/tuya/humidifier.py index 765de4d860a..a9564b94ddc 100644 --- a/homeassistant/components/tuya/humidifier.py +++ b/homeassistant/components/tuya/humidifier.py @@ -146,7 +146,7 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity): """Turn the device off.""" self._send_command([{"code": self._switch_dpcode, "value": False}]) - def set_humidity(self, humidity): + def set_humidity(self, humidity: int) -> None: """Set new target humidity.""" if self._set_humidity is None: raise RuntimeError( diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 57059a3bcc7..54d46f73b88 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -342,6 +342,23 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = { translation_key="countdown", ), ), + # Dehumidifier + # https://developer.tuya.com/en/docs/iot/categorycs?id=Kaiuz1vcz4dha + "cs": ( + SelectEntityDescription( + key=DPCode.COUNTDOWN_SET, + name="Countdown", + entity_category=EntityCategory.CONFIG, + icon="mdi:timer-cog-outline", + translation_key="countdown", + ), + SelectEntityDescription( + key=DPCode.DEHUMIDITY_SET_ENUM, + name="Target humidity", + entity_category=EntityCategory.CONFIG, + icon="mdi:water-percent", + ), + ), } # Socket (duplicate of `kg`) diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 7b09ecfaf74..15803feeaf7 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -984,6 +984,37 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = { state_class=SensorStateClass.MEASUREMENT, ), ), + # eMylo Smart WiFi IR Remote + "wnykq": ( + TuyaSensorEntityDescription( + key=DPCode.VA_TEMPERATURE, + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.VA_HUMIDITY, + name="Humidity", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + ), + # Dehumidifier + # https://developer.tuya.com/en/docs/iot/s?id=K9gf48r6jke8e + "cs": ( + TuyaSensorEntityDescription( + key=DPCode.TEMP_INDOOR, + name="Temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + TuyaSensorEntityDescription( + key=DPCode.HUMIDITY_INDOOR, + name="Humidity", + device_class=SensorDeviceClass.HUMIDITY, + state_class=SensorStateClass.MEASUREMENT, + ), + ), } # Socket (duplicate of `kg`) diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index 329b170c53f..dd9a156c554 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -38,6 +38,20 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), + # EasyBaby + # Undocumented, might have a wider use + "cn": ( + SwitchEntityDescription( + key=DPCode.DISINFECTION, + name="Disinfection", + icon="mdi:bacteria", + ), + SwitchEntityDescription( + key=DPCode.WATER, + name="Water", + icon="mdi:water", + ), + ), # Smart Pet Feeder # https://developer.tuya.com/en/docs/iot/categorycwwsq?id=Kaiuz2b6vydld "cwwsq": ( @@ -296,6 +310,12 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { SwitchEntityDescription( key=DPCode.SWITCH, name="Switch", + icon="mdi:power", + entity_category=EntityCategory.CONFIG, + ), + SwitchEntityDescription( + key=DPCode.START, + name="Start", icon="mdi:pot-steam", entity_category=EntityCategory.CONFIG, ), @@ -553,6 +573,15 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { entity_category=EntityCategory.CONFIG, ), ), + # SIREN: Siren (switch) with Temperature and humidity sensor + # https://developer.tuya.com/en/docs/iot/f?id=Kavck4sr3o5ek + "wsdcg": ( + SwitchEntityDescription( + key=DPCode.SWITCH, + name="Switch", + device_class=SwitchDeviceClass.OUTLET, + ), + ), # Ceiling Light # https://developer.tuya.com/en/docs/iot/ceiling-light?id=Kaiuz03xxfc4r "xdd": ( diff --git a/homeassistant/components/tuya/translations/fr.json b/homeassistant/components/tuya/translations/fr.json index a8049917f41..c8630ac6bac 100644 --- a/homeassistant/components/tuya/translations/fr.json +++ b/homeassistant/components/tuya/translations/fr.json @@ -16,5 +16,36 @@ "description": "Saisissez vos informations d'identification Tuya." } } + }, + "entity": { + "select": { + "countdown": { + "state": { + "1h": "1\u00a0heure", + "2h": "2\u00a0heures", + "3h": "3\u00a0heures", + "4h": "4\u00a0heures", + "5h": "5\u00a0heures", + "6h": "6\u00a0heures", + "cancel": "Annuler" + } + }, + "curtain_mode": { + "state": { + "morning": "Matin", + "night": "Nuit" + } + }, + "humidifier_spray_mode": { + "state": { + "humidity": "Humidit\u00e9" + } + }, + "led_type": { + "state": { + "halogen": "Halog\u00e8ne" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/lt.json b/homeassistant/components/tuya/translations/lt.json new file mode 100644 index 00000000000..1ab2154e4c8 --- /dev/null +++ b/homeassistant/components/tuya/translations/lt.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis", + "username": "Paskyra" + } + } + } + }, + "entity": { + "select": { + "vacuum_collection": { + "state": { + "small": "Ma\u017ea" + } + }, + "vacuum_mode": { + "state": { + "bow": "Lankas", + "chargego": "Gr\u012f\u017eti \u012f stotel\u0119", + "mop": "Plauna", + "part": "Dalis", + "pick_zone": "Pasirinkite zon\u0105", + "point": "Ta\u0161kas valymas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/nl.json b/homeassistant/components/tuya/translations/nl.json index c655330805b..50c21a29485 100644 --- a/homeassistant/components/tuya/translations/nl.json +++ b/homeassistant/components/tuya/translations/nl.json @@ -28,6 +28,7 @@ }, "basic_nightvision": { "state": { + "0": "Automatisch", "1": "Uit", "2": "Aan" } @@ -39,7 +40,8 @@ "3h": "3 uur", "4h": "4 uur", "5h": "5 uur", - "6h": "6 uur" + "6h": "6 uur", + "cancel": "Annuleren" } }, "curtain_mode": { @@ -47,6 +49,11 @@ "night": "Nacht" } }, + "curtain_motor_mode": { + "state": { + "back": "Terug" + } + }, "fan_angle": { "state": { "30": "30\u00b0", @@ -80,7 +87,21 @@ "state": { "off": "Uit", "on": "Aan", - "power_off": "Uit" + "power_off": "Uit", + "power_on": "Aan" + } + }, + "vacuum_cistern": { + "state": { + "closed": "Gesloten", + "high": "Hoog", + "low": "Laag" + } + }, + "vacuum_collection": { + "state": { + "large": "Groot", + "small": "Klein" } } }, diff --git a/homeassistant/components/tuya/translations/select.lt.json b/homeassistant/components/tuya/translations/select.lt.json new file mode 100644 index 00000000000..e32659a64c6 --- /dev/null +++ b/homeassistant/components/tuya/translations/select.lt.json @@ -0,0 +1,13 @@ +{ + "state": { + "tuya__vacuum_cistern": { + "closed": "U\u017edaryta", + "high": "Auk\u0161tas", + "low": "\u017demas", + "middle": "Vidurio" + }, + "tuya__vacuum_collection": { + "large": "Didelis" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/select.uk.json b/homeassistant/components/tuya/translations/select.uk.json index 3e802b0e97f..03d26cf6fce 100644 --- a/homeassistant/components/tuya/translations/select.uk.json +++ b/homeassistant/components/tuya/translations/select.uk.json @@ -1,5 +1,9 @@ { "state": { + "tuya__countdown": { + "1h": "1 \u0433\u043e\u0434\u0438\u043d\u0430", + "cancel": "\u0421\u043a\u0430\u0441\u0443\u0432\u0430\u0442\u0438" + }, "tuya__curtain_mode": { "morning": "\u0420\u0430\u043d\u043e\u043a", "night": "\u041d\u0456\u0447" diff --git a/homeassistant/components/tuya/translations/tr.json b/homeassistant/components/tuya/translations/tr.json index fe1b81831a6..bbb26eade83 100644 --- a/homeassistant/components/tuya/translations/tr.json +++ b/homeassistant/components/tuya/translations/tr.json @@ -16,5 +16,201 @@ "description": "Tuya kimlik bilgilerinizi girin." } } + }, + "entity": { + "select": { + "basic_anti_flicker": { + "state": { + "0": "Devre d\u0131\u015f\u0131", + "1": "50 Hz", + "2": "60 Hz" + } + }, + "basic_nightvision": { + "state": { + "0": "Otomatik", + "1": "Kapal\u0131", + "2": "A\u00e7\u0131k" + } + }, + "countdown": { + "state": { + "1h": "1 saat", + "2h": "2 saat", + "3h": "3 saat", + "4h": "4 saat", + "5h": "5 saat", + "6h": "6 saat", + "cancel": "\u0130ptal" + } + }, + "curtain_mode": { + "state": { + "morning": "Sabah", + "night": "Gece" + } + }, + "curtain_motor_mode": { + "state": { + "back": "Geri", + "forward": "\u0130leri" + } + }, + "decibel_sensitivity": { + "state": { + "0": "D\u00fc\u015f\u00fck hassasiyet", + "1": "Y\u00fcksek hassasiyet" + } + }, + "fan_angle": { + "state": { + "30": "30\u00b0", + "60": "60\u00b0", + "90": "90\u00b0" + } + }, + "fingerbot_mode": { + "state": { + "click": "Bildirim", + "switch": "Anahtar" + } + }, + "humidifier_level": { + "state": { + "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" + } + }, + "humidifier_moodlighting": { + "state": { + "1": "Mod 1", + "2": "Mod 2", + "3": "Mod 3", + "4": "Mod 4", + "5": "Mod 5" + } + }, + "humidifier_spray_mode": { + "state": { + "auto": "Otomatik", + "health": "Sa\u011fl\u0131k", + "humidity": "Nem", + "sleep": "Uyku", + "work": "\u0130\u015f" + } + }, + "ipc_work_mode": { + "state": { + "0": "D\u00fc\u015f\u00fck g\u00fc\u00e7 modu", + "1": "S\u00fcrekli \u00e7al\u0131\u015fma modu" + } + }, + "led_type": { + "state": { + "halogen": "Halojen", + "incandescent": "Akkor", + "led": "LED" + } + }, + "light_mode": { + "state": { + "none": "Kapal\u0131", + "pos": "Anahtar konumunu belirtin", + "relay": "A\u00e7ma/kapama durumunu belirtin" + } + }, + "motion_sensitivity": { + "state": { + "0": "D\u00fc\u015f\u00fck hassasiyet", + "1": "Orta hassasiyet", + "2": "Y\u00fcksek hassasiyet" + } + }, + "record_mode": { + "state": { + "1": "Yaln\u0131zca olaylar\u0131 kaydet", + "2": "S\u00fcrekli kay\u0131t" + } + }, + "relay_status": { + "state": { + "last": "Son durumu hat\u0131rla", + "memory": "Son durumu hat\u0131rla", + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k", + "power_off": "Kapal\u0131", + "power_on": "A\u00e7\u0131k" + } + }, + "vacuum_cistern": { + "state": { + "closed": "Kapand\u0131", + "high": "Y\u00fcksek", + "low": "D\u00fc\u015f\u00fck", + "middle": "Orta" + } + }, + "vacuum_collection": { + "state": { + "large": "B\u00fcy\u00fck", + "middle": "Orta", + "small": "K\u00fc\u00e7\u00fck" + } + }, + "vacuum_mode": { + "state": { + "bow": "Yay", + "chargego": "Dock'a geri d\u00f6n", + "left_bow": "Sola Yay", + "left_spiral": "Spiral Sol", + "mop": "Paspas", + "part": "B\u00f6l\u00fcm", + "partial_bow": "K\u0131smen Yay", + "pick_zone": "Se\u00e7im B\u00f6lgesi", + "point": "Nokta", + "pose": "Poz", + "random": "Rastgele", + "right_bow": "Sa\u011fa E\u011fil", + "right_spiral": "Spiral Sa\u011f", + "single": "Tek", + "smart": "Ak\u0131ll\u0131", + "spiral": "Sarmal", + "standby": "Bekleme modu", + "wall_follow": "Duvar\u0131 Takip Et", + "zone": "B\u00f6lge" + } + } + }, + "sensor": { + "air_quality": { + "state": { + "good": "\u0130yi", + "great": "B\u00fcy\u00fck", + "mild": "Hafif", + "severe": "\u015eiddetli" + } + }, + "status": { + "state": { + "boiling_temp": "Kaynama s\u0131cakl\u0131\u011f\u0131", + "cooling": "So\u011futuluyor", + "heating": "Is\u0131t\u0131l\u0131yor", + "heating_temp": "Is\u0131tma s\u0131cakl\u0131\u011f\u0131", + "reserve_1": "Rezerv 1", + "reserve_2": "Rezerv 2", + "reserve_3": "Rezerv 3", + "standby": "Bekleme modu", + "warm": "Is\u0131 korumas\u0131" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/calendar.py b/homeassistant/components/twentemilieu/calendar.py index 8bcf1b1d390..d3685051734 100644 --- a/homeassistant/components/twentemilieu/calendar.py +++ b/homeassistant/components/twentemilieu/calendar.py @@ -1,7 +1,9 @@ """Support for Twente Milieu Calendar.""" from __future__ import annotations -from datetime import datetime +from datetime import date, datetime + +from twentemilieu import WasteType from homeassistant.components.calendar import CalendarEntity, CalendarEvent from homeassistant.config_entries import ConfigEntry @@ -33,7 +35,7 @@ class TwenteMilieuCalendar(TwenteMilieuEntity, CalendarEntity): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[dict[WasteType, list[date]]], entry: ConfigEntry, ) -> None: """Initialize the Twente Milieu entity.""" diff --git a/homeassistant/components/twentemilieu/diagnostics.py b/homeassistant/components/twentemilieu/diagnostics.py index a158ead0909..5a47e5282a4 100644 --- a/homeassistant/components/twentemilieu/diagnostics.py +++ b/homeassistant/components/twentemilieu/diagnostics.py @@ -1,8 +1,11 @@ """Diagnostics support for TwenteMilieu.""" from __future__ import annotations +from datetime import date from typing import Any +from twentemilieu import WasteType + from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant @@ -15,8 +18,10 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - coordinator: DataUpdateCoordinator = hass.data[DOMAIN][entry.data[CONF_ID]] + coordinator: DataUpdateCoordinator[dict[WasteType, list[date]]] = hass.data[DOMAIN][ + entry.data[CONF_ID] + ] return { - waste_type: [waste_date.isoformat() for waste_date in waste_dates] + str(waste_type): [waste_date.isoformat() for waste_date in waste_dates] for waste_type, waste_dates in coordinator.data.items() } diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index ab69aba9abf..ab0a60c44ca 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -93,7 +93,7 @@ class TwenteMilieuSensor(TwenteMilieuEntity, SensorEntity): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[dict[WasteType, list[date]]], description: TwenteMilieuSensorDescription, entry: ConfigEntry, ) -> None: diff --git a/homeassistant/components/twilio/translations/hu.json b/homeassistant/components/twilio/translations/hu.json index 409a9b08a72..5184d4a1f4a 100644 --- a/homeassistant/components/twilio/translations/hu.json +++ b/homeassistant/components/twilio/translations/hu.json @@ -6,7 +6,7 @@ "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." }, "create_entry": { - "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni a Home Assistant alkalmaz\u00e1snak, be kell \u00e1ll\u00edtania a [Webhooks Twilio-val]({twilio_url}) alkalmaz\u00e1st. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - M\u00f3dszer: POST\n - Tartalom t\u00edpusa: application/x-www-form-urlencoded \n\n L\u00e1sd [a dokument\u00e1ci\u00f3]({docs_url}), hogyan konfigur\u00e1lhatja az automatizmusokat a bej\u00f6v\u0151 adatok kezel\u00e9s\u00e9re." + "default": "Ha esem\u00e9nyeket szeretne k\u00fcldeni Home Assistantba, be kell \u00e1ll\u00edtania a [Webhooks Twilio-val]({twilio_url}) alkalmaz\u00e1st. \n\n T\u00f6ltse ki a k\u00f6vetkez\u0151 inform\u00e1ci\u00f3kat: \n\n - URL: `{webhook_url}`\n - Met\u00f3dus: POST\n - Tartalom t\u00edpusa: application/x-www-form-urlencoded \n\nB\u0151vebb inform\u00e1ci\u00f3 [a dokument\u00e1ci\u00f3ban]({docs_url}) olvashat\u00f3, hogyan konfigur\u00e1lhatja az automatizmusokat a be\u00e9rkez\u0151 adatok kezel\u00e9s\u00e9re." }, "step": { "user": { diff --git a/homeassistant/components/twilio/translations/tr.json b/homeassistant/components/twilio/translations/tr.json index fa92c795f25..0262838cd8b 100644 --- a/homeassistant/components/twilio/translations/tr.json +++ b/homeassistant/components/twilio/translations/tr.json @@ -10,7 +10,7 @@ }, "step": { "user": { - "description": "Kuruluma ba\u015flamak ister misiniz?", + "description": "Kurulumu ba\u015flatmak istiyor musunuz?", "title": "Twilio Webhook'u kurun" } } diff --git a/homeassistant/components/twinkly/const.py b/homeassistant/components/twinkly/const.py index d0d905a5752..2158e4aae07 100644 --- a/homeassistant/components/twinkly/const.py +++ b/homeassistant/components/twinkly/const.py @@ -23,11 +23,5 @@ DEV_PROFILE_RGBW = "RGBW" DATA_CLIENT = "client" DATA_DEVICE_INFO = "device_info" -HIDDEN_DEV_VALUES = ( - "code", # This is the internal status code of the API response - "copyright", # We should not display a copyright "LEDWORKS 2018" in the Home-Assistant UI - "mac", # Does not report the actual device mac address -) - # Minimum version required to support effects MIN_EFFECT_VERSION = "2.7.1" diff --git a/homeassistant/components/twinkly/light.py b/homeassistant/components/twinkly/light.py index 3174f60edf8..0145439493b 100644 --- a/homeassistant/components/twinkly/light.py +++ b/homeassistant/components/twinkly/light.py @@ -38,7 +38,6 @@ from .const import ( DEV_PROFILE_RGB, DEV_PROFILE_RGBW, DOMAIN, - HIDDEN_DEV_VALUES, MIN_EFFECT_VERSION, ) @@ -297,10 +296,6 @@ class TwinklyLight(LightEntity): }, ) - for key, value in device_info.items(): - if key not in HIDDEN_DEV_VALUES: - self._attributes[key] = value - if LightEntityFeature.EFFECT & self.supported_features: await self.async_update_movies() await self.async_update_current_movie() diff --git a/homeassistant/components/twinkly/translations/lv.json b/homeassistant/components/twinkly/translations/lv.json new file mode 100644 index 00000000000..4e8ba8e08d9 --- /dev/null +++ b/homeassistant/components/twinkly/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "device_exists": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/tr.json b/homeassistant/components/twinkly/translations/tr.json index 39bf9b32e55..d213f64998e 100644 --- a/homeassistant/components/twinkly/translations/tr.json +++ b/homeassistant/components/twinkly/translations/tr.json @@ -8,7 +8,7 @@ }, "step": { "discovery_confirm": { - "description": "{name} - {model} ( {host} ) kurulumunu yapmak istiyor musunuz?" + "description": "{name} - {model} ( {host} ) kurmak istiyor musunuz?" }, "user": { "data": { diff --git a/homeassistant/components/twinkly/translations/uk.json b/homeassistant/components/twinkly/translations/uk.json index 8f3d6d0cbaa..f4671f12652 100644 --- a/homeassistant/components/twinkly/translations/uk.json +++ b/homeassistant/components/twinkly/translations/uk.json @@ -7,6 +7,9 @@ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" }, "step": { + "discovery_confirm": { + "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name} - {model} ({host})?" + }, "user": { "data": { "host": "\u0406\u043c'\u044f \u0445\u043e\u0441\u0442\u0430 (\u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430) \u0412\u0430\u0448\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Twinkly" diff --git a/homeassistant/components/ukraine_alarm/translations/el.json b/homeassistant/components/ukraine_alarm/translations/el.json index 779c7dc1102..96bbcd8c41e 100644 --- a/homeassistant/components/ukraine_alarm/translations/el.json +++ b/homeassistant/components/ukraine_alarm/translations/el.json @@ -25,7 +25,7 @@ "data": { "region": "\u03a0\u03b5\u03c1\u03b9\u03bf\u03c7\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 \u03c3\u03c5\u03bd\u03b1\u03b3\u03b5\u03c1\u03bc\u03bf\u03cd \u03c4\u03b7\u03c2 \u039f\u03c5\u03ba\u03c1\u03b1\u03bd\u03af\u03b1\u03c2. \u0393\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 \u03bc\u03b5\u03c4\u03b1\u03b2\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 {api_url}" + "description": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7" } } } diff --git a/homeassistant/components/ukraine_alarm/translations/id.json b/homeassistant/components/ukraine_alarm/translations/id.json index 1751c132e9d..aa91f2950cb 100644 --- a/homeassistant/components/ukraine_alarm/translations/id.json +++ b/homeassistant/components/ukraine_alarm/translations/id.json @@ -5,7 +5,7 @@ "cannot_connect": "Gagal terhubung", "max_regions": "Maksimal 5 wilayah dapat dikonfigurasi", "rate_limit": "Terlalu banyak permintaan", - "timeout": "Tenggang waktu membuat koneksi habis", + "timeout": "Tenggang waktu pembuatan koneksi habis", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index caf256ded20..64ea7b39778 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -33,7 +33,6 @@ from .const import ( CONF_ALLOW_BANDWIDTH_SENSORS, CONF_ALLOW_UPTIME_SENSORS, CONF_BLOCK_CLIENT, - CONF_CONTROLLER, CONF_DETECTION_TIME, CONF_DPI_RESTRICTIONS, CONF_IGNORE_WIRED_BUG, @@ -152,8 +151,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): unique_id = user_input[CONF_SITE_ID] self.config[CONF_SITE_ID] = self.site_ids[unique_id] - # Backwards compatible config - self.config[CONF_CONTROLLER] = self.config.copy() config_entry = await self.async_set_unique_id(unique_id) abort_reason = "configuration_updated" diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index 85f744e481f..b5cea06c719 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -14,7 +14,6 @@ PLATFORMS = [ Platform.UPDATE, ] -CONF_CONTROLLER = "controller" CONF_SITE_ID = "site" UNIFI_WIRELESS_CLIENTS = "unifi_wireless_clients" diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 8aae95bda41..d26780ab019 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -9,6 +9,7 @@ from typing import Any from aiohttp import CookieJar import aiounifi +from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.messages import DATA_CLIENT_REMOVED, DATA_EVENT from aiounifi.models.event import EventKey from aiounifi.websocket import WebsocketSignal, WebsocketState @@ -31,6 +32,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import async_entries_for_config_entry from homeassistant.helpers.event import async_track_time_interval import homeassistant.util.dt as dt_util @@ -62,6 +64,7 @@ from .const import ( PLATFORMS, UNIFI_WIRELESS_CLIENTS, ) +from .entity import UnifiEntity, UnifiEntityDescription from .errors import AuthenticationRequired, CannotConnect RETRY_TIMER = 15 @@ -183,6 +186,44 @@ class UniFiController: return client.mac return None + @callback + def register_platform_add_entities( + self, + unifi_platform_entity: type[UnifiEntity], + descriptions: tuple[UnifiEntityDescription, ...], + async_add_entities: AddEntitiesCallback, + ) -> None: + """Subscribe to UniFi API handlers and create entities.""" + + @callback + def async_load_entities(description: UnifiEntityDescription) -> None: + """Load and subscribe to UniFi endpoints.""" + entities: list[UnifiEntity] = [] + api_handler = description.api_handler_fn(self.api) + + @callback + def async_create_entity(event: ItemEvent, obj_id: str) -> None: + """Create UniFi entity.""" + if not description.allowed_fn( + self, obj_id + ) or not description.supported_fn(self, obj_id): + return + + entity = unifi_platform_entity(obj_id, self, description) + if event == ItemEvent.ADDED: + async_add_entities([entity]) + return + entities.append(entity) + + for obj_id in api_handler: + async_create_entity(ItemEvent.CHANGED, obj_id) + async_add_entities(entities) + + api_handler.subscribe(async_create_entity, ItemEvent.ADDED) + + for description in descriptions: + async_load_entities(description) + @callback def async_unifi_signalling_callback(self, signal, data): """Handle messages back from UniFi library.""" @@ -443,12 +484,12 @@ async def get_unifi_controller( config: MappingProxyType[str, Any], ) -> aiounifi.Controller: """Create a controller object and verify authentication.""" - sslcontext = None + ssl_context = False if verify_ssl := bool(config.get(CONF_VERIFY_SSL)): session = aiohttp_client.async_get_clientsession(hass) if isinstance(verify_ssl, str): - sslcontext = ssl.create_default_context(cafile=verify_ssl) + ssl_context = ssl.create_default_context(cafile=verify_ssl) else: session = aiohttp_client.async_create_clientsession( hass, verify_ssl=verify_ssl, cookie_jar=CookieJar(unsafe=True) @@ -461,7 +502,7 @@ async def get_unifi_controller( port=config[CONF_PORT], site=config[CONF_SITE_ID], websession=session, - sslcontext=sslcontext, + ssl_context=ssl_context, ) try: diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index ea8db77e124..a7678445c8c 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -1,9 +1,18 @@ """Track both clients and devices using UniFi Network.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass from datetime import timedelta import logging +from typing import Generic, TypeVar +import aiounifi +from aiounifi.interfaces.api_handlers import ItemEvent +from aiounifi.interfaces.devices import Devices from aiounifi.models.api import SOURCE_DATA, SOURCE_EVENT +from aiounifi.models.device import Device from aiounifi.models.event import EventKey from homeassistant.components.device_tracker import DOMAIN, ScannerEntity, SourceType @@ -15,8 +24,8 @@ import homeassistant.util.dt as dt_util from .const import DOMAIN as UNIFI_DOMAIN from .controller import UniFiController +from .entity import UnifiEntity, UnifiEntityDescription from .unifi_client import UniFiClientBase -from .unifi_entity_base import UniFiBase LOGGER = logging.getLogger(__name__) @@ -52,10 +61,71 @@ WIRED_CONNECTION = (EventKey.WIRED_CLIENT_CONNECTED,) WIRELESS_CONNECTION = ( EventKey.WIRELESS_CLIENT_CONNECTED, EventKey.WIRELESS_CLIENT_ROAM, - EventKey.WIRELESS_CLIENT_ROAMRADIO, + EventKey.WIRELESS_CLIENT_ROAM_RADIO, EventKey.WIRELESS_GUEST_CONNECTED, EventKey.WIRELESS_GUEST_ROAM, - EventKey.WIRELESS_GUEST_ROAMRADIO, + EventKey.WIRELESS_GUEST_ROAM_RADIO, +) + + +_DataT = TypeVar("_DataT", bound=Device) +_HandlerT = TypeVar("_HandlerT", bound=Devices) + + +@callback +def async_device_available_fn(controller: UniFiController, obj_id: str) -> bool: + """Check if device object is disabled.""" + device = controller.api.devices[obj_id] + return controller.available and not device.disabled + + +@callback +def async_device_heartbeat_timedelta_fn( + controller: UniFiController, obj_id: str +) -> timedelta: + """Check if device object is disabled.""" + device = controller.api.devices[obj_id] + return timedelta(seconds=device.next_interval + 60) + + +@dataclass +class UnifiEntityTrackerDescriptionMixin(Generic[_HandlerT, _DataT]): + """Device tracker local functions.""" + + heartbeat_timedelta_fn: Callable[[UniFiController, str], timedelta] + ip_address_fn: Callable[[aiounifi.Controller, str], str] + is_connected_fn: Callable[[UniFiController, str], bool] + hostname_fn: Callable[[aiounifi.Controller, str], str | None] + + +@dataclass +class UnifiTrackerEntityDescription( + UnifiEntityDescription[_HandlerT, _DataT], + UnifiEntityTrackerDescriptionMixin[_HandlerT, _DataT], +): + """Class describing UniFi device tracker entity.""" + + +ENTITY_DESCRIPTIONS: tuple[UnifiTrackerEntityDescription, ...] = ( + UnifiTrackerEntityDescription[Devices, Device]( + key="Device scanner", + has_entity_name=True, + icon="mdi:ethernet", + allowed_fn=lambda controller, obj_id: controller.option_track_devices, + api_handler_fn=lambda api: api.devices, + available_fn=async_device_available_fn, + device_info_fn=lambda api, obj_id: None, + event_is_on=None, + event_to_subscribe=None, + heartbeat_timedelta_fn=async_device_heartbeat_timedelta_fn, + is_connected_fn=lambda ctrlr, obj_id: ctrlr.api.devices[obj_id].state == 1, + name_fn=lambda device: device.name or device.model, + object_fn=lambda api, obj_id: api.devices[obj_id], + supported_fn=lambda controller, obj_id: True, + unique_id_fn=lambda controller, obj_id: obj_id, + ip_address_fn=lambda api, obj_id: api.devices[obj_id].ip, + hostname_fn=lambda api, obj_id: None, + ), ) @@ -66,6 +136,10 @@ async def async_setup_entry( ) -> None: """Set up device tracker for UniFi Network integration.""" controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + controller.register_platform_add_entities( + UnifiScannerEntity, ENTITY_DESCRIPTIONS, async_add_entities + ) + controller.entities[DOMAIN] = {CLIENT_TRACKER: set(), DEVICE_TRACKER: set()} @callback @@ -76,9 +150,6 @@ async def async_setup_entry( if controller.option_track_clients: add_client_entities(controller, async_add_entities, clients) - if controller.option_track_devices: - add_device_entities(controller, async_add_entities, devices) - for signal in (controller.signal_update, controller.signal_options_update): config_entry.async_on_unload( async_dispatcher_connect(hass, signal, items_added) @@ -113,21 +184,6 @@ def add_client_entities(controller, async_add_entities, clients): async_add_entities(trackers) -@callback -def add_device_entities(controller, async_add_entities, devices): - """Add new device tracker entities from the controller.""" - trackers = [] - - for mac in devices: - if mac in controller.entities[DOMAIN][UniFiDeviceTracker.TYPE]: - continue - - device = controller.api.devices[mac] - trackers.append(UniFiDeviceTracker(device, controller)) - - async_add_entities(trackers) - - class UniFiClientTracker(UniFiClientBase, ScannerEntity): """Representation of a network client.""" @@ -313,131 +369,102 @@ class UniFiClientTracker(UniFiClientBase, ScannerEntity): await self.remove_item({self.client.mac}) -class UniFiDeviceTracker(UniFiBase, ScannerEntity): - """Representation of a network infrastructure device.""" +class UnifiScannerEntity(UnifiEntity[_HandlerT, _DataT], ScannerEntity): + """Representation of a UniFi scanner.""" - DOMAIN = DOMAIN - TYPE = DEVICE_TRACKER + entity_description: UnifiTrackerEntityDescription - def __init__(self, device, controller): - """Set up tracked device.""" - super().__init__(device, controller) - - self.device = self._item - self._is_connected = device.state == 1 - self._controller_connection_state_changed = False - self.schedule_update = False - - async def async_added_to_hass(self) -> None: - """Watch object when added.""" - self.async_on_remove( - async_dispatcher_connect( - self.hass, - f"{self.controller.signal_heartbeat_missed}_{self.unique_id}", - self._make_disconnected, - ) - ) - await super().async_added_to_hass() - - async def async_will_remove_from_hass(self) -> None: - """Disconnect object when removed.""" - self.controller.async_heartbeat(self.unique_id) - await super().async_will_remove_from_hass() + _ignore_events: bool + _is_connected: bool @callback - def async_signal_reachable_callback(self) -> None: - """Call when controller connection state change.""" - self._controller_connection_state_changed = True - super().async_signal_reachable_callback() + def async_initiate_state(self) -> None: + """Initiate entity state. - @callback - def async_update_callback(self) -> None: - """Update the devices' state.""" - - if self._controller_connection_state_changed: - self._controller_connection_state_changed = False - - if self.controller.available: - if self._is_connected: - self.schedule_update = True - - else: - self.controller.async_heartbeat(self.unique_id) - - elif self.device.last_updated == SOURCE_DATA: - self._is_connected = True - self.schedule_update = True - - if self.schedule_update: - self.schedule_update = False - self.controller.async_heartbeat( - self.unique_id, - dt_util.utcnow() + timedelta(seconds=self.device.next_interval + 60), - ) - - super().async_update_callback() - - @callback - def _make_disconnected(self, *_): - """No heart beat by device.""" - self._is_connected = False - self.async_write_ha_state() + Initiate is_connected. + """ + description = self.entity_description + self._ignore_events = False + self._is_connected = description.is_connected_fn(self.controller, self._obj_id) @property - def is_connected(self): + def is_connected(self) -> bool: """Return true if the device is connected to the network.""" return self._is_connected @property - def source_type(self) -> SourceType: - """Return the source type of the device.""" - return SourceType.ROUTER - - @property - def name(self) -> str: - """Return the name of the device.""" - return self.device.name or self.device.model - - @property - def unique_id(self) -> str: - """Return a unique identifier for this device.""" - return self.device.mac - - @property - def available(self) -> bool: - """Return if controller is available.""" - return not self.device.disabled and self.controller.available - - @property - def extra_state_attributes(self): - """Return the device state attributes.""" - if self.device.state == 0: - return {} - - attributes = {} - - if self.device.has_fan: - attributes["fan_level"] = self.device.fan_level - - if self.device.overheating: - attributes["overheating"] = self.device.overheating - - if self.device.upgradable: - attributes["upgradable"] = self.device.upgradable - - return attributes + def hostname(self) -> str | None: + """Return hostname of the device.""" + return self.entity_description.hostname_fn(self.controller.api, self._obj_id) @property def ip_address(self) -> str: """Return the primary ip address of the device.""" - return self.device.ip + return self.entity_description.ip_address_fn(self.controller.api, self._obj_id) @property def mac_address(self) -> str: """Return the mac address of the device.""" - return self.device.mac + return self._obj_id - async def options_updated(self) -> None: - """Config entry options are updated, remove entity if option is disabled.""" - if not self.controller.option_track_devices: - await self.remove_item({self.device.mac}) + @property + def source_type(self) -> SourceType: + """Return the source type, eg gps or router, of the device.""" + return SourceType.ROUTER + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._attr_unique_id + + @callback + def _make_disconnected(self, *_) -> None: + """No heart beat by device.""" + self._is_connected = False + self.async_write_ha_state() + + @callback + def async_update_state(self, event: ItemEvent, obj_id: str) -> None: + """Update entity state. + + Remove heartbeat check if controller state has changed + and entity is unavailable. + Update is_connected. + Schedule new heartbeat check if connected. + """ + description = self.entity_description + + if event == ItemEvent.CHANGED: + # Prioritize normal data updates over events + self._ignore_events = True + + elif event == ItemEvent.ADDED and not self.available: + # From unifi.entity.async_signal_reachable_callback + # Controller connection state has changed and entity is unavailable + # Cancel heartbeat + self.controller.async_heartbeat(self.unique_id) + return + + if is_connected := description.is_connected_fn(self.controller, self._obj_id): + self._is_connected = is_connected + self.controller.async_heartbeat( + self.unique_id, + dt_util.utcnow() + + description.heartbeat_timedelta_fn(self.controller, self._obj_id), + ) + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + await super().async_added_to_hass() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{self.controller.signal_heartbeat_missed}_{self._obj_id}", + self._make_disconnected, + ) + ) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect object when removed.""" + await super().async_will_remove_from_hass() + self.controller.async_heartbeat(self.unique_id) diff --git a/homeassistant/components/unifi/diagnostics.py b/homeassistant/components/unifi/diagnostics.py index 495613f3b81..3c72c06d6f2 100644 --- a/homeassistant/components/unifi/diagnostics.py +++ b/homeassistant/components/unifi/diagnostics.py @@ -11,11 +11,11 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import format_mac -from .const import CONF_CONTROLLER, DOMAIN as UNIFI_DOMAIN +from .const import DOMAIN as UNIFI_DOMAIN from .controller import UniFiController -TO_REDACT = {CONF_CONTROLLER, CONF_PASSWORD} -REDACT_CONFIG = {CONF_CONTROLLER, CONF_HOST, CONF_PASSWORD, CONF_USERNAME} +TO_REDACT = {CONF_PASSWORD} +REDACT_CONFIG = {CONF_HOST, CONF_PASSWORD, CONF_USERNAME} REDACT_CLIENTS = {"bssid", "essid"} REDACT_DEVICES = { "anon_id", diff --git a/homeassistant/components/unifi/entity.py b/homeassistant/components/unifi/entity.py index 0809e81fe54..783950310e4 100644 --- a/homeassistant/components/unifi/entity.py +++ b/homeassistant/components/unifi/entity.py @@ -4,26 +4,65 @@ from __future__ import annotations from abc import abstractmethod from collections.abc import Callable from dataclasses import dataclass -from typing import Generic, TypeVar +from typing import TYPE_CHECKING, Generic, TypeVar import aiounifi -from aiounifi.interfaces.api_handlers import CallbackType, ItemEvent, UnsubscribeType -from aiounifi.interfaces.devices import Devices -from aiounifi.models.device import Device -from aiounifi.models.event import EventKey +from aiounifi.interfaces.api_handlers import ( + APIHandler, + CallbackType, + ItemEvent, + UnsubscribeType, +) +from aiounifi.interfaces.outlets import Outlets +from aiounifi.interfaces.ports import Ports +from aiounifi.models.api import APIItem +from aiounifi.models.event import Event, EventKey +from aiounifi.models.outlet import Outlet +from aiounifi.models.port import Port from homeassistant.core import callback from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription -from .controller import UniFiController +from .const import ATTR_MANUFACTURER -DataT = TypeVar("DataT", bound=Device) -HandlerT = TypeVar("HandlerT", bound=Devices) +if TYPE_CHECKING: + from .controller import UniFiController + +DataT = TypeVar("DataT", bound=APIItem | Outlet | Port) +HandlerT = TypeVar("HandlerT", bound=APIHandler | Outlets | Ports) SubscriptionT = Callable[[CallbackType, ItemEvent], UnsubscribeType] +@callback +def async_device_available_fn(controller: UniFiController, obj_id: str) -> bool: + """Check if device is available.""" + if "_" in obj_id: # Sub device (outlet or port) + obj_id = obj_id.partition("_")[0] + + device = controller.api.devices[obj_id] + return controller.available and not device.disabled + + +@callback +def async_device_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: + """Create device registry entry for device.""" + if "_" in obj_id: # Sub device (outlet or port) + obj_id = obj_id.partition("_")[0] + + device = api.devices[obj_id] + return DeviceInfo( + connections={(CONNECTION_NETWORK_MAC, device.mac)}, + manufacturer=ATTR_MANUFACTURER, + model=device.model, + name=device.name or None, + sw_version=device.version, + hw_version=str(device.board_revision), + ) + + @dataclass class UnifiDescription(Generic[HandlerT, DataT]): """Validate and load entities from different UniFi handlers.""" @@ -65,7 +104,6 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]): self.entity_description = description self._removed = False - self._write_state = False self._attr_available = description.available_fn(controller, obj_id) self._attr_device_info = description.device_info_fn(controller.api, obj_id) @@ -106,6 +144,15 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]): ) ) + # Subscribe to events if defined + if description.event_to_subscribe is not None: + self.async_on_remove( + self.controller.api.events.subscribe( + self.async_event_callback, + description.event_to_subscribe, + ) + ) + @callback def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None: """Update the entity state.""" @@ -118,14 +165,9 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]): self.hass.async_create_task(self.remove_item({self._obj_id})) return - if ( - available := description.available_fn(self.controller, self._obj_id) - ) != self.available: - self._attr_available = available - self._write_state = True + self._attr_available = description.available_fn(self.controller, self._obj_id) self.async_update_state(event, obj_id) - if self._write_state: - self.async_write_ha_state() + self.async_write_ha_state() @callback def async_signal_reachable_callback(self) -> None: @@ -148,12 +190,13 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]): await self.async_remove(force_remove=True) @callback - @abstractmethod def async_initiate_state(self) -> None: """Initiate entity state. Perform additional actions setting up platform entity child class state. + Defaults to using async_update_state to set initial state. """ + self.async_update_state(ItemEvent.ADDED, self._obj_id) @callback @abstractmethod @@ -162,3 +205,11 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]): Perform additional actions updating platform entity child class state. """ + + @callback + def async_event_callback(self, event: Event) -> None: + """Update entity state based on subscribed event. + + Perform additional action updating platform entity child class state. + """ + raise NotImplementedError() diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 34bba257a84..fb12585efaa 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "UniFi Network", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==43"], + "requirements": ["aiounifi==44"], "codeowners": ["@Kane610"], "quality_scale": "platinum", "ssdp": [ diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index e017ff4ed0e..a88b750fdf7 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -8,12 +8,14 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from datetime import datetime, timedelta -from typing import Generic, TypeVar +from typing import Generic import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.clients import Clients +from aiounifi.interfaces.ports import Ports from aiounifi.models.client import Client +from aiounifi.models.port import Port from homeassistant.components.sensor import ( SensorDeviceClass, @@ -21,25 +23,28 @@ from homeassistant.components.sensor import ( SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import UnitOfInformation +from homeassistant.const import UnitOfInformation, UnitOfPower from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo, EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.dt as dt_util from .const import DOMAIN as UNIFI_DOMAIN from .controller import UniFiController - -_DataT = TypeVar("_DataT", bound=Client) -_HandlerT = TypeVar("_HandlerT", bound=Clients) +from .entity import ( + DataT, + HandlerT, + UnifiEntity, + UnifiEntityDescription, + async_device_available_fn, + async_device_device_info_fn, +) @callback def async_client_rx_value_fn(controller: UniFiController, client: Client) -> float: - """Calculate if all apps are enabled.""" + """Calculate receiving data transfer value.""" if client.mac not in controller.wireless_clients: return client.wired_rx_bytes_r / 1000000 return client.rx_bytes_r / 1000000 @@ -47,7 +52,7 @@ def async_client_rx_value_fn(controller: UniFiController, client: Client) -> flo @callback def async_client_tx_value_fn(controller: UniFiController, client: Client) -> float: - """Calculate if all apps are enabled.""" + """Calculate transmission data transfer value.""" if client.mac not in controller.wireless_clients: return client.wired_tx_bytes_r / 1000000 return client.tx_bytes_r / 1000000 @@ -75,29 +80,23 @@ def async_client_device_info_fn(api: aiounifi.Controller, obj_id: str) -> Device @dataclass -class UnifiEntityLoader(Generic[_HandlerT, _DataT]): +class UnifiSensorEntityDescriptionMixin(Generic[HandlerT, DataT]): """Validate and load entities from different UniFi handlers.""" - allowed_fn: Callable[[UniFiController, str], bool] - api_handler_fn: Callable[[aiounifi.Controller], _HandlerT] - available_fn: Callable[[UniFiController, str], bool] - device_info_fn: Callable[[aiounifi.Controller, str], DeviceInfo] - name_fn: Callable[[_DataT], str | None] - object_fn: Callable[[aiounifi.Controller, str], _DataT] - supported_fn: Callable[[UniFiController, str], bool | None] - unique_id_fn: Callable[[str], str] - value_fn: Callable[[UniFiController, _DataT], datetime | float] + value_fn: Callable[[UniFiController, DataT], datetime | float | str | None] @dataclass -class UnifiEntityDescription( - SensorEntityDescription, UnifiEntityLoader[_HandlerT, _DataT] +class UnifiSensorEntityDescription( + SensorEntityDescription, + UnifiEntityDescription[HandlerT, DataT], + UnifiSensorEntityDescriptionMixin[HandlerT, DataT], ): """Class describing UniFi sensor entity.""" -ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( - UnifiEntityDescription[Clients, Client]( +ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = ( + UnifiSensorEntityDescription[Clients, Client]( key="Bandwidth sensor RX", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, @@ -106,13 +105,15 @@ ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( api_handler_fn=lambda api: api.clients, available_fn=lambda controller, _: controller.available, device_info_fn=async_client_device_info_fn, + event_is_on=None, + event_to_subscribe=None, name_fn=lambda _: "RX", object_fn=lambda api, obj_id: api.clients[obj_id], supported_fn=lambda controller, _: controller.option_allow_bandwidth_sensors, - unique_id_fn=lambda obj_id: f"rx-{obj_id}", + unique_id_fn=lambda controller, obj_id: f"rx-{obj_id}", value_fn=async_client_rx_value_fn, ), - UnifiEntityDescription[Clients, Client]( + UnifiSensorEntityDescription[Clients, Client]( key="Bandwidth sensor TX", entity_category=EntityCategory.DIAGNOSTIC, native_unit_of_measurement=UnitOfInformation.MEGABYTES, @@ -121,13 +122,34 @@ ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( api_handler_fn=lambda api: api.clients, available_fn=lambda controller, _: controller.available, device_info_fn=async_client_device_info_fn, + event_is_on=None, + event_to_subscribe=None, name_fn=lambda _: "TX", object_fn=lambda api, obj_id: api.clients[obj_id], supported_fn=lambda controller, _: controller.option_allow_bandwidth_sensors, - unique_id_fn=lambda obj_id: f"tx-{obj_id}", + unique_id_fn=lambda controller, obj_id: f"tx-{obj_id}", value_fn=async_client_tx_value_fn, ), - UnifiEntityDescription[Clients, Client]( + UnifiSensorEntityDescription[Ports, Port]( + key="PoE port power sensor", + device_class=SensorDeviceClass.POWER, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfPower.WATT, + has_entity_name=True, + entity_registry_enabled_default=False, + allowed_fn=lambda controller, obj_id: True, + api_handler_fn=lambda api: api.ports, + available_fn=async_device_available_fn, + device_info_fn=async_device_device_info_fn, + event_is_on=None, + event_to_subscribe=None, + name_fn=lambda port: f"{port.name} PoE Power", + object_fn=lambda api, obj_id: api.ports[obj_id], + supported_fn=lambda controller, obj_id: controller.api.ports[obj_id].port_poe, + unique_id_fn=lambda controller, obj_id: f"poe_power-{obj_id}", + value_fn=lambda _, obj: obj.poe_power if obj.poe_mode != "off" else "0", + ), + UnifiSensorEntityDescription[Clients, Client]( key="Uptime sensor", device_class=SensorDeviceClass.TIMESTAMP, entity_category=EntityCategory.DIAGNOSTIC, @@ -136,10 +158,12 @@ ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( api_handler_fn=lambda api: api.clients, available_fn=lambda controller, obj_id: controller.available, device_info_fn=async_client_device_info_fn, + event_is_on=None, + event_to_subscribe=None, name_fn=lambda client: "Uptime", object_fn=lambda api, obj_id: api.clients[obj_id], supported_fn=lambda controller, _: controller.option_allow_uptime_sensors, - unique_id_fn=lambda obj_id: f"uptime-{obj_id}", + unique_id_fn=lambda controller, obj_id: f"uptime-{obj_id}", value_fn=async_client_uptime_value_fn, ), ) @@ -152,138 +176,23 @@ async def async_setup_entry( ) -> None: """Set up sensors for UniFi Network integration.""" controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + controller.register_platform_add_entities( + UnifiSensorEntity, ENTITY_DESCRIPTIONS, async_add_entities + ) + + +class UnifiSensorEntity(UnifiEntity[HandlerT, DataT], SensorEntity): + """Base representation of a UniFi sensor.""" + + entity_description: UnifiSensorEntityDescription[HandlerT, DataT] @callback - def async_load_entities(description: UnifiEntityDescription) -> None: - """Load and subscribe to UniFi devices.""" - entities: list[SensorEntity] = [] - api_handler = description.api_handler_fn(controller.api) + def async_update_state(self, event: ItemEvent, obj_id: str) -> None: + """Update entity state. - @callback - def async_create_entity(event: ItemEvent, obj_id: str) -> None: - """Create UniFi entity.""" - if not description.allowed_fn( - controller, obj_id - ) or not description.supported_fn(controller, obj_id): - return - - entity = UnifiSensorEntity(obj_id, controller, description) - if event == ItemEvent.ADDED: - async_add_entities([entity]) - return - entities.append(entity) - - for obj_id in api_handler: - async_create_entity(ItemEvent.CHANGED, obj_id) - async_add_entities(entities) - - api_handler.subscribe(async_create_entity, ItemEvent.ADDED) - - for description in ENTITY_DESCRIPTIONS: - async_load_entities(description) - - -class UnifiSensorEntity(SensorEntity, Generic[_HandlerT, _DataT]): - """Base representation of a UniFi switch.""" - - entity_description: UnifiEntityDescription[_HandlerT, _DataT] - _attr_should_poll = False - - def __init__( - self, - obj_id: str, - controller: UniFiController, - description: UnifiEntityDescription[_HandlerT, _DataT], - ) -> None: - """Set up UniFi switch entity.""" - self._obj_id = obj_id - self.controller = controller - self.entity_description = description - - self._removed = False - - self._attr_available = description.available_fn(controller, obj_id) - self._attr_device_info = description.device_info_fn(controller.api, obj_id) - self._attr_unique_id = description.unique_id_fn(obj_id) - - obj = description.object_fn(controller.api, obj_id) - self._attr_native_value = description.value_fn(controller, obj) - self._attr_name = description.name_fn(obj) - - async def async_added_to_hass(self) -> None: - """Register callbacks.""" + Update native_value. + """ description = self.entity_description - handler = description.api_handler_fn(self.controller.api) - self.async_on_remove( - handler.subscribe( - self.async_signalling_callback, - id_filter=self._obj_id, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_reachable, - self.async_signal_reachable_callback, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_options_update, - self.options_updated, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_remove, - self.remove_item, - ) - ) - - @callback - def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None: - """Update the switch state.""" - if event == ItemEvent.DELETED and obj_id == self._obj_id: - self.hass.async_create_task(self.remove_item({self._obj_id})) - return - - description = self.entity_description - if not description.supported_fn(self.controller, self._obj_id): - self.hass.async_create_task(self.remove_item({self._obj_id})) - return - - update_state = False - obj = description.object_fn(self.controller.api, self._obj_id) if (value := description.value_fn(self.controller, obj)) != self.native_value: self._attr_native_value = value - update_state = True - if ( - available := description.available_fn(self.controller, self._obj_id) - ) != self.available: - self._attr_available = available - update_state = True - if update_state: - self.async_write_ha_state() - - @callback - def async_signal_reachable_callback(self) -> None: - """Call when controller connection state change.""" - self.async_signalling_callback(ItemEvent.ADDED, self._obj_id) - - async def options_updated(self) -> None: - """Config entry options are updated, remove entity if option is disabled.""" - if not self.entity_description.allowed_fn(self.controller, self._obj_id): - await self.remove_item({self._obj_id}) - - async def remove_item(self, keys: set) -> None: - """Remove entity if object ID is part of set.""" - if self._obj_id not in keys or self._removed: - return - self._removed = True - if self.registry_entry: - er.async_get(self.hass).async_remove(self.entity_id) - else: - await self.async_remove(force_remove=True) diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index b0f042c78cd..8186c708698 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -9,20 +9,14 @@ from __future__ import annotations import asyncio from collections.abc import Callable, Coroutine from dataclasses import dataclass -from typing import Any, Generic, TypeVar, Union +from typing import Any, Generic import aiounifi -from aiounifi.interfaces.api_handlers import ( - APIHandler, - CallbackType, - ItemEvent, - UnsubscribeType, -) +from aiounifi.interfaces.api_handlers import ItemEvent from aiounifi.interfaces.clients import Clients from aiounifi.interfaces.dpi_restriction_groups import DPIRestrictionGroups from aiounifi.interfaces.outlets import Outlets from aiounifi.interfaces.ports import Ports -from aiounifi.models.api import APIItem from aiounifi.models.client import Client, ClientBlockRequest from aiounifi.models.device import ( DeviceSetOutletRelayRequest, @@ -42,32 +36,35 @@ from homeassistant.components.switch import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.device_registry import ( CONNECTION_NETWORK_MAC, DeviceEntryType, ) -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 ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN from .controller import UniFiController +from .entity import ( + DataT, + HandlerT, + SubscriptionT, + UnifiEntity, + UnifiEntityDescription, + async_device_available_fn, + async_device_device_info_fn, +) CLIENT_BLOCKED = (EventKey.WIRED_CLIENT_BLOCKED, EventKey.WIRELESS_CLIENT_BLOCKED) CLIENT_UNBLOCKED = (EventKey.WIRED_CLIENT_UNBLOCKED, EventKey.WIRELESS_CLIENT_UNBLOCKED) -_DataT = TypeVar("_DataT", bound=Union[APIItem, Outlet, Port]) -_HandlerT = TypeVar("_HandlerT", bound=Union[APIHandler, Outlets, Ports]) - -Subscription = Callable[[CallbackType, ItemEvent], UnsubscribeType] - @callback def async_dpi_group_is_on_fn( - api: aiounifi.Controller, dpi_group: DPIRestrictionGroup + controller: UniFiController, dpi_group: DPIRestrictionGroup ) -> bool: """Calculate if all apps are enabled.""" + api = controller.api return all( api.dpi_apps[app_id].enabled for app_id in dpi_group.dpiapp_ids or [] @@ -75,14 +72,6 @@ def async_dpi_group_is_on_fn( ) -@callback -def async_sub_device_available_fn(controller: UniFiController, obj_id: str) -> bool: - """Check if sub device object is disabled.""" - device_id = obj_id.partition("_")[0] - device = controller.api.devices[device_id] - return controller.available and not device.disabled - - @callback def async_client_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: """Create device registry entry for client.""" @@ -94,23 +83,6 @@ def async_client_device_info_fn(api: aiounifi.Controller, obj_id: str) -> Device ) -@callback -def async_device_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: - """Create device registry entry for device.""" - if "_" in obj_id: # Sub device - obj_id = obj_id.partition("_")[0] - - device = api.devices[obj_id] - return DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, device.mac)}, - manufacturer=ATTR_MANUFACTURER, - model=device.model, - name=device.name or None, - sw_version=device.version, - hw_version=str(device.board_revision), - ) - - @callback def async_dpi_group_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: """Create device registry entry for DPI group.""" @@ -163,35 +135,27 @@ async def async_poe_port_control_fn( @dataclass -class UnifiEntityLoader(Generic[_HandlerT, _DataT]): +class UnifiSwitchEntityDescriptionMixin(Generic[HandlerT, DataT]): """Validate and load entities from different UniFi handlers.""" - allowed_fn: Callable[[UniFiController, str], bool] - api_handler_fn: Callable[[aiounifi.Controller], _HandlerT] - available_fn: Callable[[UniFiController, str], bool] control_fn: Callable[[aiounifi.Controller, str, bool], Coroutine[Any, Any, None]] - device_info_fn: Callable[[aiounifi.Controller, str], DeviceInfo] - event_is_on: tuple[EventKey, ...] | None - event_to_subscribe: tuple[EventKey, ...] | None - is_on_fn: Callable[[aiounifi.Controller, _DataT], bool] - name_fn: Callable[[_DataT], str | None] - object_fn: Callable[[aiounifi.Controller, str], _DataT] - supported_fn: Callable[[aiounifi.Controller, str], bool | None] - unique_id_fn: Callable[[str], str] + is_on_fn: Callable[[UniFiController, DataT], bool] @dataclass -class UnifiEntityDescription( - SwitchEntityDescription, UnifiEntityLoader[_HandlerT, _DataT] +class UnifiSwitchEntityDescription( + SwitchEntityDescription, + UnifiEntityDescription[HandlerT, DataT], + UnifiSwitchEntityDescriptionMixin[HandlerT, DataT], ): """Class describing UniFi switch entity.""" - custom_subscribe: Callable[[aiounifi.Controller], Subscription] | None = None + custom_subscribe: Callable[[aiounifi.Controller], SubscriptionT] | None = None only_event_for_state_change: bool = False -ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( - UnifiEntityDescription[Clients, Client]( +ENTITY_DESCRIPTIONS: tuple[UnifiSwitchEntityDescription, ...] = ( + UnifiSwitchEntityDescription[Clients, Client]( key="Block client", device_class=SwitchDeviceClass.SWITCH, entity_category=EntityCategory.CONFIG, @@ -204,14 +168,14 @@ ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( device_info_fn=async_client_device_info_fn, event_is_on=CLIENT_UNBLOCKED, event_to_subscribe=CLIENT_BLOCKED + CLIENT_UNBLOCKED, - is_on_fn=lambda api, client: not client.blocked, + is_on_fn=lambda controller, client: not client.blocked, name_fn=lambda client: None, object_fn=lambda api, obj_id: api.clients[obj_id], only_event_for_state_change=True, - supported_fn=lambda api, obj_id: True, - unique_id_fn=lambda obj_id: f"block-{obj_id}", + supported_fn=lambda controller, obj_id: True, + unique_id_fn=lambda controller, obj_id: f"block-{obj_id}", ), - UnifiEntityDescription[DPIRestrictionGroups, DPIRestrictionGroup]( + UnifiSwitchEntityDescription[DPIRestrictionGroups, DPIRestrictionGroup]( key="DPI restriction", entity_category=EntityCategory.CONFIG, icon="mdi:network", @@ -226,27 +190,27 @@ ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( is_on_fn=async_dpi_group_is_on_fn, name_fn=lambda group: group.name, object_fn=lambda api, obj_id: api.dpi_groups[obj_id], - supported_fn=lambda api, obj_id: bool(api.dpi_groups[obj_id].dpiapp_ids), - unique_id_fn=lambda obj_id: obj_id, + supported_fn=lambda c, obj_id: bool(c.api.dpi_groups[obj_id].dpiapp_ids), + unique_id_fn=lambda controller, obj_id: obj_id, ), - UnifiEntityDescription[Outlets, Outlet]( + UnifiSwitchEntityDescription[Outlets, Outlet]( key="Outlet control", device_class=SwitchDeviceClass.OUTLET, has_entity_name=True, allowed_fn=lambda controller, obj_id: True, api_handler_fn=lambda api: api.outlets, - available_fn=async_sub_device_available_fn, + available_fn=async_device_available_fn, control_fn=async_outlet_control_fn, device_info_fn=async_device_device_info_fn, event_is_on=None, event_to_subscribe=None, - is_on_fn=lambda api, outlet: outlet.relay_state, + is_on_fn=lambda controller, outlet: outlet.relay_state, name_fn=lambda outlet: outlet.name, object_fn=lambda api, obj_id: api.outlets[obj_id], - supported_fn=lambda api, obj_id: api.outlets[obj_id].has_relay, - unique_id_fn=lambda obj_id: f"{obj_id.split('_', 1)[0]}-outlet-{obj_id.split('_', 1)[1]}", + supported_fn=lambda c, obj_id: c.api.outlets[obj_id].has_relay, + unique_id_fn=lambda controller, obj_id: f"{obj_id.split('_', 1)[0]}-outlet-{obj_id.split('_', 1)[1]}", ), - UnifiEntityDescription[Ports, Port]( + UnifiSwitchEntityDescription[Ports, Port]( key="PoE port control", device_class=SwitchDeviceClass.OUTLET, entity_category=EntityCategory.CONFIG, @@ -255,16 +219,16 @@ ENTITY_DESCRIPTIONS: tuple[UnifiEntityDescription, ...] = ( icon="mdi:ethernet", allowed_fn=lambda controller, obj_id: True, api_handler_fn=lambda api: api.ports, - available_fn=async_sub_device_available_fn, + available_fn=async_device_available_fn, control_fn=async_poe_port_control_fn, device_info_fn=async_device_device_info_fn, event_is_on=None, event_to_subscribe=None, - is_on_fn=lambda api, port: port.poe_mode != "off", + is_on_fn=lambda controller, port: port.poe_mode != "off", name_fn=lambda port: f"{port.name} PoE", object_fn=lambda api, obj_id: api.ports[obj_id], - supported_fn=lambda api, obj_id: api.ports[obj_id].port_poe, - unique_id_fn=lambda obj_id: f"{obj_id.split('_', 1)[0]}-poe-{obj_id.split('_', 1)[1]}", + supported_fn=lambda controller, obj_id: controller.api.ports[obj_id].port_poe, + unique_id_fn=lambda controller, obj_id: f"{obj_id.split('_', 1)[0]}-poe-{obj_id.split('_', 1)[1]}", ), ) @@ -285,62 +249,24 @@ async def async_setup_entry( client = controller.api.clients_all[mac] controller.api.clients.process_raw([client.raw]) - @callback - def async_load_entities(description: UnifiEntityDescription) -> None: - """Load and subscribe to UniFi devices.""" - entities: list[SwitchEntity] = [] - api_handler = description.api_handler_fn(controller.api) - - @callback - def async_create_entity(event: ItemEvent, obj_id: str) -> None: - """Create UniFi entity.""" - if not description.allowed_fn( - controller, obj_id - ) or not description.supported_fn(controller.api, obj_id): - return - - entity = UnifiSwitchEntity(obj_id, controller, description) - if event == ItemEvent.ADDED: - async_add_entities([entity]) - return - entities.append(entity) - - for obj_id in api_handler: - async_create_entity(ItemEvent.CHANGED, obj_id) - async_add_entities(entities) - - api_handler.subscribe(async_create_entity, ItemEvent.ADDED) - - for description in ENTITY_DESCRIPTIONS: - async_load_entities(description) + controller.register_platform_add_entities( + UnifiSwitchEntity, ENTITY_DESCRIPTIONS, async_add_entities + ) -class UnifiSwitchEntity(SwitchEntity, Generic[_HandlerT, _DataT]): +class UnifiSwitchEntity(UnifiEntity[HandlerT, DataT], SwitchEntity): """Base representation of a UniFi switch.""" - entity_description: UnifiEntityDescription[_HandlerT, _DataT] - _attr_should_poll = False + entity_description: UnifiSwitchEntityDescription[HandlerT, DataT] + only_event_for_state_change = False - def __init__( - self, - obj_id: str, - controller: UniFiController, - description: UnifiEntityDescription[_HandlerT, _DataT], - ) -> None: - """Set up UniFi switch entity.""" - self._obj_id = obj_id - self.controller = controller - self.entity_description = description - - self._removed = False - - self._attr_available = description.available_fn(controller, obj_id) - self._attr_device_info = description.device_info_fn(controller.api, obj_id) - self._attr_unique_id = description.unique_id_fn(obj_id) - - obj = description.object_fn(self.controller.api, obj_id) - self._attr_is_on = description.is_on_fn(controller.api, obj) - self._attr_name = description.name_fn(obj) + @callback + def async_initiate_state(self) -> None: + """Initiate entity state.""" + self.async_update_state(ItemEvent.ADDED, self._obj_id) + self.only_event_for_state_change = ( + self.entity_description.only_event_for_state_change + ) async def async_turn_on(self, **kwargs: Any) -> None: """Turn on switch.""" @@ -354,82 +280,19 @@ class UnifiSwitchEntity(SwitchEntity, Generic[_HandlerT, _DataT]): self.controller.api, self._obj_id, False ) - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - description = self.entity_description - handler = description.api_handler_fn(self.controller.api) - self.async_on_remove( - handler.subscribe( - self.async_signalling_callback, - id_filter=self._obj_id, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_reachable, - self.async_signal_reachable_callback, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_options_update, - self.options_updated, - ) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, - self.controller.signal_remove, - self.remove_item, - ) - ) - if description.event_to_subscribe is not None: - self.async_on_remove( - self.controller.api.events.subscribe( - self.async_event_callback, - description.event_to_subscribe, - ) - ) - if description.custom_subscribe is not None: - self.async_on_remove( - description.custom_subscribe(self.controller.api)( - self.async_signalling_callback, ItemEvent.CHANGED - ), - ) - @callback - def async_signalling_callback(self, event: ItemEvent, obj_id: str) -> None: - """Update the switch state.""" - if event == ItemEvent.DELETED and obj_id == self._obj_id: - self.hass.async_create_task(self.remove_item({self._obj_id})) + def async_update_state(self, event: ItemEvent, obj_id: str) -> None: + """Update entity state. + + Update attr_is_on. + """ + if self.only_event_for_state_change: return description = self.entity_description - if not description.supported_fn(self.controller.api, self._obj_id): - self.hass.async_create_task(self.remove_item({self._obj_id})) - return - - update_state = False - - if not description.only_event_for_state_change: - obj = description.object_fn(self.controller.api, self._obj_id) - if (is_on := description.is_on_fn(self.controller.api, obj)) != self.is_on: - self._attr_is_on = is_on - update_state = True - if ( - available := description.available_fn(self.controller, self._obj_id) - ) != self.available: - self._attr_available = available - update_state = True - if update_state: - self.async_write_ha_state() - - @callback - def async_signal_reachable_callback(self) -> None: - """Call when controller connection state change.""" - self.async_signalling_callback(ItemEvent.ADDED, self._obj_id) + obj = description.object_fn(self.controller.api, self._obj_id) + if (is_on := description.is_on_fn(self.controller, obj)) != self.is_on: + self._attr_is_on = is_on @callback def async_event_callback(self, event: Event) -> None: @@ -446,17 +309,13 @@ class UnifiSwitchEntity(SwitchEntity, Generic[_HandlerT, _DataT]): self._attr_available = description.available_fn(self.controller, self._obj_id) self.async_write_ha_state() - async def options_updated(self) -> None: - """Config entry options are updated, remove entity if option is disabled.""" - if not self.entity_description.allowed_fn(self.controller, self._obj_id): - await self.remove_item({self._obj_id}) + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + await super().async_added_to_hass() - async def remove_item(self, keys: set) -> None: - """Remove entity if object ID is part of set.""" - if self._obj_id not in keys or self._removed: - return - self._removed = True - if self.registry_entry: - er.async_get(self.hass).async_remove(self.entity_id) - else: - await self.async_remove(force_remove=True) + if self.entity_description.custom_subscribe is not None: + self.async_on_remove( + self.entity_description.custom_subscribe(self.controller.api)( + self.async_signalling_callback, ItemEvent.CHANGED + ), + ) diff --git a/homeassistant/components/unifi/translations/el.json b/homeassistant/components/unifi/translations/el.json index 6c150fe8133..71cd13a7944 100644 --- a/homeassistant/components/unifi/translations/el.json +++ b/homeassistant/components/unifi/translations/el.json @@ -2,7 +2,7 @@ "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": { @@ -27,7 +27,7 @@ }, "options": { "abort": { - "integration_not_setup": "\u0397 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 UniFi \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" + "integration_not_setup": "\u0397 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 UniFi \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af" }, "step": { "client_control": { diff --git a/homeassistant/components/unifi/translations/lt.json b/homeassistant/components/unifi/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/unifi/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/tr.json b/homeassistant/components/unifi/translations/tr.json index 9e9ff5034d8..d5ac804d655 100644 --- a/homeassistant/components/unifi/translations/tr.json +++ b/homeassistant/components/unifi/translations/tr.json @@ -27,7 +27,7 @@ }, "options": { "abort": { - "integration_not_setup": "UniFi entegrasyonu kurulmad\u0131" + "integration_not_setup": "UniFi entegrasyonu kurulmam\u0131\u015f" }, "step": { "client_control": { diff --git a/homeassistant/components/unifi/update.py b/homeassistant/components/unifi/update.py index 0810cbb780c..ea02b144a2f 100644 --- a/homeassistant/components/unifi/update.py +++ b/homeassistant/components/unifi/update.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass import logging -from typing import TYPE_CHECKING, Any, Generic +from typing import TYPE_CHECKING, Any, Generic, TypeVar import aiounifi from aiounifi.interfaces.api_handlers import ItemEvent @@ -19,24 +19,23 @@ from homeassistant.components.update import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN -from .entity import DataT, HandlerT, UnifiEntity, UnifiEntityDescription +from .const import DOMAIN as UNIFI_DOMAIN +from .entity import ( + UnifiEntity, + UnifiEntityDescription, + async_device_available_fn, + async_device_device_info_fn, +) if TYPE_CHECKING: from .controller import UniFiController LOGGER = logging.getLogger(__name__) - -@callback -def async_device_available_fn(controller: UniFiController, obj_id: str) -> bool: - """Check if device is available.""" - device = controller.api.devices[obj_id] - return controller.available and not device.disabled +_DataT = TypeVar("_DataT", bound=Device) +_HandlerT = TypeVar("_HandlerT", bound=Devices) async def async_device_control_fn(api: aiounifi.Controller, obj_id: str) -> None: @@ -44,33 +43,19 @@ async def async_device_control_fn(api: aiounifi.Controller, obj_id: str) -> None await api.request(DeviceUpgradeRequest.create(obj_id)) -@callback -def async_device_device_info_fn(api: aiounifi.Controller, obj_id: str) -> DeviceInfo: - """Create device registry entry for device.""" - device = api.devices[obj_id] - return DeviceInfo( - connections={(CONNECTION_NETWORK_MAC, device.mac)}, - manufacturer=ATTR_MANUFACTURER, - model=device.model, - name=device.name or None, - sw_version=device.version, - hw_version=str(device.board_revision), - ) - - @dataclass -class UnifiEntityLoader(Generic[HandlerT, DataT]): +class UnifiUpdateEntityDescriptionMixin(Generic[_HandlerT, _DataT]): """Validate and load entities from different UniFi handlers.""" control_fn: Callable[[aiounifi.Controller, str], Coroutine[Any, Any, None]] - state_fn: Callable[[aiounifi.Controller, DataT], bool] + state_fn: Callable[[aiounifi.Controller, _DataT], bool] @dataclass class UnifiUpdateEntityDescription( UpdateEntityDescription, - UnifiEntityDescription[HandlerT, DataT], - UnifiEntityLoader[HandlerT, DataT], + UnifiEntityDescription[_HandlerT, _DataT], + UnifiUpdateEntityDescriptionMixin[_HandlerT, _DataT], ): """Class describing UniFi update entity.""" @@ -103,41 +88,15 @@ async def async_setup_entry( ) -> None: """Set up update entities for UniFi Network integration.""" controller: UniFiController = hass.data[UNIFI_DOMAIN][config_entry.entry_id] - - @callback - def async_load_entities(description: UnifiUpdateEntityDescription) -> None: - """Load and subscribe to UniFi devices.""" - entities: list[UpdateEntity] = [] - api_handler = description.api_handler_fn(controller.api) - - @callback - def async_create_entity(event: ItemEvent, obj_id: str) -> None: - """Create UniFi entity.""" - if not description.allowed_fn( - controller, obj_id - ) or not description.supported_fn(controller.api, obj_id): - return - - entity = UnifiDeviceUpdateEntity(obj_id, controller, description) - if event == ItemEvent.ADDED: - async_add_entities([entity]) - return - entities.append(entity) - - for obj_id in api_handler: - async_create_entity(ItemEvent.CHANGED, obj_id) - async_add_entities(entities) - - api_handler.subscribe(async_create_entity, ItemEvent.ADDED) - - for description in ENTITY_DESCRIPTIONS: - async_load_entities(description) + controller.register_platform_add_entities( + UnifiDeviceUpdateEntity, ENTITY_DESCRIPTIONS, async_add_entities + ) -class UnifiDeviceUpdateEntity(UnifiEntity[HandlerT, DataT], UpdateEntity): +class UnifiDeviceUpdateEntity(UnifiEntity[_HandlerT, _DataT], UpdateEntity): """Representation of a UniFi device update entity.""" - entity_description: UnifiUpdateEntityDescription[HandlerT, DataT] + entity_description: UnifiUpdateEntityDescription[_HandlerT, _DataT] @callback def async_initiate_state(self) -> None: @@ -163,12 +122,6 @@ class UnifiDeviceUpdateEntity(UnifiEntity[HandlerT, DataT], UpdateEntity): description = self.entity_description obj = description.object_fn(self.controller.api, self._obj_id) - if ( - in_progress := description.state_fn(self.controller.api, obj) - ) != self.in_progress: - self._attr_in_progress = in_progress - self._write_state = True + self._attr_in_progress = description.state_fn(self.controller.api, obj) self._attr_installed_version = obj.version self._attr_latest_version = obj.upgrade_to_firmware or obj.version - if self.installed_version != self.latest_version: - self._write_state = True diff --git a/homeassistant/components/unifiprotect/binary_sensor.py b/homeassistant/components/unifiprotect/binary_sensor.py index 4a3b76581ba..c7893eb45d1 100644 --- a/homeassistant/components/unifiprotect/binary_sensor.py +++ b/homeassistant/components/unifiprotect/binary_sensor.py @@ -346,6 +346,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = ( name="Motion", device_class=BinarySensorDeviceClass.MOTION, ufp_value="is_motion_detected", + ufp_enabled="is_motion_detection_on", ufp_event_obj="last_motion_event", ), ProtectBinaryEventEntityDescription( diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 5db0941549b..518eb623ca1 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Callable, Generator, Iterable from datetime import timedelta import logging -from typing import Any, Union, cast +from typing import Any, cast from pyunifiprotect import ProtectApiClient from pyunifiprotect.data import ( @@ -39,7 +39,7 @@ from .const import ( from .utils import async_dispatch_id as _ufpd, async_get_devices_by_type _LOGGER = logging.getLogger(__name__) -ProtectDeviceType = Union[ProtectAdoptableDeviceModel, NVR] +ProtectDeviceType = ProtectAdoptableDeviceModel | NVR @callback diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index de622497a3d..5398016ba63 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -4,7 +4,7 @@ "integration_type": "hub", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifiprotect", - "requirements": ["pyunifiprotect==4.6.1", "unifi-discovery==1.1.7"], + "requirements": ["pyunifiprotect==4.6.2", "unifi-discovery==1.1.7"], "dependencies": ["http", "repairs"], "codeowners": ["@briis", "@AngellusMortis", "@bdraco"], "quality_scale": "platinum", diff --git a/homeassistant/components/unifiprotect/media_player.py b/homeassistant/components/unifiprotect/media_player.py index 1dd1938ff49..4704c42762e 100644 --- a/homeassistant/components/unifiprotect/media_player.py +++ b/homeassistant/components/unifiprotect/media_player.py @@ -153,11 +153,11 @@ class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity): await self.device.play_audio(media_id, blocking=False) except StreamError as err: raise HomeAssistantError(err) from err - else: - # update state after starting player - self._async_updated_event(self.device) - # wait until player finishes to update state again - await self.device.wait_until_audio_completes() + + # update state after starting player + self._async_updated_event(self.device) + # wait until player finishes to update state again + await self.device.wait_until_audio_completes() self._async_updated_event(self.device) diff --git a/homeassistant/components/unifiprotect/media_source.py b/homeassistant/components/unifiprotect/media_source.py index a064295bebb..48aa7e0a6a2 100644 --- a/homeassistant/components/unifiprotect/media_source.py +++ b/homeassistant/components/unifiprotect/media_source.py @@ -5,7 +5,7 @@ from __future__ import annotations import asyncio from datetime import date, datetime, timedelta from enum import Enum -from typing import Any, cast +from typing import Any, NoReturn, cast from pyunifiprotect.data import ( Camera, @@ -107,18 +107,13 @@ def _get_month_start_end(start: datetime) -> tuple[datetime, datetime]: @callback -def _bad_identifier(identifier: str, err: Exception | None = None) -> BrowseMediaSource: +def _bad_identifier(identifier: str, err: Exception | None = None) -> NoReturn: msg = f"Unexpected identifier: {identifier}" if err is None: raise BrowseError(msg) raise BrowseError(msg) from err -@callback -def _bad_identifier_media(identifier: str, err: Exception | None = None) -> PlayMedia: - return cast(PlayMedia, _bad_identifier(identifier, err)) - - @callback def _format_duration(duration: timedelta) -> str: formatted = "" @@ -164,20 +159,20 @@ class ProtectMediaSource(MediaSource): parts = item.identifier.split(":") if len(parts) != 3 or parts[1] not in ("event", "eventthumb"): - return _bad_identifier_media(item.identifier) + _bad_identifier(item.identifier) thumbnail_only = parts[1] == "eventthumb" try: data = self.data_sources[parts[0]] except (KeyError, IndexError) as err: - return _bad_identifier_media(item.identifier, err) + _bad_identifier(item.identifier, err) event = data.api.bootstrap.events.get(parts[2]) if event is None: try: event = await data.api.get_event(parts[2]) except NvrError as err: - return _bad_identifier_media(item.identifier, err) + _bad_identifier(item.identifier, err) else: # cache the event for later data.api.bootstrap.events[event.id] = event @@ -241,15 +236,15 @@ class ProtectMediaSource(MediaSource): try: data = self.data_sources[parts[0]] except (KeyError, IndexError) as err: - return _bad_identifier(item.identifier, err) + _bad_identifier(item.identifier, err) if len(parts) < 2: - return _bad_identifier(item.identifier) + _bad_identifier(item.identifier) try: identifier_type = IdentifierType(parts[1]) except ValueError as err: - return _bad_identifier(item.identifier, err) + _bad_identifier(item.identifier, err) if identifier_type in (IdentifierType.EVENT, IdentifierType.EVENT_THUMB): thumbnail_only = identifier_type == IdentifierType.EVENT_THUMB @@ -271,7 +266,7 @@ class ProtectMediaSource(MediaSource): try: event_type = SimpleEventType(parts.pop(0).lower()) except (IndexError, ValueError) as err: - return _bad_identifier(item.identifier, err) + _bad_identifier(item.identifier, err) if len(parts) == 0: return await self._build_events_type( @@ -281,17 +276,17 @@ class ProtectMediaSource(MediaSource): try: time_type = IdentifierTimeType(parts.pop(0)) except ValueError as err: - return _bad_identifier(item.identifier, err) + _bad_identifier(item.identifier, err) if len(parts) == 0: - return _bad_identifier(item.identifier) + _bad_identifier(item.identifier) # {nvr_id}:browse:all|{camera_id}:all|{event_type}:recent:{day_count} if time_type == IdentifierTimeType.RECENT: try: days = int(parts.pop(0)) except (IndexError, ValueError) as err: - return _bad_identifier(item.identifier, err) + _bad_identifier(item.identifier, err) return await self._build_recent( data, camera_id, event_type, days, build_children=True @@ -302,7 +297,7 @@ class ProtectMediaSource(MediaSource): try: start, is_month, is_all = self._parse_range(parts) except (IndexError, ValueError) as err: - return _bad_identifier(item.identifier, err) + _bad_identifier(item.identifier, err) if is_month: return await self._build_month( @@ -336,9 +331,7 @@ class ProtectMediaSource(MediaSource): try: event = await data.api.get_event(event_id) except NvrError as err: - return _bad_identifier( - f"{data.api.bootstrap.nvr.id}:{subtype}:{event_id}", err - ) + _bad_identifier(f"{data.api.bootstrap.nvr.id}:{subtype}:{event_id}", err) if event.start is None or event.end is None: raise BrowseError("Event is still ongoing") diff --git a/homeassistant/components/unifiprotect/models.py b/homeassistant/components/unifiprotect/models.py index b7913567504..40280c02867 100644 --- a/homeassistant/components/unifiprotect/models.py +++ b/homeassistant/components/unifiprotect/models.py @@ -5,7 +5,7 @@ from collections.abc import Callable, Coroutine from dataclasses import dataclass from enum import Enum import logging -from typing import Any, Generic, TypeVar, Union, cast +from typing import Any, Generic, TypeVar, cast from pyunifiprotect.data import NVR, Event, ProtectAdoptableDeviceModel @@ -15,7 +15,7 @@ from .utils import get_nested_attr _LOGGER = logging.getLogger(__name__) -T = TypeVar("T", bound=Union[ProtectAdoptableDeviceModel, NVR]) +T = TypeVar("T", bound=ProtectAdoptableDeviceModel | NVR) class PermRequired(int, Enum): diff --git a/homeassistant/components/unifiprotect/number.py b/homeassistant/components/unifiprotect/number.py index f8575f39d86..6486ed4c2b9 100644 --- a/homeassistant/components/unifiprotect/number.py +++ b/homeassistant/components/unifiprotect/number.py @@ -31,9 +31,9 @@ from .utils import async_dispatch_id as _ufpd class NumberKeysMixin: """Mixin for required keys.""" - ufp_max: int - ufp_min: int - ufp_step: int + ufp_max: int | float + ufp_min: int | float + ufp_step: int | float @dataclass @@ -59,6 +59,10 @@ async def _set_auto_close(obj: Doorlock, value: float) -> None: await obj.set_auto_close_time(timedelta(seconds=value)) +def _get_chime_duration(obj: Camera) -> int: + return int(obj.chime_duration.total_seconds()) + + CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ProtectNumberEntityDescription( key="wdr_value", @@ -102,6 +106,21 @@ CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( ufp_set_method="set_camera_zoom", ufp_perm=PermRequired.WRITE, ), + ProtectNumberEntityDescription( + key="chime_duration", + name="Chime Duration", + icon="mdi:bell", + entity_category=EntityCategory.CONFIG, + native_unit_of_measurement=UnitOfTime.SECONDS, + ufp_min=1, + ufp_max=10, + ufp_step=0.1, + ufp_required_field="feature_flags.has_chime", + ufp_enabled="is_digital_chime", + ufp_value_fn=_get_chime_duration, + ufp_set_method="set_chime_duration", + ufp_perm=PermRequired.WRITE, + ), ) LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = ( diff --git a/homeassistant/components/unifiprotect/switch.py b/homeassistant/components/unifiprotect/switch.py index fa501f6a364..63ae4419235 100644 --- a/homeassistant/components/unifiprotect/switch.py +++ b/homeassistant/components/unifiprotect/switch.py @@ -139,6 +139,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( icon="mdi:run-fast", entity_category=EntityCategory.CONFIG, ufp_value="recording_settings.enable_motion_detection", + ufp_enabled="is_recording_enabled", ufp_set_method="set_motion_detection", ufp_perm=PermRequired.WRITE, ), @@ -149,6 +150,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_person", ufp_value="is_person_detection_on", + ufp_enabled="is_recording_enabled", ufp_set_method="set_person_detection", ufp_perm=PermRequired.WRITE, ), @@ -159,6 +161,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_vehicle", ufp_value="is_vehicle_detection_on", + ufp_enabled="is_recording_enabled", ufp_set_method="set_vehicle_detection", ufp_perm=PermRequired.WRITE, ), @@ -169,6 +172,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_face", ufp_value="is_face_detection_on", + ufp_enabled="is_recording_enabled", ufp_set_method="set_face_detection", ufp_perm=PermRequired.WRITE, ), @@ -179,6 +183,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_package", ufp_value="is_package_detection_on", + ufp_enabled="is_recording_enabled", ufp_set_method="set_package_detection", ufp_perm=PermRequired.WRITE, ), @@ -189,6 +194,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_license_plate", ufp_value="is_license_plate_detection_on", + ufp_enabled="is_recording_enabled", ufp_set_method="set_license_plate_detection", ufp_perm=PermRequired.WRITE, ), @@ -199,6 +205,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = ( entity_category=EntityCategory.CONFIG, ufp_required_field="can_detect_smoke", ufp_value="is_smoke_detection_on", + ufp_enabled="is_recording_enabled", ufp_set_method="set_smoke_detection", ufp_perm=PermRequired.WRITE, ), diff --git a/homeassistant/components/unifiprotect/translations/ca.json b/homeassistant/components/unifiprotect/translations/ca.json index 633024d0024..f6c09363faf 100644 --- a/homeassistant/components/unifiprotect/translations/ca.json +++ b/homeassistant/components/unifiprotect/translations/ca.json @@ -59,12 +59,12 @@ "fix_flow": { "step": { "confirm": { - "description": "El servei `unifiprotect.set_doorbell_message` est\u00e0 obsolet, ha canviat per la nova entitat Doorbell Text que s'afegeix a cada dispositiu Doorbell. S'eliminar\u00e0 a la versi\u00f3 2023.3.0. Actualitza-ho perqu\u00e8 utilitzi el [servei `text.set_value`]({link}).", - "title": "set_doorbell_message est\u00e0 obsolet" + "description": "El servei `unifiprotect.set_doorbell_message` \u00e9s obsolet, ha canviat per la nova entitat Doorbell Text que s'afegeix a cada dispositiu Doorbell. S'eliminar\u00e0 a la versi\u00f3 2023.3.0. Actualitzeu-ho perqu\u00e8 utilitzi el [servei `text.set_value`]({link}).", + "title": "El set_doorbell_message \u00e9s obsolet" } } }, - "title": "set_doorbell_message est\u00e0 obsolet" + "title": "El set_doorbell_message \u00e9s obsolet" }, "ea_setup_failed": { "description": "Est\u00e0s utilitzant la versi\u00f3 v{version} d'UniFi Protect, que \u00e9s una versi\u00f3 d'acc\u00e9s anticipat. S'ha produ\u00eft un error irrecuperable en intentar carregar la integraci\u00f3. [Baixa a una versi\u00f3 estable](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) d'UniFi Protect per continuar utilitzant la integraci\u00f3. \n\n Error: {error}", diff --git a/homeassistant/components/unifiprotect/translations/el.json b/homeassistant/components/unifiprotect/translations/el.json index ae4803f4619..103e380111d 100644 --- a/homeassistant/components/unifiprotect/translations/el.json +++ b/homeassistant/components/unifiprotect/translations/el.json @@ -52,9 +52,20 @@ }, "issues": { "deprecate_smart_sensor": { - "description": "\u039f \u03b5\u03bd\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \"\u0395\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03b1\u03bd\u03c4\u03b9\u03ba\u03b5\u03af\u03bc\u03b5\u03bd\u03bf\" \u03b3\u03b9\u03b1 \u03ad\u03be\u03c5\u03c0\u03bd\u03b5\u03c2 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03c3\u03b5\u03b9\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \u0388\u03c7\u03b5\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b1\u03b8\u03b5\u03af \u03bc\u03b5 \u03bc\u03b5\u03bc\u03bf\u03bd\u03c9\u03bc\u03ad\u03bd\u03bf\u03c5\u03c2 \u03b4\u03c5\u03b1\u03b4\u03b9\u03ba\u03bf\u03cd\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03ad\u03be\u03c5\u03c0\u03bd\u03b7\u03c2 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c4\u03cd\u03c0\u03bf \u03ad\u03be\u03c5\u03c0\u03bd\u03b7\u03c2 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2. \u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03b1\u03bd\u03ac\u03bb\u03bf\u03b3\u03b1 \u03c4\u03c5\u03c7\u03cc\u03bd \u03c0\u03c1\u03cc\u03c4\u03c5\u03c0\u03b1 \u03ae \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2.", + "description": "\u039f \u03b5\u03bd\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03bf\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \"\u0395\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf \u03b1\u03bd\u03c4\u03b9\u03ba\u03b5\u03af\u03bc\u03b5\u03bd\u03bf\" \u03b3\u03b9\u03b1 \u03ad\u03be\u03c5\u03c0\u03bd\u03b5\u03c2 \u03b1\u03bd\u03b9\u03c7\u03bd\u03b5\u03cd\u03c3\u03b5\u03b9\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af. \u0388\u03c7\u03b5\u03b9 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b1\u03b8\u03b5\u03af \u03bc\u03b5 \u03bc\u03b5\u03bc\u03bf\u03bd\u03c9\u03bc\u03ad\u03bd\u03bf\u03c5\u03c2 \u03b4\u03c5\u03b1\u03b4\u03b9\u03ba\u03bf\u03cd\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03ad\u03be\u03c5\u03c0\u03bd\u03b7\u03c2 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c4\u03cd\u03c0\u03bf \u03ad\u03be\u03c5\u03c0\u03bd\u03b7\u03c2 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2. \n\n \u0391\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03bf\u03cd\u03bd \u03bf\u03b9 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03bc\u03ad\u03bd\u03bf\u03b9 \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03af \u03ae \u03c4\u03b1 \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd \u03bc\u03af\u03b1 \u03ae \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03b1\u03c0\u03cc \u03c4\u03b9\u03c2 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03bc\u03ad\u03bd\u03b5\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2:\n {items}\n \u0397 \u03c0\u03b1\u03c1\u03b1\u03c0\u03ac\u03bd\u03c9 \u03bb\u03af\u03c3\u03c4\u03b1 \u03b5\u03bd\u03b4\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bb\u03bb\u03b9\u03c0\u03ae\u03c2 \u03ba\u03b1\u03b9 \u03bd\u03b1 \u03bc\u03b7\u03bd \u03c0\u03b5\u03c1\u03b9\u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03ba\u03b1\u03bc\u03af\u03b1 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c0\u03c1\u03bf\u03c4\u03cd\u03c0\u03c9\u03bd \u03bc\u03ad\u03c3\u03b1 \u03c3\u03c4\u03bf\u03c5\u03c2 \u03c0\u03af\u03bd\u03b1\u03ba\u03b5\u03c2 \u03b5\u03c1\u03b3\u03b1\u03bb\u03b5\u03af\u03c9\u03bd. \u0395\u03bd\u03b7\u03bc\u03b5\u03c1\u03ce\u03c3\u03c4\u03b5 \u03b1\u03bd\u03ac\u03bb\u03bf\u03b3\u03b1 \u03c4\u03c5\u03c7\u03cc\u03bd \u03c0\u03c1\u03cc\u03c4\u03c5\u03c0\u03b1, \u03b1\u03c5\u03c4\u03bf\u03bc\u03b1\u03c4\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03ae \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03b1.", "title": "\u039f \u03ad\u03be\u03c5\u03c0\u03bd\u03bf\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b1\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7\u03c2 \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" }, + "deprecated_service_set_doorbell_message": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \"unifiprotect.set_doorbell_message\" \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c5\u03c0\u03ad\u03c1 \u03c4\u03b7\u03c2 \u03bd\u03ad\u03b1\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 Doorbell Text \u03c0\u03bf\u03c5 \u03c0\u03c1\u03bf\u03c3\u03c4\u03ad\u03b8\u03b7\u03ba\u03b5 \u03c3\u03b5 \u03ba\u03ac\u03b8\u03b5 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae Doorbell. \u0398\u03b1 \u03b1\u03c6\u03b1\u03b9\u03c1\u03b5\u03b8\u03b5\u03af \u03c3\u03c4\u03b7\u03bd \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 2023.3.0. \u039a\u03ac\u03bd\u03c4\u03b5 \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 [`text.set_value`]( {link} ).", + "title": "\u03a4\u03bf set_doorbell_message \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + } + } + }, + "title": "\u03a4\u03bf set_doorbell_message \u03ad\u03c7\u03b5\u03b9 \u03ba\u03b1\u03c4\u03b1\u03c1\u03b3\u03b7\u03b8\u03b5\u03af" + }, "ea_setup_failed": { "description": "\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 v{version} \u03c4\u03bf\u03c5 UniFi Protect \u03c0\u03bf\u03c5 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7 Early Access. \u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ad\u03bd\u03b1 \u03bc\u03b7 \u03b1\u03bd\u03b1\u03ba\u03c4\u03ae\u03c3\u03b9\u03bc\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c3\u03c0\u03ac\u03b8\u03b5\u03b9\u03b1 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7\u03c2. \u039a\u03ac\u03bd\u03c4\u03b5 [\u03c5\u03c0\u03bf\u03b2\u03ac\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c3\u03b5 \u03c3\u03c4\u03b1\u03b8\u03b5\u03c1\u03ae \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) \u03c4\u03bf\u03c5 UniFi Protect \u03b3\u03b9\u03b1 \u03bd\u03b1 \u03c3\u03c5\u03bd\u03b5\u03c7\u03af\u03c3\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b5\u03bd\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7. \n\n \u03a3\u03c6\u03ac\u03bb\u03bc\u03b1: {error}", "title": "\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b5\u03b3\u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 \u03bc\u03b5 \u03c7\u03c1\u03ae\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03ad\u03ba\u03b4\u03bf\u03c3\u03b7\u03c2 Early Access" diff --git a/homeassistant/components/unifiprotect/translations/et.json b/homeassistant/components/unifiprotect/translations/et.json index ed93a65b463..bdd6fd6694a 100644 --- a/homeassistant/components/unifiprotect/translations/et.json +++ b/homeassistant/components/unifiprotect/translations/et.json @@ -52,9 +52,20 @@ }, "issues": { "deprecate_smart_sensor": { - "description": "\u00dchtse \"Avastatud objekti\" andur arukate tuvastuste jaoks on n\u00fc\u00fcdseks kaotanud kehtivuse. See on asendatud iga aruka avastamise t\u00fc\u00fcbi jaoks eraldi aruka avastamise binaarsete anduritega. Palun ajakohasta vastavalt k\u00f5ik mallid v\u00f5i automaatika.", + "description": "Nutikate tuvastuste jaoks m\u00f5eldud \u00fchtne andur \"Tuvastatud objekt\" on n\u00fc\u00fcd kasutuselt k\u00f5rvaldatud. See on asendatud individuaalsete nutika tuvastamise binaarsete anduritega iga nutika tuvastust\u00fc\u00fcbi jaoks.\n\nAllpool on tuvastatud automatiseerimised v\u00f5i skriptid, mis kasutavad \u00fchte v\u00f5i mitut aegunud olemit:\n{items}\n\u00dclaltoodud loend v\u00f5ib olla puudulik ja see ei sisalda armatuurlaudade sees olevaid mallikasutusi. Palun v\u00e4rskenda vastavalt k\u00f5iki malle, automaatikaid v\u00f5i skripte.", "title": "Nutika tuvastamise andur on aegunud" }, + "deprecated_service_set_doorbell_message": { + "fix_flow": { + "step": { + "confirm": { + "description": "Teenus `unifiprotect.set_doorbell_message` on kaotanud oma kehtivuse, asendades selle uue, igale uksekella seadmele lisatud uksekella teksti olemiga. See eemaldatakse versioonis v2023.3.0. Palun ajakohasta, et kasutada teenust [`text.set_value` ({link}).", + "title": "set_doorbell_message on aegunud" + } + } + }, + "title": "set_doorbell_message on aegunud" + }, "ea_setup_failed": { "description": "Kasutad UniFi Protecti v {version} mis on varajase juurdep\u00e4\u00e4su versioon. Sidumise laadimisel ilmnes parandamatu viga. Sidumise kasutamise j\u00e4tkamiseks [alanda UniFi Protecti stabiilsele versioonile](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect). \n\n Viga: {error}", "title": "Varajase juurdep\u00e4\u00e4su versiooni h\u00e4\u00e4lestamise t\u00f5rge" diff --git a/homeassistant/components/unifiprotect/translations/hu.json b/homeassistant/components/unifiprotect/translations/hu.json index ab870ed0486..4bb660a607f 100644 --- a/homeassistant/components/unifiprotect/translations/hu.json +++ b/homeassistant/components/unifiprotect/translations/hu.json @@ -52,9 +52,20 @@ }, "issues": { "deprecate_smart_sensor": { - "description": "Az intelligens \u00e9szlel\u00e9sek egys\u00e9ges \"Detected Object\" \u00e9rz\u00e9kel\u0151je elavult. Hely\u00e9t az egyes intelligens \u00e9szlel\u00e9si t\u00edpusokhoz tartoz\u00f3 egyedi intelligens \u00e9szlel\u00e9si bin\u00e1ris \u00e9rz\u00e9kel\u0151k vett\u00e9k \u00e1t. K\u00e9rj\u00fck, ennek megfelel\u0151en friss\u00edtse a sablonokat \u00e9s automatiz\u00e1l\u00e1sokat.", + "description": "Az intelligens \u00e9szlel\u00e9sek egys\u00e9ges \"Detected Object\" \u00e9rz\u00e9kel\u0151je elavult. Hely\u00e9t az egyes intelligens \u00e9szlel\u00e9si t\u00edpusokhoz tartoz\u00f3 egyedi intelligens \u00e9szlel\u00e9si bin\u00e1ris \u00e9rz\u00e9kel\u0151k vett\u00e9k \u00e1t.\n\nAz al\u00e1bbiakban az \u00e9szlelt automatiz\u00e1ci\u00f3k vagy szkriptek tal\u00e1lhat\u00f3k, amelyek egy vagy t\u00f6bb elavult entit\u00e1st haszn\u00e1lnak:\n{items}\nA fenti lista hi\u00e1nyos lehet, \u00e9s nem tartalmazza a kezel\u0151fel\u00fcleten bel\u00fcli sablonokat. K\u00e9rem, ennek megfelel\u0151en friss\u00edtse a sablonokat, automatiz\u00e1l\u00e1sokat vagy szkripteket.", "title": "Az intelligens \u00e9rz\u00e9kel\u00e9si \u00e9rz\u00e9kel\u0151 elavult" }, + "deprecated_service_set_doorbell_message": { + "fix_flow": { + "step": { + "confirm": { + "description": "Az `unifiprotect.set_doorbell_message` szolg\u00e1ltat\u00e1s elavultt\u00e1 v\u00e1lt a minden egyes Doorbell eszk\u00f6zh\u00f6z hozz\u00e1adott \u00faj Doorbell sz\u00f6veges entit\u00e1s jav\u00e1ra. A v2023.3.0 verzi\u00f3ban elt\u00e1vol\u00edt\u00e1sra ker\u00fcl. K\u00e9rem, friss\u00edtse a [`text.set_value` szolg\u00e1ltat\u00e1s]({link}) haszn\u00e1lat\u00e1ra.", + "title": "set_doorbell_message elavult" + } + } + }, + "title": "set_doorbell_message elavult" + }, "ea_setup_failed": { "description": "Az UniFi Protect {version}. verzi\u00f3j\u00e1t haszn\u00e1lja, amely egy korai hozz\u00e1f\u00e9r\u00e9s\u0171 verzi\u00f3. Helyrehozhatatlan hiba t\u00f6rt\u00e9nt az integr\u00e1ci\u00f3 bet\u00f6lt\u00e9se k\u00f6zben. K\u00e9rem, [haszn\u00e1ljon stabil verzi\u00f3t](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) az integr\u00e1ci\u00f3 tov\u00e1bbi haszn\u00e1lat\u00e1hoz.\n\nHiba: {error}", "title": "Be\u00e1ll\u00edt\u00e1si hiba a korai hozz\u00e1f\u00e9r\u00e9s\u0171 verzi\u00f3 haszn\u00e1lat\u00e1val" diff --git a/homeassistant/components/unifiprotect/translations/id.json b/homeassistant/components/unifiprotect/translations/id.json index 9869e57756b..91eee186cc0 100644 --- a/homeassistant/components/unifiprotect/translations/id.json +++ b/homeassistant/components/unifiprotect/translations/id.json @@ -52,9 +52,20 @@ }, "issues": { "deprecate_smart_sensor": { - "description": "Sensor \"Objek Terdeteksi\" terpadu untuk deteksi cerdas sekarang sudah tidak digunakan lagi. Sensor ini telah diganti dengan sensor biner deteksi cerdas individual untuk setiap jenis deteksi cerdas. Perbarui semua templat atau otomasi yang terkait.", + "description": "Sensor \"Objek Terdeteksi\" terpadu untuk deteksi cerdas sekarang sudah tidak digunakan lagi. Sensor ini telah diganti dengan sensor biner deteksi cerdas individual untuk setiap jenis deteksi cerdas. Perbarui semua templat atau otomasi yang terkait.\n\nDi bawah ini adalah otomasi atau skrip yang terdeteksi yang menggunakan satu atau lebih entitas yang sudah tidak digunakan lagi:\n{items}\nDaftar di atas mungkin tidak lengkap dan tidak termasuk penggunaan semua templat dalam dasbor. Perbarui templat, otomatisasi, atau skrip apa pun yang sesuai.", "title": "Sensor Deteksi Cerdas Tidak Digunakan Lagi" }, + "deprecated_service_set_doorbell_message": { + "fix_flow": { + "step": { + "confirm": { + "description": "Layanan `unifiprotect.set_doorbell_message` tidak digunakan lagi dan sebagai gantinya tersedia entitas Teks Bel baru yang ditambahkan ke setiap perangkat Bel Pintu. Layanan akan dihapus di Home Assistant 2023.3.0. Perbarui dengan menggunakan [layanan `text.set_value`]({link}).", + "title": "set_doorbell_message sudah tidak digunakan lagi" + } + } + }, + "title": "set_doorbell_message sudah tidak digunakan lagi" + }, "ea_setup_failed": { "description": "Anda menggunakan v{version} dari UniFi Protect yang merupakan versi Early Access. Terjadi kesalahan yang tidak dapat dipulihkan saat mencoba memuat integrasi. Silakan [turunkan ke versi stabil](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) dari UniFi Protect untuk terus menggunakan integrasi.\n\nKesalahan: {error}", "title": "Kesalahan penyiapan menggunakan versi Early Access" diff --git a/homeassistant/components/unifiprotect/translations/lv.json b/homeassistant/components/unifiprotect/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/no.json b/homeassistant/components/unifiprotect/translations/no.json index ceffb21fff2..2cc729da4b8 100644 --- a/homeassistant/components/unifiprotect/translations/no.json +++ b/homeassistant/components/unifiprotect/translations/no.json @@ -52,9 +52,20 @@ }, "issues": { "deprecate_smart_sensor": { - "description": "Den enhetlige \u00abDetected Object\u00bb-sensoren for smarte deteksjoner er n\u00e5 avviklet. Den er erstattet med individuelle smartdeteksjonsbin\u00e6re sensorer for hver smartdeteksjonstype. Oppdater eventuelle maler eller automatiseringer tilsvarende.", + "description": "Den enhetlige \u00abDetected Object\u00bb-sensoren for smarte deteksjoner er n\u00e5 avviklet. Den er erstattet med individuelle smartdeteksjonsbin\u00e6re sensorer for hver smartdeteksjonstype. \n\n Nedenfor er de oppdagede automatiseringene eller skriptene som bruker \u00e9n eller flere av de avviklede enhetene:\n {items}\n Listen ovenfor kan v\u00e6re ufullstendig, og den inkluderer ingen malbruk inne i dashbord. Vennligst oppdater eventuelle maler, automatiseringer eller skript tilsvarende.", "title": "Smart deteksjonssensor avviklet" }, + "deprecated_service_set_doorbell_message": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u00abunifiprotect.set_doorbell_message\u00bb-tjenesten er avviklet til fordel for den nye Doorbell Text-enheten som legges til hver Doorbell-enhet. Den vil bli fjernet i v2023.3.0. Vennligst oppdater for \u00e5 bruke [`text.set_value`-tjenesten]( {link} ).", + "title": "set_doorbell_message er avviklet" + } + } + }, + "title": "set_doorbell_message er avviklet" + }, "ea_setup_failed": { "description": "Du bruker v {version} av UniFi Protect som er en tidlig tilgangsversjon. Det oppstod en uopprettelig feil under fors\u00f8k p\u00e5 \u00e5 laste integrasjonen. Vennligst [nedgrader til en stabil versjon](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) av UniFi Protect for \u00e5 fortsette \u00e5 bruke integrasjonen. \n\n Feil: {error}", "title": "Konfigurasjonsfeil ved bruk av tidlig tilgangsversjon" diff --git a/homeassistant/components/unifiprotect/translations/pl.json b/homeassistant/components/unifiprotect/translations/pl.json index e54a4e5c61b..556b0fdfad0 100644 --- a/homeassistant/components/unifiprotect/translations/pl.json +++ b/homeassistant/components/unifiprotect/translations/pl.json @@ -52,9 +52,20 @@ }, "issues": { "deprecate_smart_sensor": { - "description": "Ujednolicony sensor \u201eWykryty obiekt\u201d do inteligentnego wykrywania jest teraz przestarza\u0142y. Zosta\u0142 on zast\u0105piony indywidualnymi sensorami binarnymi dla ka\u017cdego typu inteligentnej detekcji. Zaktualizuj odpowiednio wszystkie szablony lub automatyzacje.", + "description": "Ujednolicony sensor \u201eWykryty obiekt\u201d do inteligentnego wykrywania jest teraz przestarza\u0142y. Zosta\u0142 on zast\u0105piony indywidualnymi sensorami binarnymi dla ka\u017cdego typu inteligentnej detekcji.\n\nPoni\u017cej znajduj\u0105 si\u0119 wykryte automatyzacje lub skrypty korzystaj\u0105ce z conajmniej jednej przestarza\u0142ej encji:\n{item}\nPowy\u017csza lista mo\u017ce by\u0107 niekompletna i nie zawiera \u017cadnych zastosowa\u0144 szablon\u00f3w z dashboard\u00f3w. Zaktualizuj odpowiednio wszystkie szablony, automatyzacje lub skrypty.", "title": "Przestarza\u0142y inteligentny sensor wykrywania" }, + "deprecated_service_set_doorbell_message": { + "fix_flow": { + "step": { + "confirm": { + "description": "Us\u0142uga `unifiprotect.set_doorbell_message` jest przestarza\u0142a i zast\u0105piona now\u0105 encj\u0105 tekstow\u0105 dodawan\u0105 do ka\u017cdego urz\u0105dzenia typu doorbell. Us\u0142uga zostanie usuni\u0119ta w wersji 2023.3.0. Zaktualizuj, aby korzysta\u0107 z us\u0142ugi [`text.set_value`]({link}).", + "title": "set_doorbell_message jest przestarza\u0142a" + } + } + }, + "title": "set_doorbell_message jest przestarza\u0142a" + }, "ea_setup_failed": { "description": "U\u017cywasz wersji {version} UniFi Protect, kt\u00f3ra jest wersj\u0105 Early Access. Wyst\u0105pi\u0142 nienaprawialny b\u0142\u0105d podczas pr\u00f3by za\u0142adowania integracji. Aby kontynuowa\u0107 korzystanie z integracji, [zmie\u0144 wersj\u0119 na stabiln\u0105](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) UniFi Protect. \n\nB\u0142\u0105d: {error}", "title": "B\u0142\u0105d konfiguracji w wersji Early Access" diff --git a/homeassistant/components/unifiprotect/translations/pt-BR.json b/homeassistant/components/unifiprotect/translations/pt-BR.json index 564ad749a01..04bbbd6aa9a 100644 --- a/homeassistant/components/unifiprotect/translations/pt-BR.json +++ b/homeassistant/components/unifiprotect/translations/pt-BR.json @@ -52,9 +52,20 @@ }, "issues": { "deprecate_smart_sensor": { - "description": "O sensor unificado de \"objeto detectado\" para detec\u00e7\u00f5es inteligentes agora est\u00e1 obsoleto. Ele foi substitu\u00eddo por sensores bin\u00e1rios de detec\u00e7\u00e3o inteligente individuais para cada tipo de detec\u00e7\u00e3o inteligente. Atualize quaisquer modelos ou automa\u00e7\u00f5es de acordo.", + "description": "O sensor unificado de \"objeto detectado\" para detec\u00e7\u00f5es inteligentes agora est\u00e1 obsoleto. Ele foi substitu\u00eddo por sensores bin\u00e1rios de detec\u00e7\u00e3o inteligente individuais para cada tipo de detec\u00e7\u00e3o inteligente. \n\n Abaixo est\u00e3o as automa\u00e7\u00f5es ou scripts detectados que usam uma ou mais entidades obsoletas:\n {items}\n A lista acima pode estar incompleta e n\u00e3o inclui nenhum uso de modelo dentro dos pain\u00e9is. Atualize quaisquer modelos, automa\u00e7\u00f5es ou scripts de acordo.", "title": "Sensor de detec\u00e7\u00e3o inteligente obsoleto" }, + "deprecated_service_set_doorbell_message": { + "fix_flow": { + "step": { + "confirm": { + "description": "O servi\u00e7o `unifiprotect.set_doorbell_message` est\u00e1 obsoleto em favor da nova entidade de texto de campainha adicionada a cada dispositivo de campainha. Ele ser\u00e1 removido em v2023.3.0. Atualize para usar o servi\u00e7o [`text.set_value`]( {link} ).", + "title": "set_doorbell_message est\u00e1 obsoleto" + } + } + }, + "title": "set_doorbell_message est\u00e1 obsoleto" + }, "ea_setup_failed": { "description": "Voc\u00ea est\u00e1 usando v {version} do UniFi Protect, que \u00e9 uma vers\u00e3o de acesso antecipado. Ocorreu um erro irrecuper\u00e1vel ao tentar carregar a integra\u00e7\u00e3o. Fa\u00e7a [downgrade para uma vers\u00e3o est\u00e1vel](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) do UniFi Protect para continuar usando a integra\u00e7\u00e3o. \n\n Erro: {error}", "title": "Erro de configura\u00e7\u00e3o usando a vers\u00e3o de acesso antecipado" diff --git a/homeassistant/components/unifiprotect/translations/ru.json b/homeassistant/components/unifiprotect/translations/ru.json index 1e2cde53076..76462991f4f 100644 --- a/homeassistant/components/unifiprotect/translations/ru.json +++ b/homeassistant/components/unifiprotect/translations/ru.json @@ -52,9 +52,20 @@ }, "issues": { "deprecate_smart_sensor": { - "description": "\u0421\u0435\u043d\u0441\u043e\u0440 \"Detected Object\" \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0439 \u0442\u0435\u043f\u0435\u0440\u044c \u0443\u0441\u0442\u0430\u0440\u0435\u043b. \u041e\u043d \u0437\u0430\u043c\u0435\u043d\u0451\u043d \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u043c\u0438 \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u043c\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430\u043c\u0438 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0442\u0438\u043f\u0430 \u0438\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f. \u041e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u0430\u0431\u043b\u043e\u043d\u044b \u0438\u043b\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0435 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b.", + "description": "\u0421\u0435\u043d\u0441\u043e\u0440 \"Detected Object\" \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0439 \u0442\u0435\u043f\u0435\u0440\u044c \u0443\u0441\u0442\u0430\u0440\u0435\u043b. \u041e\u043d \u0437\u0430\u043c\u0435\u043d\u0451\u043d \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u044b\u043c\u0438 \u0431\u0438\u043d\u0430\u0440\u043d\u044b\u043c\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430\u043c\u0438 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u0442\u0438\u043f\u0430 \u0438\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f. \n\n\u041d\u0438\u0436\u0435 \u043f\u0440\u0438\u0432\u0435\u0434\u0435\u043d\u044b \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438 \u0438\u043b\u0438 \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0435 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b (\u0441\u043f\u0438\u0441\u043e\u043a \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043d\u0435\u043f\u043e\u043b\u043d\u044b\u043c):\n{items}\n\u041e\u0442\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u0430\u0431\u043b\u043e\u043d\u044b \u0438\u043b\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0438\u0438, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0449\u0438\u0435 \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b.", "title": "\u0421\u0435\u043d\u0441\u043e\u0440 Smart Detection \u0443\u0441\u0442\u0430\u0440\u0435\u043b" }, + "deprecated_service_set_doorbell_message": { + "fix_flow": { + "step": { + "confirm": { + "description": "\u0421\u043b\u0443\u0436\u0431\u0430 `unifiprotect.set_doorbell_message` \u0443\u0441\u0442\u0430\u0440\u0435\u043b\u0430 \u0438 \u0437\u0430\u043c\u0435\u043d\u0435\u043d\u0430 \u043d\u0430 \u043d\u043e\u0432\u044b\u0439 \u043e\u0431\u044a\u0435\u043a\u0442 '\u0421\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u0434\u0432\u0435\u0440\u043d\u043e\u0433\u043e \u0437\u0432\u043e\u043d\u043a\u0430', \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c\u044b\u0439 \u043a \u043a\u0430\u0436\u0434\u043e\u043c\u0443 \u0434\u0432\u0435\u0440\u043d\u043e\u043c\u0443 \u0437\u0432\u043e\u043d\u043a\u0443. \u041e\u043d \u0431\u0443\u0434\u0435\u0442 \u0443\u0434\u0430\u043b\u0435\u043d \u0432 \u0432\u0435\u0440\u0441\u0438\u0438 2023.3.0. \u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435, \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0441\u043b\u0443\u0436\u0431\u0443 [`text.set_value`]( {link} ).", + "title": "set_doorbell_message \u0443\u0441\u0442\u0430\u0440\u0435\u043b" + } + } + }, + "title": "set_doorbell_message \u0443\u0441\u0442\u0430\u0440\u0435\u043b" + }, "ea_setup_failed": { "description": "\u0412\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 UniFi Protect v{version}, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432\u0435\u0440\u0441\u0438\u0435\u0439 \u0440\u0430\u043d\u043d\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430. \u041f\u0440\u0438 \u043f\u043e\u043f\u044b\u0442\u043a\u0435 \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u0443\u0441\u0442\u0440\u0430\u043d\u0438\u043c\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, [\u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043d\u0430 \u0441\u0442\u0430\u0431\u0438\u043b\u044c\u043d\u0443\u044e \u0432\u0435\u0440\u0441\u0438\u044e UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect), \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438.\n\n\u041e\u0448\u0438\u0431\u043a\u0430: {error}", "title": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 \u0432\u0435\u0440\u0441\u0438\u0438 \u0440\u0430\u043d\u043d\u0435\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430" diff --git a/homeassistant/components/unifiprotect/translations/sensor.tr.json b/homeassistant/components/unifiprotect/translations/sensor.tr.json new file mode 100644 index 00000000000..516d8565841 --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/sensor.tr.json @@ -0,0 +1,7 @@ +{ + "state": { + "unifiprotect__license_plate": { + "none": "Temiz" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifiprotect/translations/sk.json b/homeassistant/components/unifiprotect/translations/sk.json index 2e0d22fc8d0..5bf823c9c0a 100644 --- a/homeassistant/components/unifiprotect/translations/sk.json +++ b/homeassistant/components/unifiprotect/translations/sk.json @@ -52,14 +52,14 @@ }, "issues": { "deprecate_smart_sensor": { - "description": "Unifikovan\u00fd sn\u00edma\u010d \u201eZisten\u00fd objekt\u201c pre inteligentn\u00e9 detekcie je teraz zastaran\u00fd. Bol nahraden\u00fd samostatn\u00fdmi bin\u00e1rnymi sn\u00edma\u010dmi inteligentnej detekcie pre ka\u017ed\u00fd typ inteligentnej detekcie. \n\nNi\u017e\u0161ie s\u00fa uveden\u00e9 zisten\u00e9 automatiz\u00e1cie alebo skripty, ktor\u00e9 pou\u017e\u00edvaj\u00fa jednu alebo viacero zastaran\u00fdch ent\u00edt:\n{items}\nVy\u0161\u0161ie uveden\u00fd zoznam m\u00f4\u017ee by\u0165 ne\u00fapln\u00fd a nezah\u0155\u0148a pou\u017eitie \u0161abl\u00f3n v informa\u010dn\u00fdch paneloch. Pod\u013ea toho aktualizujte v\u0161etky \u0161abl\u00f3ny, automatiz\u00e1cie alebo skripty.", + "description": "Unifikovan\u00fd sn\u00edma\u010d \"Zisten\u00fd objekt\" pre inteligentn\u00e9 detekcie je teraz zastaran\u00fd. Bol nahraden\u00fd samostatn\u00fdmi bin\u00e1rnymi sn\u00edma\u010dmi inteligentnej detekcie pre ka\u017ed\u00fd typ inteligentnej detekcie. \n\nNi\u017e\u0161ie s\u00fa uveden\u00e9 zisten\u00e9 automatiz\u00e1cie alebo skripty, ktor\u00e9 pou\u017e\u00edvaj\u00fa jednu alebo viacero zastaran\u00fdch ent\u00edt:\n{items}\nVy\u0161\u0161ie uveden\u00fd zoznam m\u00f4\u017ee by\u0165 ne\u00fapln\u00fd a nezah\u0155\u0148a pou\u017eitie \u0161abl\u00f3n v informa\u010dn\u00fdch paneloch. Pod\u013ea toho aktualizujte v\u0161etky \u0161abl\u00f3ny, automatiz\u00e1cie alebo skripty.", "title": "Inteligentn\u00fd detek\u010dn\u00fd senzor zastaran\u00fd" }, "deprecated_service_set_doorbell_message": { "fix_flow": { "step": { "confirm": { - "description": "Slu\u017eba \u201eunifiprotect.set_doorbell_message\u201c je zastaran\u00e1 v prospech novej entity Doorbell Text pridanej do ka\u017ed\u00e9ho zariadenia Doorbell. Bude odstr\u00e1nen\u00e1 vo verzii 2023.3.0. Aktualizujte, aby ste mohli pou\u017e\u00edva\u0165 slu\u017ebu [`text.set_value`]({link}).", + "description": "Slu\u017eba `unifiprotect.set_doorbell_message` je zastaran\u00e1 v prospech novej entity Doorbell Text pridanej do ka\u017ed\u00e9ho zariadenia Doorbell. Bude odstr\u00e1nen\u00e1 vo verzii 2023.3.0. Aktualizujte, aby ste mohli pou\u017e\u00edva\u0165 slu\u017ebu [`text.set_value`]({link}).", "title": "set_doorbell_message je zastaran\u00fd" } } diff --git a/homeassistant/components/unifiprotect/translations/tr.json b/homeassistant/components/unifiprotect/translations/tr.json index d26f6af41ce..6c720dd4453 100644 --- a/homeassistant/components/unifiprotect/translations/tr.json +++ b/homeassistant/components/unifiprotect/translations/tr.json @@ -16,7 +16,7 @@ "password": "Parola", "username": "Kullan\u0131c\u0131 Ad\u0131" }, - "description": "{name} ( {ip_address} ) kurulumu yapmak istiyor musunuz? Oturum a\u00e7mak i\u00e7in UniFi OS Konsolunuzda olu\u015fturulmu\u015f yerel bir kullan\u0131c\u0131ya ihtiyac\u0131n\u0131z olacak. Ubiquiti Bulut Kullan\u0131c\u0131lar\u0131 \u00e7al\u0131\u015fmayacakt\u0131r. Daha fazla bilgi i\u00e7in: {local_user_documentation_url}", + "description": "{name} ( {ip_address} ) kurmak istiyor musunuz? Oturum a\u00e7mak i\u00e7in UniFi OS Konsolunuzda olu\u015fturulmu\u015f yerel bir kullan\u0131c\u0131ya ihtiyac\u0131n\u0131z olacak. Ubiquiti Bulut Kullan\u0131c\u0131lar\u0131 \u00e7al\u0131\u015fmayacakt\u0131r. Daha fazla bilgi i\u00e7in: {local_user_documentation_url}", "title": "UniFi Protect Ke\u015ffedildi" }, "reauth_confirm": { @@ -41,11 +41,57 @@ } } }, + "entity": { + "sensor": { + "license_plate": { + "state": { + "none": "Temiz" + } + } + } + }, + "issues": { + "deprecate_smart_sensor": { + "description": "Ak\u0131ll\u0131 alg\u0131lamalar i\u00e7in birle\u015fik \"Alg\u0131lanan Nesne\" sens\u00f6r\u00fc art\u0131k kullan\u0131mdan kald\u0131r\u0131lm\u0131\u015ft\u0131r. Her ak\u0131ll\u0131 alg\u0131lama t\u00fcr\u00fc i\u00e7in ayr\u0131 ak\u0131ll\u0131 alg\u0131lama ikili sens\u00f6rleri ile de\u011fi\u015ftirilmi\u015ftir. \n\n Kullan\u0131mdan kald\u0131r\u0131lan varl\u0131klardan birini veya daha fazlas\u0131n\u0131 kullanan alg\u0131lanan otomasyonlar veya komut dosyalar\u0131 a\u015fa\u011f\u0131dad\u0131r:\n {items}\n Yukar\u0131daki liste eksik olabilir ve panolar\u0131n i\u00e7inde herhangi bir \u015fablon kullan\u0131m\u0131 i\u00e7ermez. L\u00fctfen t\u00fcm \u015fablonlar\u0131, otomasyonlar\u0131 veya komut dosyalar\u0131n\u0131 uygun \u015fekilde g\u00fcncelleyin.", + "title": "Ak\u0131ll\u0131 Alg\u0131lama Sens\u00f6r\u00fc Kullan\u0131mdan Kald\u0131r\u0131ld\u0131" + }, + "deprecated_service_set_doorbell_message": { + "fix_flow": { + "step": { + "confirm": { + "description": "\"unifiprotect.set_doorbell_message\" hizmeti, her Kap\u0131 Zili cihaz\u0131na eklenen yeni Kap\u0131 Zili Metni varl\u0131\u011f\u0131 lehine kullan\u0131mdan kald\u0131r\u0131lm\u0131\u015ft\u0131r. v2023.3.0'da kald\u0131r\u0131lacakt\u0131r. L\u00fctfen [`text.set_value` hizmetini]( {link} ) kullanmak i\u00e7in g\u00fcncelleyin.", + "title": "set_doorbell_message Kullan\u0131mdan Kald\u0131r\u0131ld\u0131" + } + } + }, + "title": "set_doorbell_message Kullan\u0131mdan Kald\u0131r\u0131ld\u0131" + }, + "ea_setup_failed": { + "description": "Erken Eri\u015fim s\u00fcr\u00fcm\u00fc olan UniFi Protect'in v {version} s\u00fcr\u00fcm\u00fcn\u00fc kullan\u0131yorsunuz. Entegrasyon y\u00fcklenmeye \u00e7al\u0131\u015f\u0131l\u0131rken kurtar\u0131lamaz bir hata olu\u015ftu. Entegrasyonu kullanmaya devam etmek i\u00e7in l\u00fctfen UniFi Protect'in [kararl\u0131 bir s\u00fcr\u00fcm\u00fcne ge\u00e7in](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect). \n\n Hata: {error}", + "title": "Erken Eri\u015fim s\u00fcr\u00fcm\u00fcn\u00fc kullan\u0131rken kurulum hatas\u0131" + }, + "ea_warning": { + "fix_flow": { + "step": { + "confirm": { + "description": "UniFi Protect'in desteklenmeyen s\u00fcr\u00fcmlerini \u00e7al\u0131\u015ft\u0131rmak istedi\u011finizden emin misiniz? Bu, Ev Asistan\u0131 entegrasyonunuzun bozulmas\u0131na neden olabilir.", + "title": "v {version} , bir Erken Eri\u015fim s\u00fcr\u00fcm\u00fcd\u00fcr" + }, + "start": { + "description": "Erken Eri\u015fim s\u00fcr\u00fcm\u00fc olan UniFi Protect'in v {version} s\u00fcr\u00fcm\u00fcn\u00fc kullan\u0131yorsunuz. [Erken Eri\u015fim s\u00fcr\u00fcmleri Home Assistant taraf\u0131ndan desteklenmez](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) ve en k\u0131sa s\u00fcrede kararl\u0131 bir s\u00fcr\u00fcme geri d\u00f6nmeniz \u00f6nerilir. m\u00fcmk\u00fcn. \n\n Bu formu g\u00f6ndererek, [UniFi Protect'in s\u00fcr\u00fcm\u00fcn\u00fc d\u00fc\u015f\u00fcrd\u00fcn\u00fcz](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) veya UniFi Protect'in desteklenmeyen bir s\u00fcr\u00fcm\u00fcn\u00fc \u00e7al\u0131\u015ft\u0131rmay\u0131 kabul etmi\u015f olursunuz.", + "title": "v {version} , bir Erken Eri\u015fim s\u00fcr\u00fcm\u00fcd\u00fcr" + } + } + }, + "title": "UniFi Protect v {version} , Erken Eri\u015fim s\u00fcr\u00fcm\u00fcd\u00fcr" + } + }, "options": { "step": { "init": { "data": { "all_updates": "Ger\u00e7ek zamanl\u0131 \u00f6l\u00e7\u00fcmler (UYARI: CPU kullan\u0131m\u0131n\u0131 b\u00fcy\u00fck \u00f6l\u00e7\u00fcde art\u0131r\u0131r)", + "allow_ea": "Protect'in Erken Eri\u015fim s\u00fcr\u00fcmlerine izin ver (UYARI: Entegrasyonunuzu desteklenmiyor olarak i\u015faretler)", "disable_rtsp": "RTSP ak\u0131\u015f\u0131n\u0131 devre d\u0131\u015f\u0131 b\u0131rak\u0131n", "max_media": "Medya Taray\u0131c\u0131 i\u00e7in y\u00fcklenecek maksimum olay say\u0131s\u0131 (RAM kullan\u0131m\u0131n\u0131 art\u0131r\u0131r)", "override_connection_host": "Ba\u011flant\u0131 Ana Bilgisayar\u0131n\u0131 Ge\u00e7ersiz K\u0131l" diff --git a/homeassistant/components/unifiprotect/translations/uk.json b/homeassistant/components/unifiprotect/translations/uk.json new file mode 100644 index 00000000000..3aa9b2029e2 --- /dev/null +++ b/homeassistant/components/unifiprotect/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "step": { + "discovery_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/update/__init__.py b/homeassistant/components/update/__init__.py index f8d00300d81..09207665748 100644 --- a/homeassistant/components/update/__init__.py +++ b/homeassistant/components/update/__init__.py @@ -326,16 +326,16 @@ class UpdateEntity(RestoreEntity): async def async_release_notes(self) -> str | None: """Return full release notes. - This is suitable for a long changelog that does not fit in the release_summary property. - The returned string can contain markdown. + This is suitable for a long changelog that does not fit in the release_summary + property. The returned string can contain markdown. """ return await self.hass.async_add_executor_job(self.release_notes) def release_notes(self) -> str | None: """Return full release notes. - This is suitable for a long changelog that does not fit in the release_summary property. - The returned string can contain markdown. + This is suitable for a long changelog that does not fit in the release_summary + property. The returned string can contain markdown. """ raise NotImplementedError() diff --git a/homeassistant/components/update/recorder.py b/homeassistant/components/update/recorder.py index 1b22360761f..408937c4f31 100644 --- a/homeassistant/components/update/recorder.py +++ b/homeassistant/components/update/recorder.py @@ -9,5 +9,5 @@ from .const import ATTR_IN_PROGRESS, ATTR_RELEASE_SUMMARY @callback def exclude_attributes(hass: HomeAssistant) -> set[str]: - """Exclude large and chatty update attributes from being recorded in the database.""" + """Exclude large and chatty update attributes from being recorded.""" return {ATTR_ENTITY_PICTURE, ATTR_IN_PROGRESS, ATTR_RELEASE_SUMMARY} diff --git a/homeassistant/components/upnp/coordinator.py b/homeassistant/components/upnp/coordinator.py index 18d37b4a388..2820a584632 100644 --- a/homeassistant/components/upnp/coordinator.py +++ b/homeassistant/components/upnp/coordinator.py @@ -1,6 +1,5 @@ """UPnP/IGD coordinator.""" -from collections.abc import Mapping from datetime import timedelta from typing import Any @@ -35,7 +34,7 @@ class UpnpDataUpdateCoordinator(DataUpdateCoordinator): update_interval=update_interval, ) - async def _async_update_data(self) -> Mapping[str, Any]: + async def _async_update_data(self) -> dict[str, Any]: """Update data.""" try: return await self.device.async_get_data() diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 61784749c6f..ed06a9eb363 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -1,7 +1,6 @@ """Home Assistant representation of an UPnP/IGD.""" from __future__ import annotations -from collections.abc import Mapping from functools import partial from ipaddress import ip_address from typing import Any @@ -135,7 +134,7 @@ class Device: """Get string representation.""" return f"IGD Device: {self.name}/{self.udn}::{self.device_type}" - async def async_get_data(self) -> Mapping[str, Any]: + async def async_get_data(self) -> dict[str, Any]: """Get all data from device.""" _LOGGER.debug("Getting data for device: %s", self) igd_state = await self._igd_device.async_get_traffic_and_status_data() diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index ff683c883bb..c5a872cd207 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.33.0", "getmac==0.8.2"], + "requirements": ["async-upnp-client==0.33.1", "getmac==0.8.2"], "dependencies": ["network", "ssdp"], "codeowners": ["@StevenLooman"], "ssdp": [ diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index e5b09d1a398..97741c0dbdd 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -178,7 +178,8 @@ class UpnpSensor(UpnpEntity, SensorEntity): @property def native_value(self) -> str | None: """Return the state of the device.""" - value = self.coordinator.data[self.entity_description.value_key] - if value is None: + if (key := self.entity_description.value_key) is None: + return None + if (value := self.coordinator.data[key]) is None: return None return format(value, self.entity_description.format) diff --git a/homeassistant/components/upnp/translations/hu.json b/homeassistant/components/upnp/translations/hu.json index 141d5ae8d8d..893fb5f3d1a 100644 --- a/homeassistant/components/upnp/translations/hu.json +++ b/homeassistant/components/upnp/translations/hu.json @@ -7,7 +7,7 @@ }, "error": { "one": "hiba", - "other": "" + "other": "hiba" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/upnp/translations/lt.json b/homeassistant/components/upnp/translations/lt.json new file mode 100644 index 00000000000..c82e4b7b7e6 --- /dev/null +++ b/homeassistant/components/upnp/translations/lt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u012erenginys jau sukonfig\u016bruotas" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/lv.json b/homeassistant/components/upnp/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/upnp/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/uk.json b/homeassistant/components/upnp/translations/uk.json index 870c3d38ffd..8333e8e6964 100644 --- a/homeassistant/components/upnp/translations/uk.json +++ b/homeassistant/components/upnp/translations/uk.json @@ -9,6 +9,11 @@ "step": { "ssdp_confirm": { "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0446\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 UPnP / IGD?" + }, + "user": { + "data": { + "unique_id": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } } } } diff --git a/homeassistant/components/uptime/config_flow.py b/homeassistant/components/uptime/config_flow.py index 6ff36ee34b1..edbe6d86f38 100644 --- a/homeassistant/components/uptime/config_flow.py +++ b/homeassistant/components/uptime/config_flow.py @@ -6,10 +6,9 @@ from typing import Any import voluptuous as vol from homeassistant.config_entries import ConfigFlow -from homeassistant.const import CONF_NAME from homeassistant.data_entry_flow import FlowResult -from .const import DEFAULT_NAME, DOMAIN +from .const import DOMAIN class UptimeConfigFlow(ConfigFlow, domain=DOMAIN): @@ -26,12 +25,8 @@ class UptimeConfigFlow(ConfigFlow, domain=DOMAIN): if user_input is not None: return self.async_create_entry( - title=user_input.get(CONF_NAME, DEFAULT_NAME), + title="Uptime", data={}, ) return self.async_show_form(step_id="user", data_schema=vol.Schema({})) - - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Handle import from configuration.yaml.""" - return await self.async_step_user(user_input) diff --git a/homeassistant/components/uptime/const.py b/homeassistant/components/uptime/const.py index bbce8021474..559e0f62273 100644 --- a/homeassistant/components/uptime/const.py +++ b/homeassistant/components/uptime/const.py @@ -5,5 +5,3 @@ from homeassistant.const import Platform DOMAIN: Final = "uptime" PLATFORMS: Final = [Platform.SENSOR] - -DEFAULT_NAME: Final = "Uptime" diff --git a/homeassistant/components/uptime/sensor.py b/homeassistant/components/uptime/sensor.py index ec65051867a..f3b215356e5 100644 --- a/homeassistant/components/uptime/sensor.py +++ b/homeassistant/components/uptime/sensor.py @@ -1,60 +1,15 @@ """Platform to retrieve uptime for Home Assistant.""" from __future__ import annotations -import voluptuous as vol - -from homeassistant.components.sensor import ( - PLATFORM_SCHEMA, - SensorDeviceClass, - SensorEntity, -) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT +from homeassistant.components.sensor import SensorDeviceClass, SensorEntity +from homeassistant.config_entries import ConfigEntry 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.issue_registry import IssueSeverity, async_create_issue -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType import homeassistant.util.dt as dt_util -from .const import DEFAULT_NAME, DOMAIN - -PLATFORM_SCHEMA = vol.All( - cv.removed(CONF_UNIT_OF_MEASUREMENT, raise_if_present=False), - PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Remove(CONF_UNIT_OF_MEASUREMENT): cv.string, - }, - ), -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the uptime sensor platform.""" - async_create_issue( - hass, - DOMAIN, - "removed_yaml", - breaks_in_ha_version="2022.12.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="removed_yaml", - ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config, - ) - ) +from .const import DOMAIN async def async_setup_entry( diff --git a/homeassistant/components/uptime/translations/tr.json b/homeassistant/components/uptime/translations/tr.json index 72132e5e979..707154abee2 100644 --- a/homeassistant/components/uptime/translations/tr.json +++ b/homeassistant/components/uptime/translations/tr.json @@ -5,7 +5,7 @@ }, "step": { "user": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" } } }, diff --git a/homeassistant/components/uptimerobot/__init__.py b/homeassistant/components/uptimerobot/__init__.py index 00ee0889c3d..359e4c6831a 100644 --- a/homeassistant/components/uptimerobot/__init__.py +++ b/homeassistant/components/uptimerobot/__init__.py @@ -84,9 +84,9 @@ class UptimeRobotDataUpdateCoordinator(DataUpdateCoordinator[list[UptimeRobotMon raise ConfigEntryAuthFailed(exception) from exception except UptimeRobotException as exception: raise UpdateFailed(exception) from exception - else: - if response.status != API_ATTR_OK: - raise UpdateFailed(response.error.message) + + if response.status != API_ATTR_OK: + raise UpdateFailed(response.error.message) monitors: list[UptimeRobotMonitor] = response.data diff --git a/homeassistant/components/uptimerobot/translations/el.json b/homeassistant/components/uptimerobot/translations/el.json index e0ee9b2a39c..2f873c67ca4 100644 --- a/homeassistant/components/uptimerobot/translations/el.json +++ b/homeassistant/components/uptimerobot/translations/el.json @@ -18,14 +18,14 @@ "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 \u03bd\u03ad\u03bf \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 UptimeRobot", + "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03bd\u03ad\u03bf \u00ab\u03ba\u03cd\u03c1\u03b9\u03bf\u00bb \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b1\u03c0\u03cc \u03c4\u03bf UptimeRobot", "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 UptimeRobot" + "description": "\u03a0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b5 \u03c4\u03bf \u00ab\u03ba\u03cd\u03c1\u03b9\u03bf\u00bb \u03ba\u03bb\u03b5\u03b9\u03b4\u03af API \u03b1\u03c0\u03cc \u03c4\u03bf UptimeRobot" } } }, diff --git a/homeassistant/components/uptimerobot/translations/sensor.uk.json b/homeassistant/components/uptimerobot/translations/sensor.uk.json new file mode 100644 index 00000000000..9b98369a859 --- /dev/null +++ b/homeassistant/components/uptimerobot/translations/sensor.uk.json @@ -0,0 +1,7 @@ +{ + "state": { + "uptimerobot__monitor_status": { + "pause": "\u041f\u0430\u0443\u0437\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/uptimerobot/translations/sk.json b/homeassistant/components/uptimerobot/translations/sk.json index 53e9f7caf96..d552429a99d 100644 --- a/homeassistant/components/uptimerobot/translations/sk.json +++ b/homeassistant/components/uptimerobot/translations/sk.json @@ -9,7 +9,7 @@ "error": { "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_api_key": "Neplatn\u00fd API k\u013e\u00fa\u010d", - "not_main_key": "Bol zisten\u00fd nespr\u00e1vny typ k\u013e\u00fa\u010da API, pou\u017eite \u201emain\u201c k\u013e\u00fa\u010d API", + "not_main_key": "Bol zisten\u00fd nespr\u00e1vny typ k\u013e\u00fa\u010da API, pou\u017eite `main` k\u013e\u00fa\u010d API", "reauth_failed_matching_account": "Zadan\u00fd k\u013e\u00fa\u010d API sa nezhoduje s ID \u00fa\u010dtu pre existuj\u00facu konfigur\u00e1ciu.", "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, @@ -18,14 +18,14 @@ "data": { "api_key": "API k\u013e\u00fa\u010d" }, - "description": "Mus\u00edte doda\u0165 nov\u00fd \u201ehlavn\u00fd\u201c k\u013e\u00fa\u010d API od UptimeRobot", + "description": "Mus\u00edte doda\u0165 nov\u00fd `hlavn\u00fd` k\u013e\u00fa\u010d API od UptimeRobot", "title": "Znova overi\u0165 integr\u00e1ciu" }, "user": { "data": { "api_key": "API k\u013e\u00fa\u010d" }, - "description": "Mus\u00edte doda\u0165 \u201ehlavn\u00fd\u201c k\u013e\u00fa\u010d API od UptimeRobot" + "description": "Mus\u00edte doda\u0165 `hlavn\u00fd` k\u013e\u00fa\u010d API od UptimeRobot" } } }, diff --git a/homeassistant/components/uptimerobot/translations/tr.json b/homeassistant/components/uptimerobot/translations/tr.json index 8f7291d57fd..4065a2c93eb 100644 --- a/homeassistant/components/uptimerobot/translations/tr.json +++ b/homeassistant/components/uptimerobot/translations/tr.json @@ -28,5 +28,18 @@ "description": "UptimeRobot'tan 'ana' API anahtar\u0131n\u0131 sa\u011flaman\u0131z gerekiyor" } } + }, + "entity": { + "sensor": { + "monitor_status": { + "state": { + "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/usgs_earthquakes_feed/manifest.json b/homeassistant/components/usgs_earthquakes_feed/manifest.json index ee37381a6fa..7a2b065de73 100644 --- a/homeassistant/components/usgs_earthquakes_feed/manifest.json +++ b/homeassistant/components/usgs_earthquakes_feed/manifest.json @@ -2,7 +2,7 @@ "domain": "usgs_earthquakes_feed", "name": "U.S. Geological Survey Earthquake Hazards (USGS)", "documentation": "https://www.home-assistant.io/integrations/usgs_earthquakes_feed", - "requirements": ["aio_geojson_usgs_earthquakes==0.1"], + "requirements": ["aio_geojson_usgs_earthquakes==0.2"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling", "loggers": ["aio_geojson_usgs_earthquakes"], diff --git a/homeassistant/components/utility_meter/config_flow.py b/homeassistant/components/utility_meter/config_flow.py index 5424d8a55ad..c1f82e902d2 100644 --- a/homeassistant/components/utility_meter/config_flow.py +++ b/homeassistant/components/utility_meter/config_flow.py @@ -35,15 +35,15 @@ from .const import ( ) METER_TYPES = [ - selector.SelectOptionDict(value="none", label="No cycle"), - selector.SelectOptionDict(value=QUARTER_HOURLY, label="Every 15 minutes"), - selector.SelectOptionDict(value=HOURLY, label="Hourly"), - selector.SelectOptionDict(value=DAILY, label="Daily"), - selector.SelectOptionDict(value=WEEKLY, label="Weekly"), - selector.SelectOptionDict(value=MONTHLY, label="Monthly"), - selector.SelectOptionDict(value=BIMONTHLY, label="Every two months"), - selector.SelectOptionDict(value=QUARTERLY, label="Quarterly"), - selector.SelectOptionDict(value=YEARLY, label="Yearly"), + "none", + QUARTER_HOURLY, + HOURLY, + DAILY, + WEEKLY, + MONTHLY, + BIMONTHLY, + QUARTERLY, + YEARLY, ] @@ -74,7 +74,9 @@ CONFIG_SCHEMA = vol.Schema( selector.EntitySelectorConfig(domain=SENSOR_DOMAIN), ), vol.Required(CONF_METER_TYPE): selector.SelectSelector( - selector.SelectSelectorConfig(options=METER_TYPES), + selector.SelectSelectorConfig( + options=METER_TYPES, translation_key=CONF_METER_TYPE + ), ), vol.Required(CONF_METER_OFFSET, default=0): selector.NumberSelector( selector.NumberSelectorConfig( diff --git a/homeassistant/components/utility_meter/strings.json b/homeassistant/components/utility_meter/strings.json index 35a35b7f2db..e9f8e7f2505 100644 --- a/homeassistant/components/utility_meter/strings.json +++ b/homeassistant/components/utility_meter/strings.json @@ -31,5 +31,20 @@ } } } + }, + "selector": { + "cycle": { + "options": { + "none": "No cycle", + "quarter-hourly": "Every 15 minutes", + "hourly": "Hourly", + "daily": "Daily", + "weekly": "Weekly", + "monthly": "Monthly", + "bimonthly": "Every two months", + "quarterly": "Quarterly", + "yearly": "Yearly" + } + } } } diff --git a/homeassistant/components/utility_meter/translations/bg.json b/homeassistant/components/utility_meter/translations/bg.json index 35cfa0ad1d7..96657f41a82 100644 --- a/homeassistant/components/utility_meter/translations/bg.json +++ b/homeassistant/components/utility_meter/translations/bg.json @@ -7,5 +7,20 @@ } } } + }, + "selector": { + "cycle": { + "options": { + "bimonthly": "\u041d\u0430 \u0432\u0441\u0435\u043a\u0438 \u0434\u0432\u0430 \u043c\u0435\u0441\u0435\u0446\u0430", + "daily": "\u0415\u0436\u0435\u0434\u043d\u0435\u0432\u043d\u043e", + "hourly": "\u041f\u043e\u0447\u0430\u0441\u043e\u0432\u043e", + "monthly": "\u041c\u0435\u0441\u0435\u0447\u043d\u043e", + "none": "\u0411\u0435\u0437 \u0446\u0438\u043a\u044a\u043b", + "quarter-hourly": "\u041d\u0430 \u0432\u0441\u0435\u043a\u0438 15 \u043c\u0438\u043d\u0443\u0442\u0438", + "quarterly": "\u0422\u0440\u0438\u043c\u0435\u0441\u0435\u0447\u043d\u043e", + "weekly": "\u0421\u0435\u0434\u043c\u0438\u0447\u043d\u043e", + "yearly": "\u0413\u043e\u0434\u0438\u0448\u043d\u043e" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/ca.json b/homeassistant/components/utility_meter/translations/ca.json index d48052eaf33..9969fbe6eaf 100644 --- a/homeassistant/components/utility_meter/translations/ca.json +++ b/homeassistant/components/utility_meter/translations/ca.json @@ -31,5 +31,20 @@ } } }, + "selector": { + "cycle": { + "options": { + "bimonthly": "Cada dos mesos", + "daily": "Di\u00e0riament", + "hourly": "Cada hora", + "monthly": "Mensualment", + "none": "Sense cicle", + "quarter-hourly": "Cada 15 minuts", + "quarterly": "Trimestralment", + "weekly": "Setmanalment", + "yearly": "Anualment" + } + } + }, "title": "Comptador" } \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/de.json b/homeassistant/components/utility_meter/translations/de.json index 798d5616edb..d4270e5b55b 100644 --- a/homeassistant/components/utility_meter/translations/de.json +++ b/homeassistant/components/utility_meter/translations/de.json @@ -31,5 +31,20 @@ } } }, + "selector": { + "cycle": { + "options": { + "bimonthly": "Alle zwei Monate", + "daily": "T\u00e4glich", + "hourly": "St\u00fcndlich", + "monthly": "Monatlich", + "none": "Kein Zyklus", + "quarter-hourly": "Alle 15 Minuten", + "quarterly": "Viertelj\u00e4hrlich", + "weekly": "W\u00f6chentlich", + "yearly": "J\u00e4hrlich" + } + } + }, "title": "Verbrauchsz\u00e4hler" } \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/el.json b/homeassistant/components/utility_meter/translations/el.json index 3264503bab9..6ef731c8b97 100644 --- a/homeassistant/components/utility_meter/translations/el.json +++ b/homeassistant/components/utility_meter/translations/el.json @@ -17,8 +17,8 @@ "offset": "\u0391\u03bd\u03c4\u03b9\u03c3\u03c4\u03ac\u03b8\u03bc\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b7\u03bc\u03ad\u03c1\u03b1\u03c2 \u03bc\u03b7\u03bd\u03b9\u03b1\u03af\u03b1\u03c2 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 \u03c4\u03bf\u03c5 \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae.", "tariffs": "\u039c\u03b9\u03b1 \u03bb\u03af\u03c3\u03c4\u03b1 \u03bc\u03b5 \u03c4\u03b1 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b1 \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03b1, \u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03ae \u03b5\u03ac\u03bd \u03c7\u03c1\u03b5\u03b9\u03ac\u03b6\u03b5\u03c4\u03b1\u03b9 \u03bc\u03cc\u03bd\u03bf \u03ad\u03bd\u03b1 \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03bf." }, - "description": "\u039f \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae \u03ba\u03bf\u03b9\u03bd\u03ae\u03c2 \u03c9\u03c6\u03ad\u03bb\u03b5\u03b9\u03b1\u03c2 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03b9 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03cd\u03b8\u03b7\u03c3\u03b7\u03c2 \u03c4\u03c9\u03bd \u03ba\u03b1\u03c4\u03b1\u03bd\u03b1\u03bb\u03ce\u03c3\u03b5\u03c9\u03bd \u03b4\u03b9\u03b1\u03c6\u03cc\u03c1\u03c9\u03bd \u03b2\u03bf\u03b7\u03b8\u03b7\u03c4\u03b9\u03ba\u03ce\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd (\u03c0.\u03c7. \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1, \u03c6\u03c5\u03c3\u03b9\u03ba\u03cc \u03b1\u03ad\u03c1\u03b9\u03bf, \u03bd\u03b5\u03c1\u03cc, \u03b8\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7) \u03c3\u03b5 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03ae \u03c0\u03b5\u03c1\u03af\u03bf\u03b4\u03bf, \u03c3\u03c5\u03bd\u03ae\u03b8\u03c9\u03c2 \u03bc\u03b7\u03bd\u03b9\u03b1\u03af\u03b1. \u039f \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03c4\u03bf\u03c5 \u03b2\u03bf\u03b7\u03b8\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9 \u03b5\u03c0\u03af\u03c3\u03b7\u03c2 \u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03cc \u03c4\u03b7\u03c2 \u03ba\u03b1\u03c4\u03b1\u03bd\u03ac\u03bb\u03c9\u03c3\u03b7\u03c2 \u03b1\u03bd\u03ac \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03b1.\n \u0397 \u03bc\u03b5\u03c4\u03b1\u03c4\u03cc\u03c0\u03b9\u03c3\u03b7 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae \u03b5\u03c0\u03b9\u03c4\u03c1\u03ad\u03c0\u03b5\u03b9 \u03c4\u03b7 \u03bc\u03b5\u03c4\u03b1\u03c4\u03cc\u03c0\u03b9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b7\u03bc\u03ad\u03c1\u03b1\u03c2 \u03c4\u03b7\u03c2 \u03bc\u03b7\u03bd\u03b9\u03b1\u03af\u03b1\u03c2 \u03b5\u03c0\u03b1\u03bd\u03b1\u03c6\u03bf\u03c1\u03ac\u03c2 \u03c4\u03bf\u03c5 \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae.\n \u03a4\u03b1 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b1 \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03bb\u03af\u03c3\u03c4\u03b1 \u03bc\u03b5 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b1 \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03b1 \u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b1 \u03bc\u03b5 \u03ba\u03cc\u03bc\u03bc\u03b1\u03c4\u03b1, \u03b1\u03c6\u03ae\u03c3\u03c4\u03b5 \u03ba\u03b5\u03bd\u03ae \u03b5\u03ac\u03bd \u03c7\u03c1\u03b5\u03b9\u03ac\u03b6\u03b5\u03c4\u03b1\u03b9 \u03bc\u03cc\u03bd\u03bf \u03ad\u03bd\u03b1 \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03bf.", - "title": "\u039d\u03ad\u03bf\u03c2 \u03b2\u03bf\u03b7\u03b8\u03b7\u03c4\u03b9\u03ba\u03cc\u03c2 \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae\u03c2" + "description": "\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03c4\u03b5 \u03ad\u03bd\u03b1\u03bd \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1 \u03c0\u03bf\u03c5 \u03c0\u03b1\u03c1\u03b1\u03ba\u03bf\u03bb\u03bf\u03c5\u03b8\u03b5\u03af \u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03b1\u03bd\u03ac\u03bb\u03c9\u03c3\u03b7 \u03b4\u03b9\u03b1\u03c6\u03cc\u03c1\u03c9\u03bd \u03b2\u03bf\u03b7\u03b8\u03b7\u03c4\u03b9\u03ba\u03ce\u03bd \u03c0\u03c1\u03bf\u03b3\u03c1\u03b1\u03bc\u03bc\u03ac\u03c4\u03c9\u03bd (\u03c0.\u03c7. \u03b5\u03bd\u03ad\u03c1\u03b3\u03b5\u03b9\u03b1, \u03c6\u03c5\u03c3\u03b9\u03ba\u03cc \u03b1\u03ad\u03c1\u03b9\u03bf, \u03bd\u03b5\u03c1\u03cc, \u03b8\u03ad\u03c1\u03bc\u03b1\u03bd\u03c3\u03b7) \u03c3\u03b5 \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b1\u03bc\u03bf\u03c1\u03c6\u03c9\u03bc\u03ad\u03bd\u03b7 \u03c7\u03c1\u03bf\u03bd\u03b9\u03ba\u03ae \u03c0\u03b5\u03c1\u03af\u03bf\u03b4\u03bf, \u03c3\u03c5\u03bd\u03ae\u03b8\u03c9\u03c2 \u03bc\u03b7\u03bd\u03b9\u03b1\u03af\u03b1. \u039f \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae \u03ba\u03bf\u03b9\u03bd\u03ae\u03c2 \u03c7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9 \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ac \u03c4\u03bf\u03bd \u03b4\u03b9\u03b1\u03c7\u03c9\u03c1\u03b9\u03c3\u03bc\u03cc \u03c4\u03b7\u03c2 \u03ba\u03b1\u03c4\u03b1\u03bd\u03ac\u03bb\u03c9\u03c3\u03b7\u03c2 \u03b1\u03bd\u03ac \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03b1, \u03c3\u03b5 \u03b1\u03c5\u03c4\u03ae\u03bd \u03c4\u03b7\u03bd \u03c0\u03b5\u03c1\u03af\u03c0\u03c4\u03c9\u03c3\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03b5\u03af\u03c4\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c2 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b1\u03c2 \u03b3\u03b9\u03b1 \u03ba\u03ac\u03b8\u03b5 \u03c4\u03b9\u03bc\u03bf\u03bb\u03cc\u03b3\u03b9\u03bf \u03ba\u03b1\u03b8\u03ce\u03c2 \u03ba\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03b5\u03c0\u03b9\u03bb\u03b5\u03b3\u03bc\u03ad\u03bd\u03b7 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ae \u03c4\u03b7\u03c2 \u03c4\u03c1\u03ad\u03c7\u03bf\u03c5\u03c3\u03b1\u03c2 \u03c7\u03c1\u03ad\u03c9\u03c3\u03b7\u03c2.", + "title": "\u03a0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03b2\u03bf\u03b7\u03b8\u03b7\u03c4\u03b9\u03ba\u03bf\u03cd \u03bc\u03b5\u03c4\u03c1\u03b7\u03c4\u03ae" } } }, diff --git a/homeassistant/components/utility_meter/translations/en.json b/homeassistant/components/utility_meter/translations/en.json index d5dc7f18ddd..bf2092867f0 100644 --- a/homeassistant/components/utility_meter/translations/en.json +++ b/homeassistant/components/utility_meter/translations/en.json @@ -31,5 +31,20 @@ } } }, + "selector": { + "cycle": { + "options": { + "bimonthly": "Every two months", + "daily": "Daily", + "hourly": "Hourly", + "monthly": "Monthly", + "none": "No cycle", + "quarter-hourly": "Every 15 minutes", + "quarterly": "Quarterly", + "weekly": "Weekly", + "yearly": "Yearly" + } + } + }, "title": "Utility Meter" } \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/et.json b/homeassistant/components/utility_meter/translations/et.json index 579933130fb..d8317f40e28 100644 --- a/homeassistant/components/utility_meter/translations/et.json +++ b/homeassistant/components/utility_meter/translations/et.json @@ -31,5 +31,20 @@ } } }, + "selector": { + "cycle": { + "options": { + "bimonthly": "Iga kahe kuu tagant", + "daily": "P\u00e4evas", + "hourly": "Tunnis", + "monthly": "Kuus", + "none": "Ts\u00fckkel puudub", + "quarter-hourly": "Iga 15 minuti j\u00e4rel", + "quarterly": "Kvartalis", + "weekly": "N\u00e4dalas", + "yearly": "Aastas" + } + } + }, "title": "Kommunaalarvesti" } \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/pl.json b/homeassistant/components/utility_meter/translations/pl.json index 9be6f68384b..4642d2e2b72 100644 --- a/homeassistant/components/utility_meter/translations/pl.json +++ b/homeassistant/components/utility_meter/translations/pl.json @@ -13,7 +13,7 @@ }, "data_description": { "delta_values": "W\u0142\u0105cz, je\u015bli warto\u015bci \u017ar\u00f3d\u0142owe s\u0105 warto\u015bciami delta od ostatniego odczytu, a nie warto\u015bciami bezwzgl\u0119dnymi.", - "net_consumption": "W\u0142\u0105cz, je\u015bli \u017ar\u00f3d\u0142em jest licznik netto, co oznacza, \u017ce jego warto\u015bci mog\u0105 si\u0119 zar\u00f3wno zwi\u0119ksza\u0107 jak i zmniejsza\u0107.", + "net_consumption": "W\u0142\u0105cz, je\u015bli \u017ar\u00f3d\u0142em jest licznik netto, co oznacza, \u017ce jego warto\u015bci mog\u0105 si\u0119 zar\u00f3wno zwi\u0119ksza\u0107, jak i zmniejsza\u0107.", "offset": "Przesuni\u0119cie dnia miesi\u0119cznego zerowania licznika.", "tariffs": "Lista obs\u0142ugiwanych taryf. Pozostaw puste, je\u015bli potrzebna jest tylko jedna taryfa." }, @@ -31,5 +31,20 @@ } } }, + "selector": { + "cycle": { + "options": { + "bimonthly": "dwumiesi\u0119czny", + "daily": "dzienny", + "hourly": "godzinowy", + "monthly": "miesi\u0119czny", + "none": "bez cyklu", + "quarter-hourly": "15 minut", + "quarterly": "kwartalny", + "weekly": "miesi\u0119czny", + "yearly": "roczny" + } + } + }, "title": "Licznik medi\u00f3w" } \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/ru.json b/homeassistant/components/utility_meter/translations/ru.json index 3eda01a8116..3bd29580598 100644 --- a/homeassistant/components/utility_meter/translations/ru.json +++ b/homeassistant/components/utility_meter/translations/ru.json @@ -31,5 +31,20 @@ } } }, + "selector": { + "cycle": { + "options": { + "bimonthly": "\u041a\u0430\u0436\u0434\u044b\u0435 \u0434\u0432\u0430 \u043c\u0435\u0441\u044f\u0446\u0430", + "daily": "\u041a\u0430\u0436\u0434\u044b\u0439 \u0434\u0435\u043d\u044c", + "hourly": "\u041a\u0430\u0436\u0434\u044b\u0439 \u0447\u0430\u0441", + "monthly": "\u041a\u0430\u0436\u0434\u044b\u0439 \u043c\u0435\u0441\u044f\u0446", + "none": "\u041d\u0435 \u0441\u0431\u0440\u0430\u0441\u044b\u0432\u0430\u0442\u044c", + "quarter-hourly": "\u041a\u0430\u0436\u0434\u044b\u0435 15 \u043c\u0438\u043d\u0443\u0442", + "quarterly": "\u041a\u0430\u0436\u0434\u044b\u0439 \u043a\u0432\u0430\u0440\u0442\u0430\u043b", + "weekly": "\u041a\u0430\u0436\u0434\u0443\u044e \u043d\u0435\u0434\u0435\u043b\u044e", + "yearly": "\u041a\u0430\u0436\u0434\u044b\u0439 \u0433\u043e\u0434" + } + } + }, "title": "\u0421\u0447\u0435\u0442\u0447\u0438\u043a \u043a\u043e\u043c\u043c\u0443\u043d\u0430\u043b\u044c\u043d\u044b\u0445 \u0443\u0441\u043b\u0443\u0433" } \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/uk.json b/homeassistant/components/utility_meter/translations/uk.json new file mode 100644 index 00000000000..c2064ed835b --- /dev/null +++ b/homeassistant/components/utility_meter/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "user": { + "data": { + "cycle": "\u0421\u043a\u0438\u0434\u0430\u043d\u043d\u044f \u0446\u0438\u043a\u043b\u0443 \u043b\u0456\u0447\u0438\u043b\u044c\u043d\u0438\u043a\u0430", + "delta_values": "\u0414\u0435\u043b\u044c\u0442\u0430 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "name": "\u041d\u0430\u0437\u0432\u0430", + "net_consumption": "\u0427\u0438\u0441\u0442\u0435 \u0441\u043f\u043e\u0436\u0438\u0432\u0430\u043d\u043d\u044f", + "offset": "\u0417\u0441\u0443\u0432 \u0441\u043a\u0438\u0434\u0430\u043d\u043d\u044f \u043b\u0456\u0447\u0438\u043b\u044c\u043d\u0438\u043a\u0430", + "source": "\u0414\u0430\u0442\u0447\u0438\u043a \u0432\u0445\u043e\u0434\u0443", + "tariffs": "\u041f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0456 \u0442\u0430\u0440\u0438\u0444\u0438" + }, + "description": "\u0421\u0442\u0432\u043e\u0440\u0456\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a, \u044f\u043a\u0438\u0439 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0454 \u0441\u043f\u043e\u0436\u0438\u0432\u0430\u043d\u043d\u044f \u0440\u0456\u0437\u043d\u0438\u0445 \u043a\u043e\u043c\u0443\u043d\u0430\u043b\u044c\u043d\u0438\u0445 \u043f\u043e\u0441\u043b\u0443\u0433 (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434, \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u0435\u043d\u0435\u0440\u0433\u0456\u0457, \u0433\u0430\u0437\u0443, \u0432\u043e\u0434\u0438, \u043e\u043f\u0430\u043b\u0435\u043d\u043d\u044f) \u043f\u0440\u043e\u0442\u044f\u0433\u043e\u043c \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0456\u043e\u0434\u0443 \u0447\u0430\u0441\u0443, \u0437\u0430\u0437\u0432\u0438\u0447\u0430\u0439 \u0449\u043e\u043c\u0456\u0441\u044f\u0446\u044f. \u0414\u0430\u0442\u0447\u0438\u043a \u043b\u0456\u0447\u0438\u043b\u044c\u043d\u0438\u043a\u0430 \u043a\u043e\u043c\u0443\u043d\u0430\u043b\u044c\u043d\u0438\u0445 \u043f\u043e\u0441\u043b\u0443\u0433 \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u043e \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454 \u0440\u043e\u0437\u043f\u043e\u0434\u0456\u043b \u0441\u043f\u043e\u0436\u0438\u0432\u0430\u043d\u043d\u044f \u0437\u0430 \u0442\u0430\u0440\u0438\u0444\u0430\u043c\u0438, \u0443 \u0446\u044c\u043e\u043c\u0443 \u0432\u0438\u043f\u0430\u0434\u043a\u0443 \u0441\u0442\u0432\u043e\u0440\u044e\u0454\u0442\u044c\u0441\u044f \u043e\u0434\u0438\u043d \u0434\u0430\u0442\u0447\u0438\u043a \u0434\u043b\u044f \u043a\u043e\u0436\u043d\u043e\u0433\u043e \u0442\u0430\u0440\u0438\u0444\u0443, \u0430 \u0442\u0430\u043a\u043e\u0436 \u043e\u0431\u2019\u0454\u043a\u0442 \u0432\u0438\u0431\u043e\u0440\u0443 \u0434\u043b\u044f \u0432\u0438\u0431\u043e\u0440\u0443 \u043f\u043e\u0442\u043e\u0447\u043d\u043e\u0433\u043e \u0442\u0430\u0440\u0438\u0444\u0443." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/utility_meter/translations/zh-Hant.json b/homeassistant/components/utility_meter/translations/zh-Hant.json index dc23f73a89e..536bd50074a 100644 --- a/homeassistant/components/utility_meter/translations/zh-Hant.json +++ b/homeassistant/components/utility_meter/translations/zh-Hant.json @@ -31,5 +31,20 @@ } } }, + "selector": { + "cycle": { + "options": { + "bimonthly": "\u6bcf\u5169\u500b\u6708", + "daily": "\u6bcf\u5929", + "hourly": "\u6bcf\u5c0f\u6642", + "monthly": "\u6bcf\u6708", + "none": "\u4e0d\u91cd\u8907", + "quarter-hourly": "\u6bcf 15 \u5206\u9418", + "quarterly": "\u6bcf\u5b63", + "weekly": "\u6bcf\u9031", + "yearly": "\u6bcf\u5e74" + } + } + }, "title": "\u529f\u8017\u8868" } \ No newline at end of file diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 2417908ecec..cecec49b36b 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -246,9 +246,8 @@ class UnifiVideoCamera(Camera): ( uri for i, uri in enumerate(channel["rtspUris"]) - # pylint: disable=protected-access + # pylint: disable-next=protected-access if re.search(self._nvr._host, uri) - # pylint: enable=protected-access ) ) return uri diff --git a/homeassistant/components/vacuum/translations/he.json b/homeassistant/components/vacuum/translations/he.json index 82e0d073406..622e1245591 100644 --- a/homeassistant/components/vacuum/translations/he.json +++ b/homeassistant/components/vacuum/translations/he.json @@ -6,17 +6,17 @@ }, "condition_type": { "is_cleaning": "{entity_name} \u05de\u05e0\u05e7\u05d4", - "is_docked": "{entity_name} \u05d1\u05ea\u05d7\u05d9\u05e0\u05ea \u05e2\u05d2\u05d9\u05e0\u05d4" + "is_docked": "{entity_name} \u05d1\u05ea\u05d7\u05e0\u05ea \u05e2\u05d2\u05d9\u05e0\u05d4" }, "trigger_type": { - "cleaning": "{entity_name} \u05de\u05ea\u05d7\u05d9\u05dc \u05dc\u05e0\u05e7\u05d5\u05ea", - "docked": "{entity_name} \u05d1\u05ea\u05d7\u05d9\u05e0\u05ea \u05e2\u05d2\u05d9\u05e0\u05d4" + "cleaning": "{entity_name} \u05d4\u05ea\u05d7\u05d9\u05dc \u05dc\u05e0\u05e7\u05d5\u05ea", + "docked": "{entity_name} \u05d7\u05d6\u05e8 \u05dc\u05ea\u05d7\u05e0\u05ea \u05e2\u05d2\u05d9\u05e0\u05d4" } }, "state": { "_": { "cleaning": "\u05de\u05e0\u05e7\u05d4", - "docked": "\u05d1\u05ea\u05d7\u05d9\u05e0\u05ea \u05d8\u05e2\u05d9\u05e0\u05d4", + "docked": "\u05d1\u05ea\u05d7\u05e0\u05ea \u05d8\u05e2\u05d9\u05e0\u05d4", "error": "\u05e9\u05d2\u05d9\u05d0\u05d4", "idle": "\u05de\u05de\u05ea\u05d9\u05df", "off": "\u05db\u05d1\u05d5\u05d9", diff --git a/homeassistant/components/vacuum/translations/lt.json b/homeassistant/components/vacuum/translations/lt.json index 736423f1661..ee89e6e2b03 100644 --- a/homeassistant/components/vacuum/translations/lt.json +++ b/homeassistant/components/vacuum/translations/lt.json @@ -1,7 +1,28 @@ { + "device_automation": { + "action_type": { + "clean": "Leiskite {entity_name} valyti", + "dock": "Leiskite {entity_name} gr\u012f\u017eti \u012f stotel\u0119" + }, + "condition_type": { + "is_cleaning": "{entity_name} valo", + "is_docked": "{entity_name} yra stotel\u0117je" + }, + "trigger_type": { + "cleaning": "{entity_name} prad\u0117jo valyti", + "docked": "{entity_name} stotel\u0117je" + } + }, "state": { "_": { - "docked": "Priparkuotas" + "cleaning": "Valo", + "docked": "Stotel\u0117je", + "error": "Klaida", + "idle": "Laukiama", + "off": "I\u0161jungta", + "on": "\u012ejungta", + "returning": "Gr\u012f\u017eta \u012f stotel\u0119" } - } + }, + "title": "Dulki\u0173 siurblys" } \ No newline at end of file diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 0da64227fd3..ecb0636b029 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -194,3 +194,21 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: shutil.rmtree, hass.config.path(STORAGE_DIR, f"velbuscache-{entry.entry_id}"), ) + + +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old entry.""" + _LOGGER.debug("Migrating from version %s", config_entry.version) + cache_path = hass.config.path(STORAGE_DIR, f"velbuscache-{config_entry.entry_id}/") + if config_entry.version == 1: + # This is the config entry migration for adding the new program selection + # clean the velbusCache + if os.path.isdir(cache_path): + await hass.async_add_executor_job(shutil.rmtree, cache_path) + # set the new version + config_entry.version = 2 + # update the entry + hass.config_entries.async_update_entry(config_entry) + + _LOGGER.debug("Migration to version %s successful", config_entry.version) + return True diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index 32f1f3a500d..e0394e4787c 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -19,7 +19,7 @@ from .const import DOMAIN class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" - VERSION = 1 + VERSION = 2 def __init__(self) -> None: """Initialize the velbus config flow.""" diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 86e67ca7767..9384947cb82 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.10.4"], + "requirements": ["velbus-aio==2022.12.0"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"], "dependencies": ["usb"], diff --git a/homeassistant/components/venstar/sensor.py b/homeassistant/components/venstar/sensor.py index 0d3b66c620e..e20c748f112 100644 --- a/homeassistant/components/venstar/sensor.py +++ b/homeassistant/components/venstar/sensor.py @@ -56,6 +56,14 @@ RUNTIME_ATTRIBUTES = { RUNTIME_OV: "Override", } +SCHEDULE_PARTS: dict[int, str] = { + 0: "morning", + 1: "day", + 2: "evening", + 3: "night", + 255: "inactive", +} + @dataclass class VenstarSensorTypeMixin: @@ -76,31 +84,42 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: - """Set up Vensar device binary_sensors based on a config entry.""" + """Set up Venstar device sensors based on a config entry.""" coordinator: VenstarDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] entities: list[Entity] = [] - if not (sensors := coordinator.client.get_sensor_list()): - return - - for sensor_name in sensors: - entities.extend( - [ - VenstarSensor(coordinator, config_entry, description, sensor_name) - for description in SENSOR_ENTITIES - if coordinator.client.get_sensor(sensor_name, description.key) - is not None - ] - ) - - runtimes = coordinator.runtimes[-1] - for sensor_name in runtimes: - if sensor_name in RUNTIME_DEVICES: - entities.append( - VenstarSensor(coordinator, config_entry, RUNTIME_ENTITY, sensor_name) + if sensors := coordinator.client.get_sensor_list(): + for sensor_name in sensors: + entities.extend( + [ + VenstarSensor(coordinator, config_entry, description, sensor_name) + for description in SENSOR_ENTITIES + if coordinator.client.get_sensor(sensor_name, description.key) + is not None + ] ) - async_add_entities(entities) + runtimes = coordinator.runtimes[-1] + for sensor_name in runtimes: + if sensor_name in RUNTIME_DEVICES: + entities.append( + VenstarSensor( + coordinator, config_entry, RUNTIME_ENTITY, sensor_name + ) + ) + + for description in INFO_ENTITIES: + try: + # just checking if the key exists + coordinator.client.get_info(description.key) + except KeyError: + continue + entities.append( + VenstarSensor(coordinator, config_entry, description, description.key) + ) + + if entities: + async_add_entities(entities) def temperature_unit(coordinator: VenstarDataUpdateCoordinator) -> str: @@ -210,3 +229,17 @@ RUNTIME_ENTITY = VenstarSensorEntityDescription( value_fn=lambda coordinator, sensor_name: coordinator.runtimes[-1][sensor_name], name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} {RUNTIME_ATTRIBUTES[sensor_name]} Runtime", ) + +INFO_ENTITIES: tuple[VenstarSensorEntityDescription, ...] = ( + VenstarSensorEntityDescription( + key="schedulepart", + device_class=SensorDeviceClass.ENUM, + options=list(SCHEDULE_PARTS.values()), + translation_key="schedule_part", + uom_fn=lambda _: None, + value_fn=lambda coordinator, sensor_name: SCHEDULE_PARTS[ + coordinator.client.get_info(sensor_name) + ], + name_fn=lambda coordinator, sensor_name: f"{coordinator.client.name} Schedule Part", + ), +) diff --git a/homeassistant/components/venstar/strings.json b/homeassistant/components/venstar/strings.json index 9b031d94188..a844adc2156 100644 --- a/homeassistant/components/venstar/strings.json +++ b/homeassistant/components/venstar/strings.json @@ -19,5 +19,18 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown": "[%key:common::config_flow::error::unknown%]" } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "morning": "Morning", + "day": "Day", + "evening": "Evening", + "night": "Night", + "inactive": "Inactive" + } + } + } } } diff --git a/homeassistant/components/venstar/translations/ca.json b/homeassistant/components/venstar/translations/ca.json index 5261c6d0481..2dc9699fda4 100644 --- a/homeassistant/components/venstar/translations/ca.json +++ b/homeassistant/components/venstar/translations/ca.json @@ -19,5 +19,18 @@ "title": "Connexi\u00f3 amb term\u00f2stat Venstar" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Dia", + "evening": "Vespre", + "inactive": "Inactiu", + "morning": "Mat\u00ed", + "night": "Nit" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/de.json b/homeassistant/components/venstar/translations/de.json index 01cfd333df7..b6d5642ac7e 100644 --- a/homeassistant/components/venstar/translations/de.json +++ b/homeassistant/components/venstar/translations/de.json @@ -19,5 +19,18 @@ "title": "Mit Venstar-Thermostat verbinden" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Tag", + "evening": "Abend", + "inactive": "Inaktiv", + "morning": "Morgen", + "night": "Nacht" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/el.json b/homeassistant/components/venstar/translations/el.json index f80d57e35c8..67276633772 100644 --- a/homeassistant/components/venstar/translations/el.json +++ b/homeassistant/components/venstar/translations/el.json @@ -19,5 +19,18 @@ "title": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03bc\u03b5 \u03c4\u03bf\u03bd \u03b8\u03b5\u03c1\u03bc\u03bf\u03c3\u03c4\u03ac\u03c4\u03b7 Venstar" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "\u0397\u03bc\u03ad\u03c1\u03b1", + "evening": "\u0391\u03c0\u03cc\u03b3\u03b5\u03c5\u03bc\u03b1", + "inactive": "\u0391\u03b4\u03c1\u03b1\u03bd\u03ae\u03c2", + "morning": "\u03a0\u03c1\u03c9\u03af", + "night": "\u039d\u03cd\u03c7\u03c4\u03b1" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/en.json b/homeassistant/components/venstar/translations/en.json index 8b423713f2c..107f5fd3e91 100644 --- a/homeassistant/components/venstar/translations/en.json +++ b/homeassistant/components/venstar/translations/en.json @@ -19,5 +19,18 @@ "title": "Connect to the Venstar Thermostat" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Day", + "evening": "Evening", + "inactive": "Inactive", + "morning": "Morning", + "night": "Night" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/es.json b/homeassistant/components/venstar/translations/es.json index 90d63f5aa0d..86d79202946 100644 --- a/homeassistant/components/venstar/translations/es.json +++ b/homeassistant/components/venstar/translations/es.json @@ -19,5 +19,18 @@ "title": "Conectar con el termostato Venstar" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "D\u00eda", + "evening": "Anochecer", + "inactive": "Inactivo", + "morning": "Ma\u00f1ana", + "night": "Noche" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/et.json b/homeassistant/components/venstar/translations/et.json index 5e50b2ece1c..722094e94fd 100644 --- a/homeassistant/components/venstar/translations/et.json +++ b/homeassistant/components/venstar/translations/et.json @@ -19,5 +19,18 @@ "title": "Loo \u00fchendus Venstari termostaadiga" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "P\u00e4ev", + "evening": "\u00d5htu", + "inactive": "Passiivne", + "morning": "Hommik", + "night": "\u00d6\u00f6" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/hu.json b/homeassistant/components/venstar/translations/hu.json index e2b36fd2a02..7b55e33c63a 100644 --- a/homeassistant/components/venstar/translations/hu.json +++ b/homeassistant/components/venstar/translations/hu.json @@ -19,5 +19,18 @@ "title": "Csatlakoz\u00e1s a Venstar termoszt\u00e1thoz" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Nappal", + "evening": "Este", + "inactive": "Inakt\u00edv", + "morning": "Reggel", + "night": "\u00c9jszaka" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/id.json b/homeassistant/components/venstar/translations/id.json index 1f64e8aa6a5..17768eaca8f 100644 --- a/homeassistant/components/venstar/translations/id.json +++ b/homeassistant/components/venstar/translations/id.json @@ -19,5 +19,18 @@ "title": "Hubungkan ke Termostat Venstar" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Siang", + "evening": "Sore", + "inactive": "Tidak aktif", + "morning": "Pagi", + "night": "Malam" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/it.json b/homeassistant/components/venstar/translations/it.json index c5ddc019984..b158db34f02 100644 --- a/homeassistant/components/venstar/translations/it.json +++ b/homeassistant/components/venstar/translations/it.json @@ -19,5 +19,18 @@ "title": "Connettiti al termostato Venstar" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Giorno", + "evening": "Sera", + "inactive": "Inattivo", + "morning": "Mattina", + "night": "Notte" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/lv.json b/homeassistant/components/venstar/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/venstar/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/nl.json b/homeassistant/components/venstar/translations/nl.json index b39a0f356e1..a3d3de1530b 100644 --- a/homeassistant/components/venstar/translations/nl.json +++ b/homeassistant/components/venstar/translations/nl.json @@ -19,5 +19,18 @@ "title": "Maak verbinding met de Venstar-thermostaat" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Dag", + "evening": "Avond", + "inactive": "Inactief", + "morning": "Ochtend", + "night": "Nacht" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/no.json b/homeassistant/components/venstar/translations/no.json index 6e77da8d723..15d6d529835 100644 --- a/homeassistant/components/venstar/translations/no.json +++ b/homeassistant/components/venstar/translations/no.json @@ -19,5 +19,18 @@ "title": "Koble til Venstar-termostaten" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Dag", + "evening": "Kveld", + "inactive": "Inaktiv", + "morning": "Morgen", + "night": "Natt" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/pl.json b/homeassistant/components/venstar/translations/pl.json index fdaf30ead9f..ed36fb5f1f2 100644 --- a/homeassistant/components/venstar/translations/pl.json +++ b/homeassistant/components/venstar/translations/pl.json @@ -19,5 +19,18 @@ "title": "Po\u0142\u0105czenie z termostatem Venstar" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "dzie\u0144", + "evening": "wiecz\u00f3r", + "inactive": "nieaktywny", + "morning": "ranek", + "night": "noc" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/pt-BR.json b/homeassistant/components/venstar/translations/pt-BR.json index c22a92e1808..3a41c42c45f 100644 --- a/homeassistant/components/venstar/translations/pt-BR.json +++ b/homeassistant/components/venstar/translations/pt-BR.json @@ -19,5 +19,18 @@ "title": "Conecte-se ao termostato Venstar" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Dia", + "evening": "Tarde", + "inactive": "Inativo", + "morning": "Manh\u00e3", + "night": "Noite" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/pt.json b/homeassistant/components/venstar/translations/pt.json index 632cb64e485..c410eefda02 100644 --- a/homeassistant/components/venstar/translations/pt.json +++ b/homeassistant/components/venstar/translations/pt.json @@ -14,5 +14,17 @@ } } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "Dia", + "inactive": "Inativo", + "morning": "Manh\u00e3", + "night": "Noite" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/ru.json b/homeassistant/components/venstar/translations/ru.json index c79ba70bbef..a428c9a7031 100644 --- a/homeassistant/components/venstar/translations/ru.json +++ b/homeassistant/components/venstar/translations/ru.json @@ -19,5 +19,18 @@ "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0443 Venstar" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "\u0414\u0435\u043d\u044c", + "evening": "\u0412\u0435\u0447\u0435\u0440", + "inactive": "\u041d\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u043e", + "morning": "\u0423\u0442\u0440\u043e", + "night": "\u041d\u043e\u0447\u044c" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/sk.json b/homeassistant/components/venstar/translations/sk.json index 15cffb46878..6b1ecfcb222 100644 --- a/homeassistant/components/venstar/translations/sk.json +++ b/homeassistant/components/venstar/translations/sk.json @@ -19,5 +19,18 @@ "title": "Pripojte k termostatu Venstar" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "De\u0148", + "evening": "Ve\u010der", + "inactive": "Neakt\u00edvne", + "morning": "R\u00e1no", + "night": "Noc" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/tr.json b/homeassistant/components/venstar/translations/tr.json index 5bd255daa48..50f975a78e5 100644 --- a/homeassistant/components/venstar/translations/tr.json +++ b/homeassistant/components/venstar/translations/tr.json @@ -19,5 +19,18 @@ "title": "Venstar Termostat'a ba\u011flan\u0131n" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "G\u00fcn", + "evening": "Ak\u015fam", + "inactive": "Etkin de\u011fil", + "morning": "Sabah", + "night": "Gece" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/uk.json b/homeassistant/components/venstar/translations/uk.json new file mode 100644 index 00000000000..74e2fefb2f6 --- /dev/null +++ b/homeassistant/components/venstar/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "\u0414\u0435\u043d\u044c", + "evening": "\u0412\u0435\u0447\u0456\u0440", + "inactive": "\u041d\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u0438\u0439", + "morning": "\u0420\u0430\u043d\u043e\u043a", + "night": "\u041d\u0456\u0447" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/venstar/translations/zh-Hant.json b/homeassistant/components/venstar/translations/zh-Hant.json index 74a29ecca33..d74eb6abcec 100644 --- a/homeassistant/components/venstar/translations/zh-Hant.json +++ b/homeassistant/components/venstar/translations/zh-Hant.json @@ -19,5 +19,18 @@ "title": "\u9023\u7dda\u81f3 Venstar \u6eab\u63a7\u5668" } } + }, + "entity": { + "sensor": { + "schedule_part": { + "state": { + "day": "\u65e5\u9593", + "evening": "\u508d\u665a", + "inactive": "\u9592\u7f6e", + "morning": "\u6e05\u6668", + "night": "\u591c\u665a" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/vera/strings.json b/homeassistant/components/vera/strings.json index 50d60f9a8ab..4e51177910c 100644 --- a/homeassistant/components/vera/strings.json +++ b/homeassistant/components/vera/strings.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "Could not connect to controller with URL {base_url}" }, "step": { diff --git a/homeassistant/components/vera/translations/bg.json b/homeassistant/components/vera/translations/bg.json new file mode 100644 index 00000000000..37b6f40c82e --- /dev/null +++ b/homeassistant/components/vera/translations/bg.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/translations/ca.json b/homeassistant/components/vera/translations/ca.json index 6aa7cd8292d..0d71248ff21 100644 --- a/homeassistant/components/vera/translations/ca.json +++ b/homeassistant/components/vera/translations/ca.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", "cannot_connect": "No s'ha pogut connectar amb el controlador amb URL {base_url}" }, "step": { diff --git a/homeassistant/components/vera/translations/de.json b/homeassistant/components/vera/translations/de.json index f13e347ffd9..696a1fea57e 100644 --- a/homeassistant/components/vera/translations/de.json +++ b/homeassistant/components/vera/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Konnte keine Verbindung zum Controller mit URL {base_url} herstellen" }, "step": { diff --git a/homeassistant/components/vera/translations/en.json b/homeassistant/components/vera/translations/en.json index 53c60e39c01..44a0e5444e5 100644 --- a/homeassistant/components/vera/translations/en.json +++ b/homeassistant/components/vera/translations/en.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Device is already configured", "cannot_connect": "Could not connect to controller with URL {base_url}" }, "step": { diff --git a/homeassistant/components/vera/translations/et.json b/homeassistant/components/vera/translations/et.json index 36d2cae71bf..c62603ea13e 100644 --- a/homeassistant/components/vera/translations/et.json +++ b/homeassistant/components/vera/translations/et.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "cannot_connect": "Ei saanud \u00fchendust URL-il {base_url} asuva kontrolleriga" }, "step": { diff --git a/homeassistant/components/vera/translations/no.json b/homeassistant/components/vera/translations/no.json index 807b44dc84e..e326c797b1d 100644 --- a/homeassistant/components/vera/translations/no.json +++ b/homeassistant/components/vera/translations/no.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Enheten er allerede konfigurert", "cannot_connect": "Kan ikke koble til kontrolleren med URL-adressen {base_url}" }, "step": { diff --git a/homeassistant/components/vera/translations/ru.json b/homeassistant/components/vera/translations/ru.json index 43dc0cfd168..c92117fbe33 100644 --- a/homeassistant/components/vera/translations/ru.json +++ b/homeassistant/components/vera/translations/ru.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0443 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 {base_url}." }, "step": { diff --git a/homeassistant/components/vera/translations/zh-Hant.json b/homeassistant/components/vera/translations/zh-Hant.json index 802ce7c97f1..6f178e022bf 100644 --- a/homeassistant/components/vera/translations/zh-Hant.json +++ b/homeassistant/components/vera/translations/zh-Hant.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u63a7\u5236\u5668 URL {base_url}" }, "step": { diff --git a/homeassistant/components/verisure/translations/nl.json b/homeassistant/components/verisure/translations/nl.json index 89dc4860e95..e0514bae2ee 100644 --- a/homeassistant/components/verisure/translations/nl.json +++ b/homeassistant/components/verisure/translations/nl.json @@ -6,7 +6,8 @@ }, "error": { "invalid_auth": "Ongeldige authenticatie", - "unknown": "Onverwachte fout" + "unknown": "Onverwachte fout", + "unknown_mfa": "Onbekende fout opgetreden tijdens het instellen van MFA" }, "step": { "installation": { @@ -17,7 +18,8 @@ }, "mfa": { "data": { - "code": "Verificatiecode" + "code": "Verificatiecode", + "description": "Uw account heeft verificatie in 2 stappen ingeschakeld. Voer de verificatiecode in die Verisure u toestuurt." } }, "reauth_confirm": { @@ -29,7 +31,8 @@ }, "reauth_mfa": { "data": { - "code": "Verificatiecode" + "code": "Verificatiecode", + "description": "Uw account heeft verificatie in 2 stappen ingeschakeld. Voer de verificatiecode in die Verisure u toestuurt." } }, "user": { diff --git a/homeassistant/components/version/translations/lv.json b/homeassistant/components/version/translations/lv.json index da8048f13fb..e773642b682 100644 --- a/homeassistant/components/version/translations/lv.json +++ b/homeassistant/components/version/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vesync/translations/lt.json b/homeassistant/components/vesync/translations/lt.json new file mode 100644 index 00000000000..883b5c03e2c --- /dev/null +++ b/homeassistant/components/vesync/translations/lt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis", + "username": "El. pa\u0161tas" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index 9f74df79e2b..da1119711a4 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -452,6 +452,22 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, ), + ViCareSensorEntityDescription( + key="buffer top temperature", + name="Buffer top temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_getter=lambda api: api.getBufferTopTemperature(), + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + ViCareSensorEntityDescription( + key="buffer main temperature", + name="Buffer main temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + value_getter=lambda api: api.getBufferMainTemperature(), + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), ) CIRCUIT_SENSORS: tuple[ViCareSensorEntityDescription, ...] = ( diff --git a/homeassistant/components/vicare/translations/uk.json b/homeassistant/components/vicare/translations/uk.json new file mode 100644 index 00000000000..e26b36c47c0 --- /dev/null +++ b/homeassistant/components/vicare/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u0415\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0430 \u043f\u043e\u0448\u0442\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/translations/lv.json b/homeassistant/components/vilfo/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/vilfo/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/el.json b/homeassistant/components/vizio/translations/el.json index 128d16d9c4c..1c3ab75cb5a 100644 --- a/homeassistant/components/vizio/translations/el.json +++ b/homeassistant/components/vizio/translations/el.json @@ -3,7 +3,7 @@ "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." + "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 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03c0\u03bf\u03c5 \u03b5\u03af\u03c7\u03b5 \u03b5\u03b9\u03c3\u03b1\u03c7\u03b8\u03b5\u03af \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03c5\u03bc\u03ad\u03bd\u03c9\u03c2, \u03b5\u03c0\u03bf\u03bc\u03ad\u03bd\u03c9\u03c2 \u03b7 \u03ba\u03b1\u03c4\u03b1\u03c7\u03ce\u03c1\u03b9\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\u03ac\u03bb\u03bf\u03b3\u03b1." }, "error": { "cannot_connect": "\u0391\u03c0\u03bf\u03c4\u03c5\u03c7\u03af\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2", diff --git a/homeassistant/components/vizio/translations/hu.json b/homeassistant/components/vizio/translations/hu.json index 908dfad8f76..e8a6cf63b3d 100644 --- a/homeassistant/components/vizio/translations/hu.json +++ b/homeassistant/components/vizio/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "already_configured_device": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s", - "updated_entry": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban defini\u00e1lt n\u00e9v, appok \u00e9s/vagy be\u00e1ll\u00edt\u00e1sok nem egyeznek meg a kor\u00e1bban import\u00e1lt konfigur\u00e1ci\u00f3val, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt." + "updated_entry": "Ez a bejegyz\u00e9s m\u00e1r be lett \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban meghat\u00e1rozott n\u00e9v, alkalmaz\u00e1sok \u00e9s/vagy be\u00e1ll\u00edt\u00e1sok nem egyeznek a kor\u00e1bban import\u00e1lt konfigur\u00e1ci\u00f3val, ez\u00e9rt a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00edt\u00e9sre ker\u00fclt." }, "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", diff --git a/homeassistant/components/vizio/translations/lv.json b/homeassistant/components/vizio/translations/lv.json new file mode 100644 index 00000000000..9eea6cd040d --- /dev/null +++ b/homeassistant/components/vizio/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured_device": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/tr.json b/homeassistant/components/vizio/translations/tr.json index 77487c0d127..8367b912d2f 100644 --- a/homeassistant/components/vizio/translations/tr.json +++ b/homeassistant/components/vizio/translations/tr.json @@ -3,7 +3,7 @@ "abort": { "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "cannot_connect": "Ba\u011flanma hatas\u0131", - "updated_entry": "Bu giri\u015f zaten kuruldu ancak konfig\u00fcrasyonda tan\u0131mlanan ad, uygulamalar ve/veya se\u00e7enekler daha \u00f6nce i\u00e7e aktar\u0131lan konfig\u00fcrasyonla e\u015fle\u015fmiyor, bu nedenle konfig\u00fcrasyon giri\u015fi buna g\u00f6re g\u00fcncellendi." + "updated_entry": "Bu giri\u015f zaten ayarlanm\u0131\u015f ancak yap\u0131land\u0131rmada tan\u0131mlanan ad, uygulamalar ve/veya se\u00e7enekler daha \u00f6nce i\u00e7e aktar\u0131lan yap\u0131land\u0131rmayla e\u015fle\u015fmiyor, bu nedenle yap\u0131land\u0131rma giri\u015fi buna g\u00f6re g\u00fcncellendi." }, "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", diff --git a/homeassistant/components/vlc_telnet/config_flow.py b/homeassistant/components/vlc_telnet/config_flow.py index 35898e91b34..6995a16c3ab 100644 --- a/homeassistant/components/vlc_telnet/config_flow.py +++ b/homeassistant/components/vlc_telnet/config_flow.py @@ -180,10 +180,8 @@ class VLCTelnetConfigFlow(ConfigFlow, domain=DOMAIN): except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") return self.async_abort(reason="unknown") - else: - return self.async_create_entry( - title=info["title"], data=self.hassio_discovery - ) + + return self.async_create_entry(title=info["title"], data=self.hassio_discovery) class CannotConnect(exceptions.HomeAssistantError): diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 5d366b6a8aa..a8e12cd771b 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -4,11 +4,10 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Coroutine from datetime import datetime from functools import wraps -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar 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.media_player import ( diff --git a/homeassistant/components/volkszaehler/manifest.json b/homeassistant/components/volkszaehler/manifest.json index baa9e4a8f14..6c422271a48 100644 --- a/homeassistant/components/volkszaehler/manifest.json +++ b/homeassistant/components/volkszaehler/manifest.json @@ -2,7 +2,7 @@ "domain": "volkszaehler", "name": "Volkszaehler", "documentation": "https://www.home-assistant.io/integrations/volkszaehler", - "requirements": ["volkszaehler==0.3.2"], + "requirements": ["volkszaehler==0.4.0"], "codeowners": [], "iot_class": "local_polling", "loggers": ["volkszaehler"] diff --git a/homeassistant/components/volvooncall/translations/it.json b/homeassistant/components/volvooncall/translations/it.json index 781233e5356..8c4060a5821 100644 --- a/homeassistant/components/volvooncall/translations/it.json +++ b/homeassistant/components/volvooncall/translations/it.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "mutable": "Consenti da remoto l'avvio / il blocco / ecc.", + "mutable": "Consenti da remoto l'avvio / la chiusura / ecc.", "password": "Password", "region": "Regione", "unit_system": "Unit\u00e0 di misura", diff --git a/homeassistant/components/volvooncall/translations/nl.json b/homeassistant/components/volvooncall/translations/nl.json index c9aa1582d52..a22f3bc9d4e 100644 --- a/homeassistant/components/volvooncall/translations/nl.json +++ b/homeassistant/components/volvooncall/translations/nl.json @@ -11,11 +11,19 @@ "step": { "user": { "data": { + "mutable": "Op afstand starten / vergrendelen / etc toestaan.", "password": "Wachtwoord", "region": "Regio", + "unit_system": "Eenheidssysteem", "username": "Gebruikersnaam" } } } + }, + "issues": { + "deprecated_yaml": { + "description": "Het configureren van Volvo On Call met YAML wordt verwijderd in een toekomstige versie van Home Assistant. \n\nDe bestaande YAML-configuratie is automatisch in de gebruikersinterface ge\u00efmporteerd. Verwijder de YAML configuratie uit het configuration.yaml bestand en start Home Assistant opnieuw om dit probleem op te lossen.", + "title": "De Volvo On Call YAML-configuratie wordt verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/volvooncall/translations/uk.json b/homeassistant/components/volvooncall/translations/uk.json new file mode 100644 index 00000000000..2aed6be91ba --- /dev/null +++ b/homeassistant/components/volvooncall/translations/uk.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wake_on_lan/translations/tr.json b/homeassistant/components/wake_on_lan/translations/tr.json new file mode 100644 index 00000000000..a66fbfc53a9 --- /dev/null +++ b/homeassistant/components/wake_on_lan/translations/tr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "moved_yaml": { + "description": "Wake on Lan'\u0131 YAML kullanarak yap\u0131land\u0131rma, entegrasyon anahtar\u0131na ta\u015f\u0131nd\u0131. \n\n Mevcut YAML yap\u0131land\u0131rman\u0131z 2 s\u00fcr\u00fcm daha \u00e7al\u0131\u015facak. \n\n YAML yap\u0131land\u0131rman\u0131z\u0131 belgelere g\u00f6re entegrasyon anahtar\u0131na ge\u00e7irin.", + "title": "Wake on Lan YAML yap\u0131land\u0131rmas\u0131 ta\u015f\u0131nd\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/number.py b/homeassistant/components/wallbox/number.py index 04db0ad9f7c..50d8310eae3 100644 --- a/homeassistant/components/wallbox/number.py +++ b/homeassistant/components/wallbox/number.py @@ -2,7 +2,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Optional, cast +from typing import cast from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.config_entries import ConfigEntry @@ -95,7 +95,7 @@ class WallboxNumber(WallboxEntity, NumberEntity): def native_value(self) -> float | None: """Return the value of the entity.""" return cast( - Optional[float], self._coordinator.data[CHARGER_MAX_CHARGING_CURRENT_KEY] + float | None, self._coordinator.data[CHARGER_MAX_CHARGING_CURRENT_KEY] ) async def async_set_native_value(self, value: float) -> None: diff --git a/homeassistant/components/wallbox/translations/lv.json b/homeassistant/components/wallbox/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/wallbox/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/uk.json b/homeassistant/components/wallbox/translations/uk.json new file mode 100644 index 00000000000..af1fed1b7ea --- /dev/null +++ b/homeassistant/components/wallbox/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "reauth_invalid": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457; \u0421\u0435\u0440\u0456\u0439\u043d\u0438\u0439 \u043d\u043e\u043c\u0435\u0440 \u043d\u0435 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u0454 \u043e\u0440\u0438\u0433\u0456\u043d\u0430\u043b\u0443" + }, + "step": { + "reauth_confirm": { + "data": { + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/bg.json b/homeassistant/components/water_heater/translations/bg.json index c80c861f5dd..a1e412a99f4 100644 --- a/homeassistant/components/water_heater/translations/bg.json +++ b/homeassistant/components/water_heater/translations/bg.json @@ -7,6 +7,9 @@ }, "state": { "_": { + "eco": "\u0415\u043a\u043e", + "gas": "\u0413\u0430\u0437", + "heat_pump": "\u0422\u0435\u0440\u043c\u043e\u043f\u043e\u043c\u043f\u0430", "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d" } } diff --git a/homeassistant/components/watttime/config_flow.py b/homeassistant/components/watttime/config_flow.py index a5d9c6925c8..4f4206da6ec 100644 --- a/homeassistant/components/watttime/config_flow.py +++ b/homeassistant/components/watttime/config_flow.py @@ -244,7 +244,7 @@ class WattTimeOptionsFlowHandler(config_entries.OptionsFlow): ) -> FlowResult: """Manage the options.""" if user_input is not None: - return self.async_create_entry(title="", data=user_input) + return self.async_create_entry(data=user_input) return self.async_show_form( step_id="init", diff --git a/homeassistant/components/watttime/translations/lv.json b/homeassistant/components/watttime/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/watttime/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/watttime/translations/uk.json b/homeassistant/components/watttime/translations/uk.json new file mode 100644 index 00000000000..73425b9ad26 --- /dev/null +++ b/homeassistant/components/watttime/translations/uk.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username}:" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0441\u0432\u043e\u0454 \u0456\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c:" + } + } + } +} \ 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 index 7c072fd84bc..0d7760b9a50 100644 --- a/homeassistant/components/waze_travel_time/translations/sk.json +++ b/homeassistant/components/waze_travel_time/translations/sk.json @@ -31,7 +31,7 @@ "units": "Jednotky", "vehicle_type": "Typ vozidla" }, - "description": "Vstupy \u201epodre\u0165azca\u201c v\u00e1m umo\u017enia prin\u00fati\u0165 integr\u00e1ciu pou\u017ei\u0165 konkr\u00e9tnu trasu alebo sa vyhn\u00fa\u0165 konkr\u00e9tnej trase pri v\u00fdpo\u010dte cestovania v \u010dase." + "description": "Vstupy `podre\u0165azca` v\u00e1m umo\u017enia prin\u00fati\u0165 integr\u00e1ciu pou\u017ei\u0165 konkr\u00e9tnu trasu alebo sa vyhn\u00fa\u0165 konkr\u00e9tnej trase pri v\u00fdpo\u010dte cestovania v \u010dase." } } }, diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 805139d134e..52642c4f1bf 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -9,6 +9,8 @@ import inspect import logging from typing import Any, Final, TypedDict, final +from typing_extensions import Required + from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( PRECISION_HALVES, @@ -155,11 +157,12 @@ def round_temperature(temperature: float | None, precision: float) -> float | No class Forecast(TypedDict, total=False): """Typed weather forecast dict. - All attributes are in native units and old attributes kept for backwards compatibility. + All attributes are in native units and old attributes kept + for backwards compatibility. """ condition: str | None - datetime: str + datetime: Required[str] precipitation_probability: int | None native_precipitation: float | None precipitation: None @@ -620,7 +623,10 @@ class WeatherEntity(Entity): @final @property def state_attributes(self) -> dict[str, Any]: - """Return the state attributes, converted from native units to user-configured units.""" + """Return the state attributes, converted. + + Attributes are configured from native units to user-configured units. + """ data: dict[str, Any] = {} precision = self.precision diff --git a/homeassistant/components/weather/translations/lt.json b/homeassistant/components/weather/translations/lt.json new file mode 100644 index 00000000000..477ad21ee01 --- /dev/null +++ b/homeassistant/components/weather/translations/lt.json @@ -0,0 +1,8 @@ +{ + "state": { + "_": { + "lightning": "\u017daibuoja", + "lightning-rainy": "\u017daibuoja, lietingas" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index cd5485d4fd2..6e960ceb143 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import Event, HomeAssistant, ServiceCall +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType @@ -75,8 +76,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: host = entry.data[CONF_HOST] key = entry.data[CONF_CLIENT_SECRET] - wrapper = WebOsClientWrapper(host, client_key=key) - await wrapper.connect() + # Attempt a connection, but fail gracefully if tv is off for example. + client = WebOsClient(host, key) + with suppress(*WEBOSTV_EXCEPTIONS): + try: + await client.connect() + except WebOsTvPairError as err: + raise ConfigEntryAuthFailed(err) from err + + # If pairing request accepted there will be no error + # Update the stored key without triggering reauth + update_client_key(hass, entry, client) async def async_service_handler(service: ServiceCall) -> None: method = SERVICE_TO_METHOD[service.service] @@ -90,7 +100,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: DOMAIN, service, async_service_handler, schema=schema ) - hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = wrapper + hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = client await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) # set up notify platform, no entry support for notify component yet, @@ -113,7 +123,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_on_stop(_event: Event) -> None: """Unregister callbacks and disconnect.""" - await wrapper.shutdown() + client.clear_state_update_callbacks() + await client.disconnect() entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_on_stop) @@ -138,6 +149,19 @@ async def async_control_connect(host: str, key: str | None) -> WebOsClient: return client +def update_client_key( + hass: HomeAssistant, entry: ConfigEntry, client: WebOsClient +) -> None: + """Check and update stored client key if key has changed.""" + host = entry.data[CONF_HOST] + key = entry.data[CONF_CLIENT_SECRET] + + if client.client_key != key: + _LOGGER.debug("Updating client key for host %s", host) + data = {CONF_HOST: host, CONF_CLIENT_SECRET: client.client_key} + hass.config_entries.async_update_entry(entry, data=data) + + 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) @@ -145,7 +169,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok: client = hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id) await hass_notify.async_reload(hass, DOMAIN) - await client.shutdown() + client.clear_state_update_callbacks() + await client.disconnect() # unregister service calls, check if this is the last entry to unload if unload_ok and not hass.data[DOMAIN][DATA_CONFIG_ENTRY]: @@ -153,25 +178,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.services.async_remove(DOMAIN, service) return unload_ok - - -class WebOsClientWrapper: - """Wrapper for a WebOS TV client with Home Assistant specific functions.""" - - def __init__(self, host: str, client_key: str) -> None: - """Set up the client.""" - self.host = host - self.client_key = client_key - self.client: WebOsClient | None = None - - async def connect(self) -> None: - """Attempt a connection, but fail gracefully if tv is off for example.""" - self.client = WebOsClient(self.host, self.client_key) - with suppress(*WEBOSTV_EXCEPTIONS, WebOsTvPairError): - await self.client.connect() - - async def shutdown(self) -> None: - """Unregister callbacks and disconnect.""" - assert self.client - self.client.clear_state_update_callbacks() - await self.client.disconnect() diff --git a/homeassistant/components/webostv/config_flow.py b/homeassistant/components/webostv/config_flow.py index d04e8a54121..1669e5a4c89 100644 --- a/homeassistant/components/webostv/config_flow.py +++ b/homeassistant/components/webostv/config_flow.py @@ -1,6 +1,7 @@ """Config flow to configure webostv component.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any from urllib.parse import urlparse @@ -8,14 +9,14 @@ from urllib.parse import urlparse from aiowebostv import WebOsTvPairError import voluptuous as vol -from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST, CONF_NAME from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult +from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers import config_validation as cv -from . import async_control_connect +from . import async_control_connect, update_client_key from .const import CONF_SOURCES, DEFAULT_NAME, DOMAIN, WEBOSTV_EXCEPTIONS from .helpers import async_get_sources @@ -30,7 +31,7 @@ DATA_SCHEMA = vol.Schema( _LOGGER = logging.getLogger(__name__) -class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class FlowHandler(ConfigFlow, domain=DOMAIN): """WebosTV configuration flow.""" VERSION = 1 @@ -40,12 +41,11 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._host: str = "" self._name: str = "" self._uuid: str | None = None + self._entry: ConfigEntry | None = None @staticmethod @callback - def async_get_options_flow( - config_entry: config_entries.ConfigEntry, - ) -> OptionsFlowHandler: + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) @@ -78,7 +78,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) self.hass.config_entries.async_update_entry(entry, unique_id=self._uuid) - raise data_entry_flow.AbortFlow("already_configured") + raise AbortFlow("already_configured") async def async_step_pairing( self, user_input: dict[str, Any] | None = None @@ -90,10 +90,7 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = {"name": self._name} errors = {} - if ( - self.context["source"] == config_entries.SOURCE_IMPORT - or user_input is not None - ): + if user_input is not None: try: client = await async_control_connect(self._host, None) except WebOsTvPairError: @@ -132,11 +129,37 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self._uuid = uuid return await self.async_step_pairing() + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an WebOsTvPairError.""" + self._host = entry_data[CONF_HOST] + self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() -class OptionsFlowHandler(config_entries.OptionsFlow): + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Dialog that informs the user that reauth is required.""" + assert self._entry is not None + + if user_input is not None: + try: + client = await async_control_connect(self._host, None) + except WebOsTvPairError: + return self.async_abort(reason="error_pairing") + except WEBOSTV_EXCEPTIONS: + return self.async_abort(reason="reauth_unsuccessful") + + update_client_key(self.hass, self._entry, client) + await self.hass.config_entries.async_reload(self._entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form(step_id="reauth_confirm") + + +class OptionsFlowHandler(OptionsFlow): """Handle options.""" - def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" self.config_entry = config_entry self.options = config_entry.options diff --git a/homeassistant/components/webostv/device_trigger.py b/homeassistant/components/webostv/device_trigger.py index 14854383ec8..c7e5701af02 100644 --- a/homeassistant/components/webostv/device_trigger.py +++ b/homeassistant/components/webostv/device_trigger.py @@ -16,7 +16,7 @@ from homeassistant.helpers.typing import ConfigType from . import trigger from .const import DOMAIN from .helpers import ( - async_get_client_wrapper_by_device_entry, + async_get_client_by_device_entry, async_get_device_entry_by_device_id, ) from .triggers.turn_on import ( @@ -43,7 +43,7 @@ async def async_validate_trigger_config( try: device = async_get_device_entry_by_device_id(hass, device_id) if DOMAIN in hass.data: - async_get_client_wrapper_by_device_entry(hass, device) + async_get_client_by_device_entry(hass, device) except ValueError as err: raise InvalidDeviceAutomationConfig(err) from err diff --git a/homeassistant/components/webostv/diagnostics.py b/homeassistant/components/webostv/diagnostics.py index ce62f51b540..5d88d61aa9d 100644 --- a/homeassistant/components/webostv/diagnostics.py +++ b/homeassistant/components/webostv/diagnostics.py @@ -27,7 +27,7 @@ async def async_get_config_entry_diagnostics( hass: HomeAssistant, entry: ConfigEntry ) -> dict[str, Any]: """Return diagnostics for a config entry.""" - client: WebOsClient = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id].client + client: WebOsClient = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] client_data = { "is_registered": client.is_registered(), diff --git a/homeassistant/components/webostv/helpers.py b/homeassistant/components/webostv/helpers.py index 4f1ab9dfebe..1de1070a2f1 100644 --- a/homeassistant/components/webostv/helpers.py +++ b/homeassistant/components/webostv/helpers.py @@ -1,11 +1,13 @@ """Helper functions for webOS Smart TV.""" from __future__ import annotations +from aiowebostv import WebOsClient + from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.device_registry import DeviceEntry -from . import WebOsClientWrapper, async_control_connect +from . import async_control_connect from .const import DATA_CONFIG_ENTRY, DOMAIN, LIVE_TV_APP_ID, WEBOSTV_EXCEPTIONS @@ -46,25 +48,24 @@ def async_get_device_id_from_entity_id(hass: HomeAssistant, entity_id: str) -> s @callback -def async_get_client_wrapper_by_device_entry( +def async_get_client_by_device_entry( hass: HomeAssistant, device: DeviceEntry -) -> WebOsClientWrapper: +) -> WebOsClient: """ - Get WebOsClientWrapper from Device Registry by device entry. + Get WebOsClient from Device Registry by device entry. - Raises ValueError if client wrapper is not found. + Raises ValueError if client is not found. """ for config_entry_id in device.config_entries: - wrapper: WebOsClientWrapper | None - if wrapper := hass.data[DOMAIN][DATA_CONFIG_ENTRY].get(config_entry_id): + if client := hass.data[DOMAIN][DATA_CONFIG_ENTRY].get(config_entry_id): break - if not wrapper: + if not client: raise ValueError( f"Device {device.id} is not from an existing {DOMAIN} config entry" ) - return wrapper + return client async def async_get_sources(host: str, key: str) -> list[str]: diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 1d7c92741a8..d7eb306ef3c 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -9,11 +9,10 @@ from functools import wraps from http import HTTPStatus import logging from ssl import SSLContext -from typing import Any, TypeVar, cast +from typing import Any, Concatenate, ParamSpec, TypeVar, cast from aiowebostv import WebOsClient, WebOsTvPairError import async_timeout -from typing_extensions import Concatenate, ParamSpec from homeassistant import util from homeassistant.components.media_player import ( @@ -39,7 +38,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.trigger import PluggableAction -from . import WebOsClientWrapper +from . import update_client_key from .const import ( ATTR_PAYLOAD, ATTR_SOUND_OUTPUT, @@ -74,18 +73,11 @@ SCAN_INTERVAL = timedelta(seconds=10) async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the LG webOS Smart TV platform.""" - unique_id = config_entry.unique_id - assert unique_id - name = config_entry.title - sources = config_entry.options.get(CONF_SOURCES) - wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] - - async_add_entities([LgWebOSMediaPlayerEntity(wrapper, name, sources, unique_id)]) + client = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] + async_add_entities([LgWebOSMediaPlayerEntity(entry, client)]) _T = TypeVar("_T", bound="LgWebOSMediaPlayerEntity") @@ -124,20 +116,14 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): _attr_device_class = MediaPlayerDeviceClass.TV - def __init__( - self, - wrapper: WebOsClientWrapper, - name: str, - sources: list[str] | None, - unique_id: str, - ) -> None: + def __init__(self, entry: ConfigEntry, client: WebOsClient) -> None: """Initialize the webos device.""" - self._wrapper = wrapper - self._client: WebOsClient = wrapper.client + self._entry = entry + self._client = client self._attr_assumed_state = True - self._attr_name = name - self._attr_unique_id = unique_id - self._sources = sources + self._attr_name = entry.title + self._attr_unique_id = entry.unique_id + self._sources = entry.options.get(CONF_SOURCES) # Assume that the TV is not paused self._paused = False @@ -328,7 +314,12 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity): return with suppress(*WEBOSTV_EXCEPTIONS, WebOsTvPairError): - await self._client.connect() + try: + await self._client.connect() + except WebOsTvPairError: + self._entry.async_start_reauth(self.hass) + else: + update_client_key(self.hass, self._entry, self._client) @property def supported_features(self) -> MediaPlayerEntityFeature: diff --git a/homeassistant/components/webostv/notify.py b/homeassistant/components/webostv/notify.py index 82e61856187..c4cefc3cffe 100644 --- a/homeassistant/components/webostv/notify.py +++ b/homeassistant/components/webostv/notify.py @@ -26,9 +26,7 @@ async def async_get_service( if discovery_info is None: return None - client = hass.data[DOMAIN][DATA_CONFIG_ENTRY][ - discovery_info[ATTR_CONFIG_ENTRY_ID] - ].client + client = hass.data[DOMAIN][DATA_CONFIG_ENTRY][discovery_info[ATTR_CONFIG_ENTRY_ID]] return LgWebOSNotificationService(client) diff --git a/homeassistant/components/webostv/services.yaml b/homeassistant/components/webostv/services.yaml index 0fb3cd1ae16..1985857d128 100644 --- a/homeassistant/components/webostv/services.yaml +++ b/homeassistant/components/webostv/services.yaml @@ -17,7 +17,7 @@ button: description: >- Name of the button to press. Known possible values are LEFT, RIGHT, DOWN, UP, HOME, MENU, BACK, ENTER, DASH, INFO, ASTERISK, CC, EXIT, - MUTE, RED, GREEN, BLUE, VOLUMEUP, VOLUMEDOWN, CHANNELUP, CHANNELDOWN, + MUTE, RED, GREEN, BLUE, YELLOW, VOLUMEUP, VOLUMEDOWN, CHANNELUP, CHANNELDOWN, PLAY, PAUSE, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 required: true example: "LEFT" diff --git a/homeassistant/components/webostv/strings.json b/homeassistant/components/webostv/strings.json index 41755e94f01..c623effe22b 100644 --- a/homeassistant/components/webostv/strings.json +++ b/homeassistant/components/webostv/strings.json @@ -13,6 +13,10 @@ "pairing": { "title": "webOS TV Pairing", "description": "Click submit and accept the pairing request on your TV.\n\n![Image](/static/images/config_webos.png)" + }, + "reauth_confirm": { + "title": "webOS TV Pairing", + "description": "Click submit and accept the pairing request on your TV.\n\n![Image](/static/images/config_webos.png)" } }, "error": { @@ -21,7 +25,9 @@ "abort": { "error_pairing": "Connected to LG webOS TV but not paired", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", - "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%]", + "reauth_unsuccessful": "Re-authentication was unsuccessful, please turn on your TV and try again." } }, "options": { @@ -35,7 +41,6 @@ } }, "error": { - "script_not_found": "Script not found", "cannot_retrieve": "Unable to retrieve the list of sources. Make sure device is switched on" } }, diff --git a/homeassistant/components/webostv/translations/bg.json b/homeassistant/components/webostv/translations/bg.json index 28092bd8b8c..b916fdbc712 100644 --- a/homeassistant/components/webostv/translations/bg.json +++ b/homeassistant/components/webostv/translations/bg.json @@ -1,7 +1,9 @@ { "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", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u0443\u0441\u043f\u0435\u0448\u043d\u0430", + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u0442\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u043a\u0430\u0446\u0438\u044f \u0431\u0435\u0448\u0435 \u043d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430, \u043c\u043e\u043b\u044f, \u0432\u043a\u043b\u044e\u0447\u0435\u0442\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0441\u0438 \u0438 \u043e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e." }, "flow_title": "LG webOS Smart TV", "step": { diff --git a/homeassistant/components/webostv/translations/ca.json b/homeassistant/components/webostv/translations/ca.json index 512165b6ba7..16438e18064 100644 --- a/homeassistant/components/webostv/translations/ca.json +++ b/homeassistant/components/webostv/translations/ca.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", - "error_pairing": "Connectat per\u00f2 no vinculat a TV LG webOS" + "error_pairing": "Connectat per\u00f2 no vinculat a TV LG webOS", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", + "reauth_unsuccessful": "La re-autenticaci\u00f3 no ha tingut \u00e8xit, engega el televisor i torna-ho a provar." }, "error": { "cannot_connect": "No s'ha pogut connectar, engega el televisor i comprova l'adre\u00e7a IP" @@ -14,6 +16,10 @@ "description": "Fes clic a envia i accepta la sol\u00b7licitud de vinculaci\u00f3 del televisor.\n\n![Image](/static/images/config_webos.png)", "title": "Vinculaci\u00f3 de TV webOS" }, + "reauth_confirm": { + "description": "Fes clic a 'envia' i accepta la sol\u00b7licitud de vinculaci\u00f3 del televisor.\n\n![Image](/static/images/config_webos.png)", + "title": "Vinculaci\u00f3 de TV webOS" + }, "user": { "data": { "host": "Amfitri\u00f3", @@ -31,8 +37,7 @@ }, "options": { "error": { - "cannot_retrieve": "No es pot obtenir la llista de fonts. Assegura't que el dispositiu est\u00e0 enc\u00e8s", - "script_not_found": "No s'ha trobat l'script" + "cannot_retrieve": "No es pot obtenir la llista de fonts. Assegura't que el dispositiu est\u00e0 enc\u00e8s" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/cs.json b/homeassistant/components/webostv/translations/cs.json index 4ab388e95df..1fc30b6ccc6 100644 --- a/homeassistant/components/webostv/translations/cs.json +++ b/homeassistant/components/webostv/translations/cs.json @@ -31,8 +31,7 @@ }, "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" + "cannot_retrieve": "Nelze na\u010d\u00edst seznam zdroj\u016f. Zkontrolujte, zda je za\u0159\u00edzen\u00ed zapnut\u00e9" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/de.json b/homeassistant/components/webostv/translations/de.json index 22ac8b87663..204d1b8eed4 100644 --- a/homeassistant/components/webostv/translations/de.json +++ b/homeassistant/components/webostv/translations/de.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", - "error_pairing": "Verbunden mit LG webOS TV, aber nicht gekoppelt" + "error_pairing": "Verbunden mit LG webOS TV, aber nicht gekoppelt", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "reauth_unsuccessful": "Die erneute Authentifizierung war nicht erfolgreich. Bitte schalte deinen Fernseher ein und versuche es erneut." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen, bitte schalte deinen Fernseher ein oder \u00fcberpr\u00fcfe die IP-Adresse" @@ -14,6 +16,10 @@ "description": "Dr\u00fccke auf Senden und akzeptiere die Kopplungsanfrage auf deinem Fernsehger\u00e4t.\n\n![Bild](/static/images/config_webos.png)", "title": "webOS TV-Kopplung" }, + "reauth_confirm": { + "description": "Dr\u00fccke auf Senden und akzeptiere die Kopplungsanfrage auf deinem Fernsehger\u00e4t.\n\n![Bild](/static/images/config_webos.png)", + "title": "webOS TV-Kopplung" + }, "user": { "data": { "host": "Host", @@ -31,8 +37,7 @@ }, "options": { "error": { - "cannot_retrieve": "Die Liste der Quellen kann nicht abgerufen werden. Stelle sicher, dass das Ger\u00e4t eingeschaltet ist.", - "script_not_found": "Skript nicht gefunden" + "cannot_retrieve": "Die Liste der Quellen kann nicht abgerufen werden. Stelle sicher, dass das Ger\u00e4t eingeschaltet ist." }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/el.json b/homeassistant/components/webostv/translations/el.json index 9b5566b1ea4..d0edc216ff9 100644 --- a/homeassistant/components/webostv/translations/el.json +++ b/homeassistant/components/webostv/translations/el.json @@ -3,7 +3,9 @@ "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_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", + "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 \u03b4\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03ae\u03c2, \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03ba\u03b1\u03b9 \u03b4\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac." }, "error": { "cannot_connect": "\u0397 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03b1\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5, \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2 \u03ae \u03b5\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP" @@ -14,6 +16,10 @@ "description": "\u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae \u03ba\u03b1\u03b9 \u03b1\u03c0\u03bf\u03b4\u03b5\u03c7\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2. \n\n ![Image](/static/images/config_webos.png)", "title": "\u03a3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7\u03c2 webOS" }, + "reauth_confirm": { + "description": "\u039a\u03ac\u03bd\u03c4\u03b5 \u03ba\u03bb\u03b9\u03ba \u03c3\u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae \u03ba\u03b1\u03b9 \u03b1\u03c0\u03bf\u03b4\u03b5\u03c7\u03c4\u03b5\u03af\u03c4\u03b5 \u03c4\u03bf \u03b1\u03af\u03c4\u03b7\u03bc\u03b1 \u03c3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7\u03c2 \u03c3\u03c4\u03b7\u03bd \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03ae \u03c3\u03b1\u03c2. \n\n ![Image](/static/images/config_webos.png)", + "title": "\u03a3\u03cd\u03b6\u03b5\u03c5\u03be\u03b7 \u03c4\u03b7\u03bb\u03b5\u03cc\u03c1\u03b1\u03c3\u03b7\u03c2 webOS" + }, "user": { "data": { "host": "\u039a\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2", @@ -31,8 +37,7 @@ }, "options": { "error": { - "cannot_retrieve": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bb\u03af\u03c3\u03c4\u03b1\u03c2 \u03c0\u03b7\u03b3\u03ce\u03bd. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7", - "script_not_found": "\u03a4\u03bf \u03c3\u03b5\u03bd\u03ac\u03c1\u03b9\u03bf \u03b4\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b5" + "cannot_retrieve": "\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bb\u03af\u03c3\u03c4\u03b1\u03c2 \u03c0\u03b7\u03b3\u03ce\u03bd. \u0392\u03b5\u03b2\u03b1\u03b9\u03c9\u03b8\u03b5\u03af\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b7 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ae \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/en.json b/homeassistant/components/webostv/translations/en.json index bd39d6899ee..6a6ad5e14c4 100644 --- a/homeassistant/components/webostv/translations/en.json +++ b/homeassistant/components/webostv/translations/en.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", - "error_pairing": "Connected to LG webOS TV but not paired" + "error_pairing": "Connected to LG webOS TV but not paired", + "reauth_successful": "Re-authentication was successful", + "reauth_unsuccessful": "Re-authentication was unsuccessful, please turn on your TV and try again." }, "error": { "cannot_connect": "Failed to connect, please turn on your TV or check ip address" @@ -14,6 +16,10 @@ "description": "Click submit and accept the pairing request on your TV.\n\n![Image](/static/images/config_webos.png)", "title": "webOS TV Pairing" }, + "reauth_confirm": { + "description": "Click submit and accept the pairing request on your TV.\n\n![Image](/static/images/config_webos.png)", + "title": "webOS TV Pairing" + }, "user": { "data": { "host": "Host", @@ -31,8 +37,7 @@ }, "options": { "error": { - "cannot_retrieve": "Unable to retrieve the list of sources. Make sure device is switched on", - "script_not_found": "Script not found" + "cannot_retrieve": "Unable to retrieve the list of sources. Make sure device is switched on" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/es-419.json b/homeassistant/components/webostv/translations/es-419.json new file mode 100644 index 00000000000..141bff966fd --- /dev/null +++ b/homeassistant/components/webostv/translations/es-419.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "description": "Haga clic en \"Enviar\" y acepte la demande de emparejamiento en su Televisi\u00f3n.\n\n![Image](/static/images/config_webos.png)", + "title": "v\u00ednculo webOS TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/webostv/translations/es.json b/homeassistant/components/webostv/translations/es.json index c09d156bfb5..91053261579 100644 --- a/homeassistant/components/webostv/translations/es.json +++ b/homeassistant/components/webostv/translations/es.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en curso", - "error_pairing": "Conectado a LG webOS TV pero no emparejado" + "error_pairing": "Conectado a LG webOS TV pero no emparejado", + "reauth_successful": "La autenticaci\u00f3n se volvi\u00f3 a realizar correctamente", + "reauth_unsuccessful": "La nueva autenticaci\u00f3n no se ha realizado correctamente, por favor, enciende tu TV e int\u00e9ntalo de nuevo." }, "error": { "cannot_connect": "No se pudo conectar, por favor, enciende tu TV o comprueba la direcci\u00f3n IP" @@ -14,6 +16,10 @@ "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" }, + "reauth_confirm": { + "description": "Haz clic en enviar y acepta la solicitud de emparejamiento en tu TV. \n\n![Image](/static/images/config_webos.png)", + "title": "Emparejamiento de webOS TV" + }, "user": { "data": { "host": "Host", @@ -31,8 +37,7 @@ }, "options": { "error": { - "cannot_retrieve": "No se puede recuperar la lista de fuentes. Aseg\u00farate de que el dispositivo est\u00e9 encendido", - "script_not_found": "Script no encontrado" + "cannot_retrieve": "No se puede recuperar la lista de fuentes. Aseg\u00farate de que el dispositivo est\u00e9 encendido" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/et.json b/homeassistant/components/webostv/translations/et.json index 1cadc13a06b..58758d19839 100644 --- a/homeassistant/components/webostv/translations/et.json +++ b/homeassistant/components/webostv/translations/et.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Sidumine on juba k\u00e4imas", - "error_pairing": "\u00dchendatud LG webOS teleriga kuid pole seotud" + "error_pairing": "\u00dchendatud LG webOS teleriga kuid pole seotud", + "reauth_successful": "Taastuvastamine \u00f5nnestus", + "reauth_unsuccessful": "Taastuvastamine eba\u00f5nnestus. L\u00fclita teler sisse ja proovi uuesti." }, "error": { "cannot_connect": "\u00dchenduse loomine nurjus, l\u00fclita teler sisse v\u00f5i kontrolli IP-aadressi" @@ -14,6 +16,10 @@ "description": "Kl\u00f5psa nuppu submit ja n\u00f5ustu oma teleri paaritamisn\u00f5udega.\n\n![Image](/static/images/config_webos.png)", "title": "webOS TV sidumine" }, + "reauth_confirm": { + "description": "Kl\u00f5psa nuppu submit ja n\u00f5ustu oma teleri paaritamisn\u00f5udega.\n\n![Image](/static/images/config_webos.png)", + "title": "webOS TV sidumine" + }, "user": { "data": { "host": "Host", @@ -31,8 +37,7 @@ }, "options": { "error": { - "cannot_retrieve": "Allikate loendit ei saa tuua. Veendu, et seade on sisse l\u00fclitatud", - "script_not_found": "Skripti ei leitud" + "cannot_retrieve": "Allikate loendit ei saa tuua. Veendu, et seade on sisse l\u00fclitatud" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/fr.json b/homeassistant/components/webostv/translations/fr.json index 7621d297a1d..541613c2703 100644 --- a/homeassistant/components/webostv/translations/fr.json +++ b/homeassistant/components/webostv/translations/fr.json @@ -31,8 +31,7 @@ }, "options": { "error": { - "cannot_retrieve": "Impossible de r\u00e9cup\u00e9rer la liste des sources. Assurez-vous que l'appareil est allum\u00e9", - "script_not_found": "Script introuvable" + "cannot_retrieve": "Impossible de r\u00e9cup\u00e9rer la liste des sources. Assurez-vous que l'appareil est allum\u00e9" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/hu.json b/homeassistant/components/webostv/translations/hu.json index b4ef5f39aa1..5ad77b84e2d 100644 --- a/homeassistant/components/webostv/translations/hu.json +++ b/homeassistant/components/webostv/translations/hu.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", - "error_pairing": "Csatlakozva az LG webOS TV-hez, de a p\u00e1ros\u00edt\u00e1s nem siker\u00fclt" + "error_pairing": "Csatlakozva az LG webOS TV-hez, de a p\u00e1ros\u00edt\u00e1s nem siker\u00fclt", + "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", + "reauth_unsuccessful": "Az \u00fajrahiteles\u00edt\u00e9s sikertelen volt, kapcsolja be ism\u00e9t a t\u00e9v\u00e9t, \u00e9s pr\u00f3b\u00e1lja \u00fajra." }, "error": { "cannot_connect": "Nem siker\u00fclt csatlakozni, k\u00e9rem, kapcsolja be a TV-t vagy ellen\u0151rizze az ip-c\u00edmet." @@ -14,6 +16,10 @@ "description": "K\u00fcldje el a k\u00e9r\u00e9st, \u00e9s fogadja el a p\u00e1ros\u00edt\u00e1si k\u00e9relmet a t\u00e9v\u00e9n. \n\n ![Image](/static/images/config_webos.png)", "title": "webOS TV p\u00e1ros\u00edt\u00e1s" }, + "reauth_confirm": { + "description": "Kattintson a k\u00fcld\u00e9s gombra, \u00e9s fogadja el a p\u00e1ros\u00edt\u00e1si k\u00e9relmet a t\u00e9v\u00e9j\u00e9n. \n\n ![K\u00e9p](/static/images/config_webos.png)", + "title": "webOS TV p\u00e1ros\u00edt\u00e1s" + }, "user": { "data": { "host": "C\u00edm", @@ -31,8 +37,7 @@ }, "options": { "error": { - "cannot_retrieve": "Nem siker\u00fclt lek\u00e9rni a forr\u00e1sok list\u00e1j\u00e1t. Gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy a k\u00e9sz\u00fcl\u00e9k be van kapcsolva", - "script_not_found": "A szkript nem tal\u00e1lhat\u00f3" + "cannot_retrieve": "Nem siker\u00fclt lek\u00e9rni a forr\u00e1sok list\u00e1j\u00e1t. Gy\u0151z\u0151dj\u00f6n meg r\u00f3la, hogy a k\u00e9sz\u00fcl\u00e9k be van kapcsolva" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/id.json b/homeassistant/components/webostv/translations/id.json index 81bc9f86bf6..30f2d09745a 100644 --- a/homeassistant/components/webostv/translations/id.json +++ b/homeassistant/components/webostv/translations/id.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Perangkat sudah dikonfigurasi", "already_in_progress": "Alur konfigurasi sedang berlangsung", - "error_pairing": "Terhubung ke LG webOS TV tetapi tidak dipasangkan" + "error_pairing": "Terhubung ke LG webOS TV tetapi tidak dipasangkan", + "reauth_successful": "Autentikasi ulang berhasil", + "reauth_unsuccessful": "Autentikasi ulang tidak berhasil, matikan TV dan coba lagi." }, "error": { "cannot_connect": "Gagal terhubung, nyalakan TV atau periksa alamat IP" @@ -14,6 +16,10 @@ "description": "Klik kirim dan terima permintaan pemasangan di TV Anda. \n\n![Image](/static/images/config_webos.png)", "title": "Pasangan webOS TV" }, + "reauth_confirm": { + "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", @@ -31,8 +37,7 @@ }, "options": { "error": { - "cannot_retrieve": "Tidak dapat mengambil daftar sumber. Pastikan perangkat dihidupkan", - "script_not_found": "Skrip tidak ditemukan" + "cannot_retrieve": "Tidak dapat mengambil daftar sumber. Pastikan perangkat dihidupkan" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/it.json b/homeassistant/components/webostv/translations/it.json index c5653248030..acd0097e8a7 100644 --- a/homeassistant/components/webostv/translations/it.json +++ b/homeassistant/components/webostv/translations/it.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", - "error_pairing": "Collegato a TV webOS LG, ma non accoppiato" + "error_pairing": "Collegato a TV webOS LG, ma non accoppiato", + "reauth_successful": "La nuova autenticazione \u00e8 stata eseguita correttamente", + "reauth_unsuccessful": "La riautenticazione non \u00e8 andata a buon fine, accendi la TV e riprova." }, "error": { "cannot_connect": "Impossibile connettersi, accendi la TV o controlla l'indirizzo IP" @@ -14,6 +16,10 @@ "description": "Fai clic su Invia e accetta la richiesta di associazione sulla TV. \n\n ![Immagine](/static/images/config_webos.png)", "title": "Accoppiamento webOS TV" }, + "reauth_confirm": { + "description": "Fare clic su Invia e accettare la richiesta di abbinamento sulla TV. \n\n![Immagine](/static/images/config_webos.png)", + "title": "Abbinamento TV webOS" + }, "user": { "data": { "host": "Host", @@ -31,8 +37,7 @@ }, "options": { "error": { - "cannot_retrieve": "Impossibile recuperare l'elenco delle sorgenti. Assicurati che il dispositivo sia acceso", - "script_not_found": "Script non trovato" + "cannot_retrieve": "Impossibile recuperare l'elenco delle sorgenti. Assicurati che il dispositivo sia acceso" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/ja.json b/homeassistant/components/webostv/translations/ja.json index 614c4188498..8fcbe4ffbfa 100644 --- a/homeassistant/components/webostv/translations/ja.json +++ b/homeassistant/components/webostv/translations/ja.json @@ -3,7 +3,8 @@ "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", - "error_pairing": "LG webOS TV\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u305b\u3093" + "error_pairing": "LG webOS TV\u306b\u63a5\u7d9a\u3055\u308c\u3066\u3044\u307e\u3059\u304c\u3001\u30da\u30a2\u30ea\u30f3\u30b0\u3055\u308c\u3066\u3044\u307e\u305b\u3093", + "reauth_successful": "\u518d\u8a8d\u8a3c\u306b\u6210\u529f\u3057\u307e\u3057\u305f" }, "error": { "cannot_connect": "\u63a5\u7d9a\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002TV\u306e\u96fb\u6e90\u3092\u5165\u308c\u308b\u304b\u3001IP\u30a2\u30c9\u30ec\u30b9\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044" @@ -31,8 +32,7 @@ }, "options": { "error": { - "cannot_retrieve": "\u30bd\u30fc\u30b9\u306e\u30ea\u30b9\u30c8\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3002\u30c7\u30d0\u30a4\u30b9\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002", - "script_not_found": "\u30b9\u30af\u30ea\u30d7\u30c8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093" + "cannot_retrieve": "\u30bd\u30fc\u30b9\u306e\u30ea\u30b9\u30c8\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3002\u30c7\u30d0\u30a4\u30b9\u306e\u96fb\u6e90\u304c\u5165\u3063\u3066\u3044\u308b\u3053\u3068\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044\u3002" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/ko.json b/homeassistant/components/webostv/translations/ko.json index e79ddbd7b3f..ac06e63a3a0 100644 --- a/homeassistant/components/webostv/translations/ko.json +++ b/homeassistant/components/webostv/translations/ko.json @@ -31,8 +31,7 @@ }, "options": { "error": { - "cannot_retrieve": "\uc18c\uc2a4 \ubaa9\ub85d\uc744 \uac80\uc0c9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc7a5\uce58\uac00 \ucf1c\uc838 \uc788\ub294\uc9c0 \ud655\uc778\ud558\uc138\uc694", - "script_not_found": "\uc2a4\ud06c\ub9bd\ud2b8\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc74c" + "cannot_retrieve": "\uc18c\uc2a4 \ubaa9\ub85d\uc744 \uac80\uc0c9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc7a5\uce58\uac00 \ucf1c\uc838 \uc788\ub294\uc9c0 \ud655\uc778\ud558\uc138\uc694" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/lv.json b/homeassistant/components/webostv/translations/lv.json index 676af9e30aa..0154a09be38 100644 --- a/homeassistant/components/webostv/translations/lv.json +++ b/homeassistant/components/webostv/translations/lv.json @@ -1,4 +1,9 @@ { + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/webostv/translations/nl.json b/homeassistant/components/webostv/translations/nl.json index f914287ce16..d7985373d85 100644 --- a/homeassistant/components/webostv/translations/nl.json +++ b/homeassistant/components/webostv/translations/nl.json @@ -31,8 +31,7 @@ }, "options": { "error": { - "cannot_retrieve": "Kan de lijst met bronnen niet ophalen. Zorg ervoor dat het apparaat is ingeschakeld", - "script_not_found": "Script niet gevonden" + "cannot_retrieve": "Kan de lijst met bronnen niet ophalen. Zorg ervoor dat het apparaat is ingeschakeld" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/no.json b/homeassistant/components/webostv/translations/no.json index 3cb8a11154c..b53a8d1aef7 100644 --- a/homeassistant/components/webostv/translations/no.json +++ b/homeassistant/components/webostv/translations/no.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", - "error_pairing": "Koblet til LG webOS TV, men ikke sammenkoblet" + "error_pairing": "Koblet til LG webOS TV, men ikke sammenkoblet", + "reauth_successful": "Re-autentisering var vellykket", + "reauth_unsuccessful": "Re-autentisering mislyktes. Sl\u00e5 p\u00e5 TV-en og pr\u00f8v igjen." }, "error": { "cannot_connect": "Kunne ikke koble til. Sl\u00e5 p\u00e5 TV-en eller sjekk IP-adressen" @@ -14,6 +16,10 @@ "description": "Klikk p\u00e5 send og godta sammenkoblingsforesp\u00f8rselen p\u00e5 TV-en. \n\n ![Image](/static/images/config_webos.png)", "title": "webOS TV-sammenkobling" }, + "reauth_confirm": { + "description": "Klikk p\u00e5 send inn og godta sammenkoblingsforesp\u00f8rselen p\u00e5 TV-en. \n\n ![Image](/static/images/config_webos.png)", + "title": "webOS TV-sammenkobling" + }, "user": { "data": { "host": "Vert", @@ -31,8 +37,7 @@ }, "options": { "error": { - "cannot_retrieve": "Kan ikke hente listen over kilder. S\u00f8rg for at enheten er sl\u00e5tt p\u00e5", - "script_not_found": "Skriptet ikke funnet" + "cannot_retrieve": "Kan ikke hente listen over kilder. S\u00f8rg for at enheten er sl\u00e5tt p\u00e5" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/pl.json b/homeassistant/components/webostv/translations/pl.json index 97929946536..8ae6e511268 100644 --- a/homeassistant/components/webostv/translations/pl.json +++ b/homeassistant/components/webostv/translations/pl.json @@ -3,7 +3,9 @@ "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_pairing": "Po\u0142\u0105czono z telewizorem LG webOS, ale nie sparowano", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", + "reauth_unsuccessful": "B\u0142\u0105d ponownego uwierzytelnienia, w\u0142\u0105cz telewizor i spr\u00f3buj ponownie." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, w\u0142\u0105cz telewizor lub sprawd\u017a adres IP" @@ -14,6 +16,10 @@ "description": "Kliknij \"Zatwierd\u017a\" i zaakceptuj \u017c\u0105danie parowania na swoim telewizorze. \n\n![Obraz](/static/images/config_webos.png)", "title": "Parowanie webOS TV" }, + "reauth_confirm": { + "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": { "data": { "host": "Nazwa hosta lub adres IP", @@ -31,8 +37,7 @@ }, "options": { "error": { - "cannot_retrieve": "Nie mo\u017cna pobra\u0107 listy \u017ar\u00f3de\u0142. Upewnij si\u0119, \u017ce urz\u0105dzenie jest w\u0142\u0105czone", - "script_not_found": "Skrypt nie zosta\u0142 znaleziony" + "cannot_retrieve": "Nie mo\u017cna pobra\u0107 listy \u017ar\u00f3de\u0142. Upewnij si\u0119, \u017ce urz\u0105dzenie jest w\u0142\u0105czone" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/pt-BR.json b/homeassistant/components/webostv/translations/pt-BR.json index 9eddde059a8..8a6914b9733 100644 --- a/homeassistant/components/webostv/translations/pt-BR.json +++ b/homeassistant/components/webostv/translations/pt-BR.json @@ -3,7 +3,9 @@ "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_pairing": "Conectado \u00e0 LG webOS TV, mas n\u00e3o emparelhado", + "reauth_successful": "A reautentica\u00e7\u00e3o foi bem-sucedida", + "reauth_unsuccessful": "A reautentica\u00e7\u00e3o n\u00e3o foi bem-sucedida, ligue a TV e tente novamente." }, "error": { "cannot_connect": "Falha ao conectar, ligue sua TV ou verifique o endere\u00e7o IP" @@ -14,6 +16,10 @@ "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" }, + "reauth_confirm": { + "description": "Clique em enviar e aceite a solicita\u00e7\u00e3o de pareamento na sua TV. \n\n ![Imagem](/static/images/config_webos.png)", + "title": "Pareamento da TV webOS" + }, "user": { "data": { "host": "Nome do host", @@ -31,8 +37,7 @@ }, "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" + "cannot_retrieve": "N\u00e3o foi poss\u00edvel recuperar a lista de fontes. Verifique se o dispositivo est\u00e1 ligado" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/ru.json b/homeassistant/components/webostv/translations/ru.json index a8c2a5dadfd..c826b79b704 100644 --- a/homeassistant/components/webostv/translations/ru.json +++ b/homeassistant/components/webostv/translations/ru.json @@ -3,7 +3,9 @@ "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.", - "error_pairing": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a LG webOS TV, \u043d\u043e \u043d\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u043e." + "error_pairing": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a LG webOS TV, \u043d\u043e \u043d\u0435 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u043e.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e.", + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0435 \u0443\u0434\u0430\u043b\u0430\u0441\u044c, \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443." }, "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, \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u043b\u0438 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0438 \u0432\u0435\u0440\u043d\u043e \u043b\u0438 \u0443\u043a\u0430\u0437\u0430\u043d IP-\u0430\u0434\u0440\u0435\u0441." @@ -14,6 +16,10 @@ "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 '\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c' \u0438 \u043f\u0440\u0438\u043c\u0438\u0442\u0435 \u0437\u0430\u043f\u0440\u043e\u0441 \u043d\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435. \n\n![Image](/static/images/config_webos.png)", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u043e\u043c" }, + "reauth_confirm": { + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 '\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c' \u0438 \u043f\u0440\u0438\u043c\u0438\u0442\u0435 \u0437\u0430\u043f\u0440\u043e\u0441 \u043d\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435. \n\n![Image](/static/images/config_webos.png)", + "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u043e\u043c" + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", @@ -31,8 +37,7 @@ }, "options": { "error": { - "cannot_retrieve": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e.", - "script_not_found": "\u0421\u043a\u0440\u0438\u043f\u0442 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d." + "cannot_retrieve": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432. \u0423\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e." }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/sk.json b/homeassistant/components/webostv/translations/sk.json index eb20925e0a4..1b13aa4f33b 100644 --- a/homeassistant/components/webostv/translations/sk.json +++ b/homeassistant/components/webostv/translations/sk.json @@ -3,7 +3,9 @@ "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_pairing": "Pripojen\u00e9 k telev\u00edzoru LG so syst\u00e9mom webOS, ale nesp\u00e1rovan\u00e9", + "reauth_successful": "Op\u00e4tovn\u00e9 overenie bolo \u00faspe\u0161n\u00e9", + "reauth_unsuccessful": "Op\u00e4tovn\u00e9 overenie nebolo \u00faspe\u0161n\u00e9, zapnite telev\u00edzor a sk\u00faste to znova." }, "error": { "cannot_connect": "Nepodarilo sa pripoji\u0165, pros\u00edm, zapnite telev\u00edzor alebo skontrolujte IP adresu" @@ -14,6 +16,10 @@ "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" }, + "reauth_confirm": { + "description": "Kliknite na Odosla\u0165 a prijmite \u017eiados\u0165 o p\u00e1rovanie na telev\u00edzore.\n\n![Image](/static/images/config_webos.png)", + "title": "p\u00e1rovanie s webOS TV" + }, "user": { "data": { "host": "Hostite\u013e", @@ -31,8 +37,7 @@ }, "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" + "cannot_retrieve": "Nie je mo\u017en\u00e9 na\u010d\u00edta\u0165 zoznam zdrojov. Skontrolujte, \u010di je zariadenie zapnut\u00e9" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/sv.json b/homeassistant/components/webostv/translations/sv.json index dcd01faf6a8..6dcb2ce7b60 100644 --- a/homeassistant/components/webostv/translations/sv.json +++ b/homeassistant/components/webostv/translations/sv.json @@ -31,8 +31,7 @@ }, "options": { "error": { - "cannot_retrieve": "Det gick inte att h\u00e4mta k\u00e4lllistan. Se till att enheten \u00e4r p\u00e5slagen", - "script_not_found": "Skriptet hittades inte" + "cannot_retrieve": "Det gick inte att h\u00e4mta k\u00e4lllistan. Se till att enheten \u00e4r p\u00e5slagen" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/tr.json b/homeassistant/components/webostv/translations/tr.json index c4f0f5c65c4..20c977d5732 100644 --- a/homeassistant/components/webostv/translations/tr.json +++ b/homeassistant/components/webostv/translations/tr.json @@ -14,6 +14,9 @@ "description": "G\u00f6nder'e t\u0131klay\u0131n ve TV'nizdeki e\u015fle\u015ftirme iste\u011fini kabul edin. \n\n ![Resim](/static/images/config_webos.png)", "title": "webOS TV E\u015fle\u015ftirme" }, + "reauth_confirm": { + "title": "webOS TV E\u015fle\u015ftirme" + }, "user": { "data": { "host": "Sunucu", @@ -31,8 +34,7 @@ }, "options": { "error": { - "cannot_retrieve": "Kaynak listesi al\u0131namad\u0131. Cihaz\u0131n a\u00e7\u0131k oldu\u011fundan emin olun", - "script_not_found": "Senaryo bulunamad\u0131" + "cannot_retrieve": "Kaynak listesi al\u0131namad\u0131. Cihaz\u0131n a\u00e7\u0131k oldu\u011fundan emin olun" }, "step": { "init": { diff --git a/homeassistant/components/webostv/translations/uk.json b/homeassistant/components/webostv/translations/uk.json new file mode 100644 index 00000000000..8a295f10b11 --- /dev/null +++ b/homeassistant/components/webostv/translations/uk.json @@ -0,0 +1,16 @@ +{ + "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", + "reauth_unsuccessful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043d\u0435 \u0432\u0434\u0430\u043b\u0430\u0441\u044f, \u0443\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0456 \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0442\u044c \u0441\u043f\u0440\u043e\u0431\u0443." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f, \u0443\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0430\u0431\u043e \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441\u0443" + }, + "step": { + "reauth_confirm": { + "title": "\u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 webOS" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/webostv/translations/zh-Hant.json b/homeassistant/components/webostv/translations/zh-Hant.json index 2908d0a85ce..54e0b9bcd21 100644 --- a/homeassistant/components/webostv/translations/zh-Hant.json +++ b/homeassistant/components/webostv/translations/zh-Hant.json @@ -3,7 +3,9 @@ "abort": { "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "error_pairing": "\u5df2\u9023\u7dda\u81f3 LG webOS TV \u4f46\u672a\u914d\u5c0d" + "error_pairing": "\u5df2\u9023\u7dda\u81f3 LG webOS TV \u4f46\u672a\u914d\u5c0d", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "reauth_unsuccessful": "\u91cd\u65b0\u8a8d\u8b49\u5931\u6557\uff0c\u8acb\u958b\u555f\u96fb\u8996\u5f8c\u518d\u8a66\u4e00\u6b21\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u78ba\u8a8d\u96fb\u8996\u5df2\u958b\u555f\u6216\u6aa2\u67e5 IP \u4f4d\u5740" @@ -14,6 +16,10 @@ "description": "\u9ede\u9078\u50b3\u9001\u4e26\u65bc\u96fb\u8996\u4e0a\u63a5\u53d7\u914d\u5c0d\u3002\n\n![Image](/static/images/config_webos.png)", "title": "webOS TV \u914d\u5c0d\u4e2d" }, + "reauth_confirm": { + "description": "\u9ede\u9078\u50b3\u9001\u4e26\u65bc\u96fb\u8996\u4e0a\u63a5\u53d7\u914d\u5c0d\u3002\n\n![Image](/static/images/config_webos.png)", + "title": "webOS TV \u914d\u5c0d\u4e2d" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", @@ -31,8 +37,7 @@ }, "options": { "error": { - "cannot_retrieve": "\u7121\u6cd5\u63a5\u6536\u4f86\u6e90\u5217\u8868\uff0c\u8acb\u78ba\u5b9a\u88dd\u7f6e\u70ba\u958b\u555f\u72c0\u614b", - "script_not_found": "\u627e\u4e0d\u5230\u8173\u672c" + "cannot_retrieve": "\u7121\u6cd5\u63a5\u6536\u4f86\u6e90\u5217\u8868\uff0c\u8acb\u78ba\u5b9a\u88dd\u7f6e\u70ba\u958b\u555f\u72c0\u614b" }, "step": { "init": { diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index 043eb42c12a..c98ca54d25a 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -12,10 +12,6 @@ from homeassistant.loader import bind_hass from . import commands, connection, const, decorators, http, messages # noqa: F401 from .connection import ActiveConnection, current_connection # noqa: F401 from .const import ( # noqa: F401 - COMPRESSED_STATE_ATTRIBUTES, - COMPRESSED_STATE_LAST_CHANGED, - COMPRESSED_STATE_LAST_UPDATED, - COMPRESSED_STATE_STATE, ERR_HOME_ASSISTANT_ERROR, ERR_INVALID_FORMAT, ERR_NOT_FOUND, diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index b4a18ab9ff0..d163db55b25 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -94,7 +94,7 @@ def handle_subscribe_events( ) -> None: """Handle subscribe events command.""" # Circular dep - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .permissions import SUBSCRIBE_ALLOWLIST event_type = msg["event_type"] @@ -311,7 +311,7 @@ def handle_subscribe_entities( connection.send_result(msg["id"]) data: dict[str, dict[str, dict]] = { messages.ENTITY_EVENT_ADD: { - state.entity_id: messages.compressed_state_dict_add(state) + state.entity_id: state.as_compressed_state() for state in states if not entity_ids or state.entity_id in entity_ids } @@ -561,7 +561,7 @@ async def handle_subscribe_trigger( ) -> None: """Handle subscribe trigger command.""" # Circular dep - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.helpers import trigger trigger_config = await trigger.async_validate_trigger_config(hass, msg["trigger"]) @@ -612,7 +612,7 @@ async def handle_test_condition( ) -> None: """Handle test condition command.""" # Circular dep - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.helpers import condition # Do static + dynamic validation of the condition @@ -638,7 +638,7 @@ async def handle_execute_script( ) -> None: """Handle execute script command.""" # Circular dep - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.helpers.script import Script context = connection.context(msg) @@ -680,7 +680,7 @@ async def handle_validate_config( ) -> None: """Handle validate config command.""" # Circular dep - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.helpers import condition, script, trigger result = {} diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 6135a821d53..7f9a9a7b561 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -50,10 +50,4 @@ SIGNAL_WEBSOCKET_DISCONNECTED: Final = "websocket_disconnected" # Data used to store the current connection list DATA_CONNECTIONS: Final = f"{DOMAIN}.connections" -COMPRESSED_STATE_STATE = "s" -COMPRESSED_STATE_ATTRIBUTES = "a" -COMPRESSED_STATE_CONTEXT = "c" -COMPRESSED_STATE_LAST_CHANGED = "lc" -COMPRESSED_STATE_LAST_UPDATED = "lu" - FEATURE_COALESCE_MESSAGES = "coalesce_messages" diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 7a08d19d857..e33e1cc3ce8 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -57,6 +57,8 @@ class WebSocketAdapter(logging.LoggerAdapter): def process(self, msg: str, kwargs: Any) -> tuple[str, Any]: """Add connid to websocket log messages.""" + if not self.extra or "connid" not in self.extra: + return msg, kwargs return f'[{self.extra["connid"]}] {msg}', kwargs diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index c3e5f6bb5f5..15965b37faa 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -7,6 +7,13 @@ from typing import Any, Final import voluptuous as vol +from homeassistant.const import ( + COMPRESSED_STATE_ATTRIBUTES, + COMPRESSED_STATE_CONTEXT, + COMPRESSED_STATE_LAST_CHANGED, + COMPRESSED_STATE_LAST_UPDATED, + COMPRESSED_STATE_STATE, +) from homeassistant.core import Event, State from homeassistant.helpers import config_validation as cv from homeassistant.helpers.json import JSON_DUMP @@ -17,13 +24,6 @@ from homeassistant.util.json import ( from homeassistant.util.yaml.loader import JSON_TYPE from . import const -from .const import ( - COMPRESSED_STATE_ATTRIBUTES, - COMPRESSED_STATE_CONTEXT, - COMPRESSED_STATE_LAST_CHANGED, - COMPRESSED_STATE_LAST_UPDATED, - COMPRESSED_STATE_STATE, -) _LOGGER: Final = logging.getLogger(__name__) @@ -128,13 +128,14 @@ def _state_diff_event(event: Event) -> dict: if (event_old_state := event.data["old_state"]) is None: return { ENTITY_EVENT_ADD: { - event_new_state.entity_id: compressed_state_dict_add(event_new_state) + event_new_state.entity_id: event_new_state.as_compressed_state() } } assert isinstance(event_old_state, State) return _state_diff(event_old_state, event_new_state) +@lru_cache(maxsize=128) def _state_diff( old_state: State, new_state: State ) -> dict[str, dict[str, dict[str, dict[str, str | list[str]]]]]: @@ -160,37 +161,17 @@ def _state_diff( additions[COMPRESSED_STATE_CONTEXT]["id"] = new_state.context.id else: additions[COMPRESSED_STATE_CONTEXT] = new_state.context.id - old_attributes = old_state.attributes - for key, value in new_state.attributes.items(): - if old_attributes.get(key) != value: - additions.setdefault(COMPRESSED_STATE_ATTRIBUTES, {})[key] = value - if removed := set(old_attributes).difference(new_state.attributes): - diff[STATE_DIFF_REMOVALS] = {COMPRESSED_STATE_ATTRIBUTES: removed} + if (old_attributes := old_state.attributes) != ( + new_attributes := new_state.attributes + ): + for key, value in new_attributes.items(): + if old_attributes.get(key) != value: + additions.setdefault(COMPRESSED_STATE_ATTRIBUTES, {})[key] = value + if removed := set(old_attributes).difference(new_attributes): + diff[STATE_DIFF_REMOVALS] = {COMPRESSED_STATE_ATTRIBUTES: removed} return {ENTITY_EVENT_CHANGE: {new_state.entity_id: diff}} -def compressed_state_dict_add(state: State) -> dict[str, Any]: - """Build a compressed dict of a state for adds. - - Omits the lu (last_updated) if it matches (lc) last_changed. - - Sends c (context) as a string if it only contains an id. - """ - if state.context.parent_id is None and state.context.user_id is None: - context: dict[str, Any] | str = state.context.id - else: - context = state.context.as_dict() - compressed_state: dict[str, Any] = { - COMPRESSED_STATE_STATE: state.state, - COMPRESSED_STATE_ATTRIBUTES: state.attributes, - COMPRESSED_STATE_CONTEXT: context, - COMPRESSED_STATE_LAST_CHANGED: state.last_changed.timestamp(), - } - if state.last_changed != state.last_updated: - compressed_state[COMPRESSED_STATE_LAST_UPDATED] = state.last_updated.timestamp() - return compressed_state - - def message_to_json(message: dict[str, Any]) -> str: """Serialize a websocket message to json.""" try: diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 2727c913fee..a70b5d70898 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Sequence from datetime import datetime import logging -from typing import Optional import pywemo import voluptuous as vol @@ -43,7 +42,7 @@ WEMO_MODEL_DISPATCH = { _LOGGER = logging.getLogger(__name__) -HostPortTuple = tuple[str, Optional[int]] +HostPortTuple = tuple[str, int | None] def coerce_host_port(value: str) -> HostPortTuple: diff --git a/homeassistant/components/whirlpool/__init__.py b/homeassistant/components/whirlpool/__init__.py index 0d6ae706f0c..42ffe7dd77e 100644 --- a/homeassistant/components/whirlpool/__init__.py +++ b/homeassistant/components/whirlpool/__init__.py @@ -1,52 +1,57 @@ -"""The Whirlpool Sixth Sense integration.""" +"""The Whirlpool Appliances integration.""" +import asyncio from dataclasses import dataclass import logging -import aiohttp +from aiohttp import ClientError from whirlpool.appliancesmanager import AppliancesManager from whirlpool.auth import Auth -from whirlpool.backendselector import BackendSelector, Brand, Region +from whirlpool.backendselector import BackendSelector from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN +from .const import CONF_REGIONS_MAP, DOMAIN +from .util import get_brand_for_region _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.CLIMATE] +PLATFORMS = [Platform.CLIMATE, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Whirlpool Sixth Sense from a config entry.""" hass.data.setdefault(DOMAIN, {}) - backend_selector = BackendSelector(Brand.Whirlpool, Region.EU) - auth = Auth(backend_selector, entry.data["username"], entry.data["password"]) + session = async_get_clientsession(hass) + region = CONF_REGIONS_MAP[entry.data.get(CONF_REGION, "EU")] + brand = get_brand_for_region(region) + backend_selector = BackendSelector(brand, region) + auth = Auth( + backend_selector, entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], session + ) try: await auth.do_auth(store=False) - except aiohttp.ClientError as ex: + except (ClientError, asyncio.TimeoutError) as ex: raise ConfigEntryNotReady("Cannot connect") from ex if not auth.is_access_token_valid(): _LOGGER.error("Authentication failed") raise ConfigEntryAuthFailed("Incorrect Password") - appliances_manager = AppliancesManager(backend_selector, auth) + appliances_manager = AppliancesManager(backend_selector, auth, session) if not await appliances_manager.fetch_appliances(): _LOGGER.error("Cannot fetch appliances") return False hass.data[DOMAIN][entry.entry_id] = WhirlpoolData( - appliances_manager, - auth, - backend_selector, + appliances_manager, auth, backend_selector ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - return True diff --git a/homeassistant/components/whirlpool/climate.py b/homeassistant/components/whirlpool/climate.py index ad4cd2ea389..1c6f770c886 100644 --- a/homeassistant/components/whirlpool/climate.py +++ b/homeassistant/components/whirlpool/climate.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging from typing import Any +from aiohttp import ClientSession from whirlpool.aircon import Aircon, FanSpeed as AirconFanSpeed, Mode as AirconMode from whirlpool.auth import Auth from whirlpool.backendselector import BackendSelector @@ -24,7 +25,8 @@ from homeassistant.components.climate import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import generate_entity_id +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo, generate_entity_id from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import WhirlpoolData @@ -71,9 +73,6 @@ async def async_setup_entry( ) -> None: """Set up entry.""" whirlpool_data: WhirlpoolData = hass.data[DOMAIN][config_entry.entry_id] - if not (aircons := whirlpool_data.appliances_manager.aircons): - _LOGGER.debug("No aircons found") - return aircons = [ AirConEntity( @@ -82,8 +81,9 @@ async def async_setup_entry( ac_data["NAME"], whirlpool_data.backend_selector, whirlpool_data.auth, + async_get_clientsession(hass), ) - for ac_data in aircons + for ac_data in whirlpool_data.appliances_manager.aircons ] async_add_entities(aircons, True) @@ -92,9 +92,11 @@ class AirConEntity(ClimateEntity): """Representation of an air conditioner.""" _attr_fan_modes = SUPPORTED_FAN_MODES + _attr_has_entity_name = True _attr_hvac_modes = SUPPORTED_HVAC_MODES _attr_max_temp = SUPPORTED_MAX_TEMP _attr_min_temp = SUPPORTED_MIN_TEMP + _attr_should_poll = False _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE @@ -103,20 +105,38 @@ class AirConEntity(ClimateEntity): _attr_swing_modes = SUPPORTED_SWING_MODES _attr_target_temperature_step = SUPPORTED_TARGET_TEMPERATURE_STEP _attr_temperature_unit = UnitOfTemperature.CELSIUS - _attr_should_poll = False - def __init__(self, hass, said, name, backend_selector: BackendSelector, auth: Auth): + def __init__( + self, + hass, + said, + name, + backend_selector: BackendSelector, + auth: Auth, + session: ClientSession, + ): """Initialize the entity.""" - self._aircon = Aircon(backend_selector, auth, said, self.async_write_ha_state) - + self._aircon = Aircon(backend_selector, auth, said, session) self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, said, hass=hass) - self._attr_name = name if name is not None else said self._attr_unique_id = said + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, said)}, + name=name if name is not None else said, + manufacturer="Whirlpool", + model="Sixth Sense", + ) + async def async_added_to_hass(self) -> None: """Connect aircon to the cloud.""" + self._aircon.register_attr_callback(self.async_write_ha_state) await self._aircon.connect() + async def async_will_remove_from_hass(self) -> None: + """Close Whrilpool Appliance sockets before removing.""" + self._aircon.unregister_attr_callback(self.async_write_ha_state) + await self._aircon.disconnect() + @property def available(self) -> bool: """Return True if entity is available.""" diff --git a/homeassistant/components/whirlpool/config_flow.py b/homeassistant/components/whirlpool/config_flow.py index 4a41c353d7f..fbbb670b6da 100644 --- a/homeassistant/components/whirlpool/config_flow.py +++ b/homeassistant/components/whirlpool/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for Whirlpool Sixth Sense integration.""" +"""Config flow for Whirlpool Appliances integration.""" from __future__ import annotations import asyncio @@ -6,21 +6,29 @@ from collections.abc import Mapping import logging from typing import Any -import aiohttp +from aiohttp import ClientError import voluptuous as vol +from whirlpool.appliancesmanager import AppliancesManager from whirlpool.auth import Auth -from whirlpool.backendselector import BackendSelector, Brand, Region +from whirlpool.backendselector import BackendSelector from homeassistant import config_entries, core, exceptions -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN +from .const import CONF_REGIONS_MAP, DOMAIN +from .util import get_brand_for_region _LOGGER = logging.getLogger(__name__) + STEP_USER_DATA_SCHEMA = vol.Schema( - {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_REGION): vol.In(list(CONF_REGIONS_MAP)), + } ) REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) @@ -33,16 +41,24 @@ async def validate_input( Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. """ - backend_selector = BackendSelector(Brand.Whirlpool, Region.EU) - auth = Auth(backend_selector, data[CONF_USERNAME], data[CONF_PASSWORD]) + session = async_get_clientsession(hass) + region = CONF_REGIONS_MAP[data[CONF_REGION]] + brand = get_brand_for_region(region) + backend_selector = BackendSelector(brand, region) + auth = Auth(backend_selector, data[CONF_USERNAME], data[CONF_PASSWORD], session) try: await auth.do_auth() - except (asyncio.TimeoutError, aiohttp.ClientConnectionError) as exc: + except (asyncio.TimeoutError, ClientError) as exc: raise CannotConnect from exc if not auth.is_access_token_valid(): raise InvalidAuth + appliances_manager = AppliancesManager(backend_selector, auth, session) + await appliances_manager.fetch_appliances() + if appliances_manager.aircons is None and appliances_manager.washer_dryers is None: + raise NoAppliances + return {"title": data[CONF_USERNAME]} @@ -68,7 +84,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): assert self.entry is not None password = user_input[CONF_PASSWORD] data = { - CONF_USERNAME: self.entry.data[CONF_USERNAME], + **self.entry.data, CONF_PASSWORD: password, } @@ -110,6 +126,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" + except NoAppliances: + errors["base"] = "no_appliances" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -131,3 +149,7 @@ class CannotConnect(exceptions.HomeAssistantError): class InvalidAuth(exceptions.HomeAssistantError): """Error to indicate there is invalid auth.""" + + +class NoAppliances(exceptions.HomeAssistantError): + """Error to indicate no supported appliances in the user account.""" diff --git a/homeassistant/components/whirlpool/const.py b/homeassistant/components/whirlpool/const.py index 8a030d8fab2..b472c7f9156 100644 --- a/homeassistant/components/whirlpool/const.py +++ b/homeassistant/components/whirlpool/const.py @@ -1,3 +1,10 @@ -"""Constants for the Whirlpool Sixth Sense integration.""" +"""Constants for the Whirlpool Appliances integration.""" + +from whirlpool.backendselector import Region DOMAIN = "whirlpool" + +CONF_REGIONS_MAP = { + "EU": Region.EU, + "US": Region.US, +} diff --git a/homeassistant/components/whirlpool/manifest.json b/homeassistant/components/whirlpool/manifest.json index a7c99e9066c..ddee3ec4595 100644 --- a/homeassistant/components/whirlpool/manifest.json +++ b/homeassistant/components/whirlpool/manifest.json @@ -1,10 +1,11 @@ { "domain": "whirlpool", - "name": "Whirlpool Sixth Sense", + "name": "Whirlpool Appliances", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/whirlpool", - "requirements": ["whirlpool-sixth-sense==0.17.0"], - "codeowners": ["@abmantis"], + "requirements": ["whirlpool-sixth-sense==0.18.2"], + "codeowners": ["@abmantis", "@mkmer"], "iot_class": "cloud_push", - "loggers": ["whirlpool"] + "loggers": ["whirlpool"], + "integration_type": "hub" } diff --git a/homeassistant/components/whirlpool/sensor.py b/homeassistant/components/whirlpool/sensor.py new file mode 100644 index 00000000000..972760f5af6 --- /dev/null +++ b/homeassistant/components/whirlpool/sensor.py @@ -0,0 +1,300 @@ +"""The Washer/Dryer Sensor for Whirlpool Appliances.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime, timedelta +import logging + +from whirlpool.washerdryer import MachineState, WasherDryer + +from homeassistant.components.sensor import ( + RestoreSensor, + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.util.dt import utcnow + +from . import WhirlpoolData +from .const import DOMAIN + +TANK_FILL = { + "0": "unknown", + "1": "empty", + "2": "25", + "3": "50", + "4": "100", + "5": "active", +} + +MACHINE_STATE = { + MachineState.Standby: "standby", + MachineState.Setting: "setting", + MachineState.DelayCountdownMode: "delay_countdown", + MachineState.DelayPause: "delay_paused", + MachineState.SmartDelay: "smart_delay", + MachineState.SmartGridPause: "smart_grid_pause", + MachineState.Pause: "pause", + MachineState.RunningMainCycle: "running_maincycle", + MachineState.RunningPostCycle: "running_postcycle", + MachineState.Exceptions: "exception", + MachineState.Complete: "complete", + MachineState.PowerFailure: "power_failure", + MachineState.ServiceDiagnostic: "service_diagnostic_mode", + MachineState.FactoryDiagnostic: "factory_diagnostic_mode", + MachineState.LifeTest: "life_test", + MachineState.CustomerFocusMode: "customer_focus_mode", + MachineState.DemoMode: "demo_mode", + MachineState.HardStopOrError: "hard_stop_or_error", + MachineState.SystemInit: "system_initialize", +} + +CYCLE_FUNC = [ + (WasherDryer.get_cycle_status_filling, "cycle_filling"), + (WasherDryer.get_cycle_status_rinsing, "cycle_rinsing"), + (WasherDryer.get_cycle_status_sensing, "cycle_sensing"), + (WasherDryer.get_cycle_status_soaking, "cycle_soaking"), + (WasherDryer.get_cycle_status_spinning, "cycle_spinning"), + (WasherDryer.get_cycle_status_washing, "cycle_washing"), +] + +DOOR_OPEN = "door_open" +ICON_D = "mdi:tumble-dryer" +ICON_W = "mdi:washing-machine" + +_LOGGER = logging.getLogger(__name__) + + +def washer_state(washer: WasherDryer) -> str | None: + """Determine correct states for a washer.""" + + if washer.get_attribute("Cavity_OpStatusDoorOpen") == "1": + return DOOR_OPEN + + machine_state = washer.get_machine_state() + + if machine_state == MachineState.RunningMainCycle: + for func, cycle_name in CYCLE_FUNC: + if func(washer): + return cycle_name + + return MACHINE_STATE.get(machine_state, None) + + +@dataclass +class WhirlpoolSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable + + +@dataclass +class WhirlpoolSensorEntityDescription( + SensorEntityDescription, WhirlpoolSensorEntityDescriptionMixin +): + """Describes Whirlpool Washer sensor entity.""" + + +SENSORS: tuple[WhirlpoolSensorEntityDescription, ...] = ( + WhirlpoolSensorEntityDescription( + key="state", + name="State", + translation_key="whirlpool_machine", + device_class=SensorDeviceClass.ENUM, + options=( + list(MACHINE_STATE.values()) + + [value for _, value in CYCLE_FUNC] + + [DOOR_OPEN] + ), + value_fn=washer_state, + ), + WhirlpoolSensorEntityDescription( + key="DispenseLevel", + name="Detergent Level", + translation_key="whirlpool_tank", + device_class=SensorDeviceClass.ENUM, + options=list(TANK_FILL.values()), + value_fn=lambda WasherDryer: TANK_FILL[ + WasherDryer.get_attribute("WashCavity_OpStatusBulkDispense1Level") + ], + ), +) + +SENSOR_TIMER: tuple[SensorEntityDescription] = ( + SensorEntityDescription( + key="timeremaining", + name="End Time", + device_class=SensorDeviceClass.TIMESTAMP, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Config flow entry for Whrilpool Laundry.""" + entities: list = [] + whirlpool_data: WhirlpoolData = hass.data[DOMAIN][config_entry.entry_id] + for appliance in whirlpool_data.appliances_manager.washer_dryers: + _wd = WasherDryer( + whirlpool_data.backend_selector, + whirlpool_data.auth, + appliance["SAID"], + async_get_clientsession(hass), + ) + await _wd.connect() + + entities.extend( + [ + WasherDryerClass( + appliance["SAID"], + appliance["NAME"], + description, + _wd, + ) + for description in SENSORS + ] + ) + entities.extend( + [ + WasherDryerTimeClass( + appliance["SAID"], + appliance["NAME"], + description, + _wd, + ) + for description in SENSOR_TIMER + ] + ) + async_add_entities(entities) + + +class WasherDryerClass(SensorEntity): + """A class for the whirlpool/maytag washer account.""" + + _attr_should_poll = False + + def __init__( + self, + said: str, + name: str, + description: WhirlpoolSensorEntityDescription, + washdry: WasherDryer, + ) -> None: + """Initialize the washer sensor.""" + self._wd: WasherDryer = washdry + + if name == "dryer": + self._attr_icon = ICON_D + else: + self._attr_icon = ICON_W + + self.entity_description: WhirlpoolSensorEntityDescription = description + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, said)}, + name=name.capitalize(), + manufacturer="Whirlpool", + ) + self._attr_has_entity_name = True + self._attr_unique_id = f"{said}-{description.key}" + + async def async_added_to_hass(self) -> None: + """Connect washer/dryer to the cloud.""" + self._wd.register_attr_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Close Whrilpool Appliance sockets before removing.""" + self._wd.unregister_attr_callback(self.async_write_ha_state) + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._wd.get_online() + + @property + def native_value(self) -> StateType | str: + """Return native value of sensor.""" + return self.entity_description.value_fn(self._wd) + + +class WasherDryerTimeClass(RestoreSensor): + """A timestamp class for the whirlpool/maytag washer account.""" + + _attr_should_poll = False + + def __init__( + self, + said: str, + name: str, + description: SensorEntityDescription, + washdry: WasherDryer, + ) -> None: + """Initialize the washer sensor.""" + self._wd: WasherDryer = washdry + + if name == "dryer": + self._attr_icon = ICON_D + else: + self._attr_icon = ICON_W + + self.entity_description: SensorEntityDescription = description + self._running: bool | None = None + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, said)}, + name=name.capitalize(), + manufacturer="Whirlpool", + ) + self._attr_has_entity_name = True + self._attr_unique_id = f"{said}-{description.key}" + + async def async_added_to_hass(self) -> None: + """Connect washer/dryer to the cloud.""" + if restored_data := await self.async_get_last_sensor_data(): + self._attr_native_value = restored_data.native_value + await super().async_added_to_hass() + self._wd.register_attr_callback(self.update_from_latest_data) + + async def async_will_remove_from_hass(self) -> None: + """Close Whrilpool Appliance sockets before removing.""" + await self._wd.disconnect() + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._wd.get_online() + + @callback + def update_from_latest_data(self) -> None: + """Calculate the time stamp for completion.""" + machine_state = self._wd.get_machine_state() + now = utcnow() + if ( + machine_state.value + in {MachineState.Complete.value, MachineState.Standby.value} + and self._running + ): + self._running = False + self._attr_native_value = now + self._async_write_ha_state() + + if machine_state is MachineState.RunningMainCycle: + self._running = True + new_timestamp = now + timedelta( + seconds=int(self._wd.get_attribute("Cavity_TimeStatusEstTimeRemaining")) + ) + + if isinstance(self._attr_native_value, datetime) and abs( + new_timestamp - self._attr_native_value + ) > timedelta(seconds=60): + + self._attr_native_value = new_timestamp + self._async_write_ha_state() diff --git a/homeassistant/components/whirlpool/strings.json b/homeassistant/components/whirlpool/strings.json index 78a46954183..aff89019e4c 100644 --- a/homeassistant/components/whirlpool/strings.json +++ b/homeassistant/components/whirlpool/strings.json @@ -8,10 +8,58 @@ } } }, + "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%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "unknown": "[%key:common::config_flow::error::unknown%]", + "no_appliances": "No supported appliances found" + } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "standby": "[%key:common::state::standby%]", + "setting": "Setting", + "delay_countdown": "Delay Countdown", + "delay_paused": "Delay Paused", + "smart_delay": "Smart Delay", + "smart_grid_pause": "Smart Delay", + "pause": "[%key:common::state::paused%]", + "running_maincycle": "Running Maincycle", + "running_postcycle": "Running Postcycle", + "exception": "Exception", + "complete": "Complete", + "power_failure": "Power Failure", + "service_diagnostic_mode": "Service Diagnostic Mode", + "factory_diagnostic_mode": "Factory Diagnostic Mode", + "life_test": "Life Test", + "customer_focus_mode": "Customer Focus Mode", + "demo_mode": "Demo Mode", + "hard_stop_or_error": "Hard Stop or Error", + "system_initialize": "System Initialize", + "cycle_filling": "Cycle Filling", + "cycle_rinsing": "Cycle Rinsing", + "cycle_sensing": "Cycle Sensing", + "cycle_soaking": "Cycle Soaking", + "cycle_spinning": "Cycle Spinning", + "cycle_washing": "Cycle Washing", + "door_open": "Door Open" + } + }, + "whirlpool_tank": { + "state": { + "unknown": "Unknown", + "empty": "Empty", + "25": "25%", + "50": "50%", + "100": "100%", + "active": "[%key:common::state::active%]" + } + } } } } diff --git a/homeassistant/components/whirlpool/translations/bg.json b/homeassistant/components/whirlpool/translations/bg.json index 9cbfcd4ede8..6756059f804 100644 --- a/homeassistant/components/whirlpool/translations/bg.json +++ b/homeassistant/components/whirlpool/translations/bg.json @@ -1,8 +1,12 @@ { "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\u0445 \u043f\u0440\u0438 \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435", "invalid_auth": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0443\u0434\u043e\u0441\u0442\u043e\u0432\u0435\u0440\u044f\u0432\u0430\u043d\u0435", + "no_appliances": "\u041d\u044f\u043c\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u043f\u043e\u0434\u0434\u044a\u0440\u0436\u0430\u043d\u0438 \u0443\u0440\u0435\u0434\u0438", "unknown": "\u041d\u0435\u043e\u0447\u0430\u043a\u0432\u0430\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430" }, "step": { @@ -13,5 +17,27 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "cycle_rinsing": "\u0426\u0438\u043a\u044a\u043b \u0438\u0437\u043f\u043b\u0430\u043a\u0432\u0430\u043d\u0435", + "cycle_soaking": "\u0426\u0438\u043a\u044a\u043b \u043d\u0430\u043a\u0438\u0441\u0432\u0430\u043d\u0435", + "cycle_spinning": "\u0426\u0438\u043a\u044a\u043b \u0446\u0435\u043d\u0442\u0440\u043e\u0444\u0443\u0433\u0438\u0440\u0430\u043d\u0435", + "cycle_washing": "\u0426\u0438\u043a\u044a\u043b \u043f\u0440\u0430\u043d\u0435", + "demo_mode": "\u0414\u0435\u043c\u043e \u0440\u0435\u0436\u0438\u043c", + "door_open": "\u041e\u0442\u0432\u043e\u0440\u0435\u043d\u0430 \u0432\u0440\u0430\u0442\u0430", + "system_initialize": "\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430\u0442\u0430" + } + }, + "whirlpool_tank": { + "state": { + "100": "100%", + "25": "25%", + "50": "50%" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/ca.json b/homeassistant/components/whirlpool/translations/ca.json index f844476e4c6..c3fae940ccc 100644 --- a/homeassistant/components/whirlpool/translations/ca.json +++ b/homeassistant/components/whirlpool/translations/ca.json @@ -1,8 +1,12 @@ { "config": { + "abort": { + "already_configured": "El compte ja est\u00e0 configurat" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "no_appliances": "No s'han trobat aparells compatibles", "unknown": "Error inesperat" }, "step": { @@ -13,5 +17,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Completa", + "customer_focus_mode": "Mode client", + "cycle_filling": "Cicle d'omplerta", + "cycle_rinsing": "Cicle d'esbandit", + "cycle_sensing": "Cicle de sensat", + "cycle_soaking": "Cicle de remull", + "cycle_spinning": "Cicle de centrifugat", + "cycle_washing": "Cicle de rentat", + "delay_countdown": "Retard en compte enrere", + "delay_paused": "Retard en pausa", + "demo_mode": "Mode demostraci\u00f3", + "door_open": "Porta oberta", + "exception": "Excepci\u00f3", + "factory_diagnostic_mode": "Mode diagn\u00f2stic de f\u00e0brica", + "hard_stop_or_error": "Error o parada for\u00e7osa", + "life_test": "Test de vida", + "pause": "Pausat/ada", + "power_failure": "Fallada d'alimentaci\u00f3", + "running_maincycle": "Executant cicle principal", + "running_postcycle": "Executant post-cicle", + "service_diagnostic_mode": "Mode diagn\u00f2stic de servei", + "setting": "Configurant", + "smart_delay": "Retard intel\u00b7ligent", + "smart_grid_pause": "Retard intel\u00b7ligent", + "standby": "En espera", + "system_initialize": "Inicialitzaci\u00f3 del sistema" + } + }, + "whirlpool_tank": { + "state": { + "100": "100%", + "25": "25%", + "50": "50%", + "active": "Actiu", + "empty": "Buit", + "unknown": "Desconegut" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/de.json b/homeassistant/components/whirlpool/translations/de.json index 57f62e0da32..ac99ee618de 100644 --- a/homeassistant/components/whirlpool/translations/de.json +++ b/homeassistant/components/whirlpool/translations/de.json @@ -1,8 +1,12 @@ { "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", + "no_appliances": "Keine unterst\u00fctzten Ger\u00e4te gefunden", "unknown": "Unerwarteter Fehler" }, "step": { @@ -13,5 +17,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Vollst\u00e4ndig", + "customer_focus_mode": "Kundenfokus-Modus", + "cycle_filling": "Zyklus F\u00fcllen", + "cycle_rinsing": "Zyklus Sp\u00fclen", + "cycle_sensing": "Zyklus Erkennung", + "cycle_soaking": "Zyklus Einweichen", + "cycle_spinning": "Zyklus Schleudern", + "cycle_washing": "Zyklus Waschen", + "delay_countdown": "Verz\u00f6gerungs-Countdown", + "delay_paused": "Verz\u00f6gerung pausiert", + "demo_mode": "Demo-Modus", + "door_open": "T\u00fcr \u00f6ffen", + "exception": "Ausnahme", + "factory_diagnostic_mode": "Werksdiagnosemodus", + "hard_stop_or_error": "Hardstop oder Fehler", + "life_test": "Life Test", + "pause": "Pausiert", + "power_failure": "Stromausfall", + "running_maincycle": "Laufender Hauptzyklus", + "running_postcycle": "Laufender Postzyklus", + "service_diagnostic_mode": "Service-Diagnosemodus", + "setting": "Einstellung", + "smart_delay": "Intelligente Verz\u00f6gerung", + "smart_grid_pause": "Intelligente Verz\u00f6gerung", + "standby": "Standby", + "system_initialize": "System initialisieren" + } + }, + "whirlpool_tank": { + "state": { + "100": "100%", + "25": "25%", + "50": "50%", + "active": "Aktiv", + "empty": "Leer", + "unknown": "Unbekannt" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/el.json b/homeassistant/components/whirlpool/translations/el.json index 9ffc0f96a83..4fc55d1e785 100644 --- a/homeassistant/components/whirlpool/translations/el.json +++ b/homeassistant/components/whirlpool/translations/el.json @@ -3,6 +3,7 @@ "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_appliances": "\u0394\u03b5\u03bd \u03b2\u03c1\u03ad\u03b8\u03b7\u03ba\u03b1\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b5\u03c2 \u03c3\u03c5\u03c3\u03ba\u03b5\u03c5\u03ad\u03c2", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" }, "step": { @@ -13,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "\u03a0\u03bb\u03ae\u03c1\u03b7\u03c2", + "customer_focus_mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b5\u03c3\u03c4\u03af\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf\u03bd \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7", + "cycle_filling": "\u03a0\u03bb\u03ae\u03c1\u03c9\u03c3\u03b7 \u03ba\u03cd\u03ba\u03bb\u03bf\u03c5", + "cycle_rinsing": "\u039a\u03cd\u03ba\u03bb\u03bf\u03c2 \u03be\u03b5\u03c0\u03bb\u03cd\u03bc\u03b1\u03c4\u03bf\u03c2", + "cycle_sensing": "\u0391\u03bd\u03af\u03c7\u03bd\u03b5\u03c5\u03c3\u03b7 \u03ba\u03cd\u03ba\u03bb\u03bf\u03c5", + "cycle_soaking": "\u039a\u03cd\u03ba\u03bb\u03bf\u03c2 \u03bc\u03bf\u03c5\u03bb\u03b9\u03ac\u03c3\u03bc\u03b1\u03c4\u03bf\u03c2", + "cycle_spinning": "\u039a\u03cd\u03ba\u03bb\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03c3\u03c4\u03c1\u03bf\u03c6\u03ae\u03c2", + "cycle_washing": "\u039a\u03cd\u03ba\u03bb\u03bf\u03c2 \u03c0\u03bb\u03cd\u03c3\u03b7\u03c2", + "delay_countdown": "\u0391\u03bd\u03c4\u03af\u03c3\u03c4\u03c1\u03bf\u03c6\u03b7 \u03bc\u03ad\u03c4\u03c1\u03b7\u03c3\u03b7 \u03ba\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7\u03c2", + "delay_paused": "\u039a\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7 \u03c3\u03b5 \u03c0\u03b1\u03cd\u03c3\u03b7", + "demo_mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b5\u03c0\u03af\u03b4\u03b5\u03b9\u03be\u03b7\u03c2", + "door_open": "\u03a0\u03cc\u03c1\u03c4\u03b1 \u03b1\u03bd\u03bf\u03b9\u03c7\u03c4\u03ae", + "exception": "\u0395\u03be\u03b1\u03af\u03c1\u03b5\u03c3\u03b7", + "factory_diagnostic_mode": "\u0395\u03c1\u03b3\u03bf\u03c3\u03c4\u03b1\u03c3\u03b9\u03b1\u03ba\u03ae \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b4\u03b9\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2", + "hard_stop_or_error": "\u03a3\u03ba\u03bb\u03b7\u03c1\u03ae \u03b4\u03b9\u03b1\u03ba\u03bf\u03c0\u03ae \u03ae \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1", + "life_test": "\u0394\u03bf\u03ba\u03b9\u03bc\u03ae \u03b6\u03c9\u03ae\u03c2", + "pause": "\u03a3\u03b5 \u03c0\u03b1\u03cd\u03c3\u03b7", + "power_failure": "\u0394\u03b9\u03b1\u03ba\u03bf\u03c0\u03ae \u03c1\u03b5\u03cd\u03bc\u03b1\u03c4\u03bf\u03c2", + "running_maincycle": "\u0395\u03ba\u03c4\u03ad\u03bb\u03b5\u03c3\u03b7 Maincycle", + "running_postcycle": "\u0395\u03ba\u03c4\u03ad\u03bb\u03b5\u03c3\u03b7 Postcycle", + "service_diagnostic_mode": "\u039b\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b4\u03b9\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7\u03c2 \u03c3\u03ad\u03c1\u03b2\u03b9\u03c2", + "setting": "\u03a1\u03cd\u03b8\u03bc\u03b9\u03c3\u03b7", + "smart_delay": "\u0388\u03be\u03c5\u03c0\u03bd\u03b7 \u03ba\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7", + "smart_grid_pause": "\u0388\u03be\u03c5\u03c0\u03bd\u03b7 \u03ba\u03b1\u03b8\u03c5\u03c3\u03c4\u03ad\u03c1\u03b7\u03c3\u03b7", + "standby": "\u0391\u03bd\u03b1\u03bc\u03bf\u03bd\u03ae", + "system_initialize": "\u0391\u03c1\u03c7\u03b9\u03ba\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2" + } + }, + "whirlpool_tank": { + "state": { + "100": "100%", + "25": "25%", + "50": "50%", + "active": "\u0395\u03bd\u03b5\u03c1\u03b3\u03cc", + "empty": "\u0386\u03b4\u03b5\u03b9\u03bf", + "unknown": "\u0386\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/en.json b/homeassistant/components/whirlpool/translations/en.json index 74817db9ba7..1bfa4386fd8 100644 --- a/homeassistant/components/whirlpool/translations/en.json +++ b/homeassistant/components/whirlpool/translations/en.json @@ -1,8 +1,12 @@ { "config": { + "abort": { + "already_configured": "Account is already configured" + }, "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", + "no_appliances": "No supported appliances found", "unknown": "Unexpected error" }, "step": { @@ -13,5 +17,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Complete", + "customer_focus_mode": "Customer Focus Mode", + "cycle_filling": "Cycle Filling", + "cycle_rinsing": "Cycle Rinsing", + "cycle_sensing": "Cycle Sensing", + "cycle_soaking": "Cycle Soaking", + "cycle_spinning": "Cycle Spinning", + "cycle_washing": "Cycle Washing", + "delay_countdown": "Delay Countdown", + "delay_paused": "Delay Paused", + "demo_mode": "Demo Mode", + "door_open": "Door Open", + "exception": "Exception", + "factory_diagnostic_mode": "Factory Diagnostic Mode", + "hard_stop_or_error": "Hard Stop or Error", + "life_test": "Life Test", + "pause": "Paused", + "power_failure": "Power Failure", + "running_maincycle": "Running Maincycle", + "running_postcycle": "Running Postcycle", + "service_diagnostic_mode": "Service Diagnostic Mode", + "setting": "Setting", + "smart_delay": "Smart Delay", + "smart_grid_pause": "Smart Delay", + "standby": "Standby", + "system_initialize": "System Initialize" + } + }, + "whirlpool_tank": { + "state": { + "100": "100%", + "25": "25%", + "50": "50%", + "active": "Active", + "empty": "Empty", + "unknown": "Unknown" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/es.json b/homeassistant/components/whirlpool/translations/es.json index b5ec4d85cb0..ec8c9b8140e 100644 --- a/homeassistant/components/whirlpool/translations/es.json +++ b/homeassistant/components/whirlpool/translations/es.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "No se pudo conectar", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "no_appliances": "No se encontraron dispositivos compatibles", "unknown": "Error inesperado" }, "step": { @@ -13,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Completo", + "customer_focus_mode": "Modo de atenci\u00f3n al cliente", + "cycle_filling": "Ciclo de llenado", + "cycle_rinsing": "Ciclo de enjuague", + "cycle_sensing": "Ciclo de detecci\u00f3n", + "cycle_soaking": "Ciclo de remojo", + "cycle_spinning": "Ciclo de giro", + "cycle_washing": "Ciclo de lavado", + "delay_countdown": "Cuenta atr\u00e1s de retraso", + "delay_paused": "Retraso en pausa", + "demo_mode": "Modo de demostraci\u00f3n", + "door_open": "Puerta abierta", + "exception": "Excepci\u00f3n", + "factory_diagnostic_mode": "Modo de diagn\u00f3stico de f\u00e1brica", + "hard_stop_or_error": "Parada brusca o error", + "life_test": "Prueba de vida", + "pause": "En pausa", + "power_failure": "Fallo de alimentaci\u00f3n", + "running_maincycle": "Ejecuci\u00f3n del ciclo principal", + "running_postcycle": "Ejecuci\u00f3n del postciclo", + "service_diagnostic_mode": "Modo de diagn\u00f3stico de servicio", + "setting": "Ajuste", + "smart_delay": "Retraso inteligente", + "smart_grid_pause": "Retraso inteligente", + "standby": "En espera", + "system_initialize": "Inicializaci\u00f3n del sistema" + } + }, + "whirlpool_tank": { + "state": { + "100": "100%", + "25": "25%", + "50": "50%", + "active": "Activo", + "empty": "Vac\u00eda", + "unknown": "Desconocido" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/et.json b/homeassistant/components/whirlpool/translations/et.json index 983f599c870..483887b79b7 100644 --- a/homeassistant/components/whirlpool/translations/et.json +++ b/homeassistant/components/whirlpool/translations/et.json @@ -1,8 +1,12 @@ { "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud" + }, "error": { "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamine nurjus", + "no_appliances": "Toetatud seadmeid ei leitud", "unknown": "Ootamatu t\u00f5rge" }, "step": { @@ -13,5 +17,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "L\u00f5petatud", + "customer_focus_mode": "Kliendikesksuse re\u017eiim", + "cycle_filling": "T\u00e4itmine", + "cycle_rinsing": "Loputamine", + "cycle_sensing": "Tuvastamine", + "cycle_soaking": "Leotus", + "cycle_spinning": "Keerutamine", + "cycle_washing": "Pesemine", + "delay_countdown": "Viivituste loendur", + "delay_paused": "Viivitus pausil", + "demo_mode": "Demore\u017eiim", + "door_open": "Uks avatud", + "exception": "Erand", + "factory_diagnostic_mode": "Tehase diagnostika re\u017eiim", + "hard_stop_or_error": "Rike v\u00f5i viga", + "life_test": "Eluea test", + "pause": "Ootel", + "power_failure": "Elektrikatkestus", + "running_maincycle": "P\u00f5hits\u00fckkel", + "running_postcycle": "J\u00e4relts\u00fckkel", + "service_diagnostic_mode": "Teenuse diagnostika re\u017eiim", + "setting": "Seadistamine", + "smart_delay": "Nutikas viivitus", + "smart_grid_pause": "Nutikas viivitus", + "standby": "Ootel", + "system_initialize": "S\u00fcsteemi l\u00e4htestamine" + } + }, + "whirlpool_tank": { + "state": { + "100": "100%", + "25": "25%", + "50": "50%", + "active": "Aktiivne", + "empty": "T\u00fchi", + "unknown": "Teadmata" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/hu.json b/homeassistant/components/whirlpool/translations/hu.json index e1cc19c9c30..4436bc43a5d 100644 --- a/homeassistant/components/whirlpool/translations/hu.json +++ b/homeassistant/components/whirlpool/translations/hu.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Sikertelen csatlakoz\u00e1s", "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s", + "no_appliances": "Nem tal\u00e1lhat\u00f3 t\u00e1mogatott g\u00e9p", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" }, "step": { @@ -13,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "K\u00e9sz", + "customer_focus_mode": "\u00dcgyf\u00e9lk\u00f6zpont\u00fa \u00fczemm\u00f3d", + "cycle_filling": "T\u00f6lt\u00e9si ciklus", + "cycle_rinsing": "\u00d6bl\u00edt\u00e9si ciklus", + "cycle_sensing": "\u00c9rz\u00e9kel\u00e9si ciklus", + "cycle_soaking": "\u00c1ztat\u00e1si ciklus", + "cycle_spinning": "Forg\u00e1si ciklus", + "cycle_washing": "Mos\u00e1si ciklus", + "delay_countdown": "K\u00e9sleltetett visszasz\u00e1ml\u00e1l\u00e1s", + "delay_paused": "K\u00e9sleltet\u00e9s sz\u00fcneteltetve", + "demo_mode": "Demo m\u00f3d", + "door_open": "Ajt\u00f3 nyitva", + "exception": "Kiv\u00e9teles helyzet", + "factory_diagnostic_mode": "Gy\u00e1ri diagnosztikai m\u00f3d", + "hard_stop_or_error": "Azonnali le\u00e1ll\u00e1s hiba ok\u00e1n", + "life_test": "\u00c9let teszt", + "pause": "Sz\u00fcnetel", + "power_failure": "\u00c1ramkimarad\u00e1s", + "running_maincycle": "F\u0151ciklus", + "running_postcycle": "Ut\u00f3ciklus", + "service_diagnostic_mode": "Szerviz diagnosztikai \u00fczemm\u00f3d", + "setting": "Be\u00e1ll\u00edt\u00e1s", + "smart_delay": "Okos k\u00e9sleltet\u00e9s", + "smart_grid_pause": "Smart Grid k\u00e9sleltet\u00e9s", + "standby": "K\u00e9szenl\u00e9t", + "system_initialize": "Rendszer inicializ\u00e1l\u00e1sa" + } + }, + "whirlpool_tank": { + "state": { + "100": "100%", + "25": "25%", + "50": "50%", + "active": "Akt\u00edv", + "empty": "\u00dcres", + "unknown": "Ismeretlen" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/id.json b/homeassistant/components/whirlpool/translations/id.json index 7244ccf8912..560e7ac1c33 100644 --- a/homeassistant/components/whirlpool/translations/id.json +++ b/homeassistant/components/whirlpool/translations/id.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Gagal terhubung", "invalid_auth": "Autentikasi tidak valid", + "no_appliances": "Tidak ditemukan peralatan yang didukung", "unknown": "Kesalahan yang tidak diharapkan" }, "step": { @@ -13,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Lengkap", + "customer_focus_mode": "Mode Fokus Pelanggan", + "cycle_filling": "Pengisian Siklus", + "cycle_rinsing": "Pembilasan Siklus", + "cycle_sensing": "Penginderaan Siklus", + "cycle_soaking": "Perendaman Siklus", + "cycle_spinning": "Pemintalan Siklus", + "cycle_washing": "Pencucian Siklus", + "delay_countdown": "Penundaan Hitung Mundur", + "delay_paused": "Penundaan Dijeda", + "demo_mode": "Mode Demo", + "door_open": "Pintu Terbuka", + "exception": "Pengecualian", + "factory_diagnostic_mode": "Mode Diagnostik Pabrik", + "hard_stop_or_error": "Berhenti atau Kesalahan", + "life_test": "Uji Nyala", + "pause": "Jeda", + "power_failure": "Kegagalan Daya", + "running_maincycle": "Menjalankan Siklus Utama", + "running_postcycle": "Menjalankan Pasca-Siklus", + "service_diagnostic_mode": "Mode Diagnostik Layanan", + "setting": "Pengaturan", + "smart_delay": "Penundaan Cerdas", + "smart_grid_pause": "Penundaan Cerdas", + "standby": "Siaga", + "system_initialize": "Inisialisasi Sistem" + } + }, + "whirlpool_tank": { + "state": { + "100": "100%", + "25": "25%", + "50": "50%", + "active": "Aktif", + "empty": "Kosong", + "unknown": "Tidak Dikenal" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/it.json b/homeassistant/components/whirlpool/translations/it.json index eb5545ca85a..79672fe5097 100644 --- a/homeassistant/components/whirlpool/translations/it.json +++ b/homeassistant/components/whirlpool/translations/it.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida", + "no_appliances": "Non sono state trovate apparecchiature supportate", "unknown": "Errore imprevisto" }, "step": { @@ -13,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Completo", + "customer_focus_mode": "Modalit\u00e0 di attenzione al cliente", + "cycle_filling": "Ciclo di riempimento", + "cycle_rinsing": "Ciclo di risciacquo", + "cycle_sensing": "Ciclo di rilevamento", + "cycle_soaking": "Ciclo di ammollo", + "cycle_spinning": "Ciclo di centrifugazione", + "cycle_washing": "Ciclo di lavaggio", + "delay_countdown": "Conto alla rovescia del ritardo", + "delay_paused": "Ritardo in pausa", + "demo_mode": "Modalit\u00e0 Demo", + "door_open": "Porta aperta", + "exception": "Eccezione", + "factory_diagnostic_mode": "Modalit\u00e0 diagnostica di fabbrica", + "hard_stop_or_error": "Arresto brusco o errore", + "life_test": "Test di vita", + "pause": "In pausa", + "power_failure": "Interruzione di corrente", + "running_maincycle": "Ciclo principale in esecuzione", + "running_postcycle": "Postciclo in esecuzione", + "service_diagnostic_mode": "Modalit\u00e0 diagnostica di servizio", + "setting": "Impostazione", + "smart_delay": "Ritardo intelligente", + "smart_grid_pause": "Ritardo intelligente", + "standby": "In attesa", + "system_initialize": "Inizializzazione del sistema" + } + }, + "whirlpool_tank": { + "state": { + "100": "100%", + "25": "25%", + "50": "50%", + "active": "Attivo", + "empty": "Vuoto", + "unknown": "Sconosciuto" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/ja.json b/homeassistant/components/whirlpool/translations/ja.json index 1defa16a2fa..5b9eb4f603f 100644 --- a/homeassistant/components/whirlpool/translations/ja.json +++ b/homeassistant/components/whirlpool/translations/ja.json @@ -13,5 +13,14 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_tank": { + "state": { + "50": "50%" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/lv.json b/homeassistant/components/whirlpool/translations/lv.json new file mode 100644 index 00000000000..7a07d1e6126 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/lv.json @@ -0,0 +1,12 @@ +{ + "entity": { + "sensor": { + "whirlpool_tank": { + "state": { + "25": "25%", + "50": "50%" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/nl.json b/homeassistant/components/whirlpool/translations/nl.json index a4954b83866..a76b1e71a0c 100644 --- a/homeassistant/components/whirlpool/translations/nl.json +++ b/homeassistant/components/whirlpool/translations/nl.json @@ -13,5 +13,37 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Voltooid", + "customer_focus_mode": "Klant focus mode", + "cycle_filling": "Vullen", + "cycle_rinsing": "Spoelen", + "cycle_sensing": "Detectiefase", + "cycle_soaking": "Weken", + "cycle_spinning": "Centrifugeren", + "cycle_washing": "Was cyclus", + "delay_countdown": "Wacht op vertraagde inschakeling", + "delay_paused": "Vertraagde inschakeling gepauzeerd", + "door_open": "Deur open", + "pause": "Gepauzeerd", + "setting": "Instelling", + "standby": "Stand-by" + } + }, + "whirlpool_tank": { + "state": { + "100": "Vol", + "25": "Bijna leeg", + "50": "Half vol", + "active": "Actief", + "empty": "Leeg", + "unknown": "Onbekend" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/no.json b/homeassistant/components/whirlpool/translations/no.json index 4bcac3aada8..43fe0f63526 100644 --- a/homeassistant/components/whirlpool/translations/no.json +++ b/homeassistant/components/whirlpool/translations/no.json @@ -1,8 +1,12 @@ { "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, "error": { "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning", + "no_appliances": "Ingen st\u00f8ttede apparater funnet", "unknown": "Uventet feil" }, "step": { @@ -13,5 +17,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Fullstendig", + "customer_focus_mode": "Kundefokusmodus", + "cycle_filling": "Syklusfylling", + "cycle_rinsing": "Syklus skylling", + "cycle_sensing": "Syklussensor", + "cycle_soaking": "Syklus bl\u00f8tlegging", + "cycle_spinning": "Syklus Spinning", + "cycle_washing": "Syklus vask", + "delay_countdown": "Forsinket nedtelling", + "delay_paused": "Forsinkelse satt p\u00e5 pause", + "demo_mode": "Demomodus", + "door_open": "D\u00f8r \u00e5pen", + "exception": "Unntak", + "factory_diagnostic_mode": "Diagnosemodus til fabrikkstandard", + "hard_stop_or_error": "Hard stopp eller feil", + "life_test": "Livstest", + "pause": "Pauset", + "power_failure": "Str\u00f8mbrudd", + "running_maincycle": "Kj\u00f8rer hovedsyklus", + "running_postcycle": "L\u00f8pende postsyklus", + "service_diagnostic_mode": "Diagnosemodus for tjeneste", + "setting": "Innstilling", + "smart_delay": "Smart forsinkelse", + "smart_grid_pause": "Smart forsinkelse", + "standby": "Avventer", + "system_initialize": "Initialiser systemet" + } + }, + "whirlpool_tank": { + "state": { + "100": "100 %", + "25": "25 %", + "50": "50 %", + "active": "Aktiv", + "empty": "Tom", + "unknown": "Ukjent" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/pl.json b/homeassistant/components/whirlpool/translations/pl.json index 91d1ceab868..d5dd250c657 100644 --- a/homeassistant/components/whirlpool/translations/pl.json +++ b/homeassistant/components/whirlpool/translations/pl.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie", + "no_appliances": "Nie znaleziono obs\u0142ugiwanych urz\u0105dze\u0144", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { @@ -13,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "zako\u0144czono", + "customer_focus_mode": "tryb koncentracji na kliencie", + "cycle_filling": "nape\u0142nianie", + "cycle_rinsing": "p\u0142ukanie", + "cycle_sensing": "wa\u017cenie", + "cycle_soaking": "moczenie", + "cycle_spinning": "wirowanie", + "cycle_washing": "pranie", + "delay_countdown": "op\u00f3\u017anienie", + "delay_paused": "op\u00f3\u017anienie wstrzymane", + "demo_mode": "tryb demonstracyjny", + "door_open": "drzwi otwarte", + "exception": "wyj\u0105tek", + "factory_diagnostic_mode": "tryb diagnostyki fabrycznej", + "hard_stop_or_error": "nag\u0142e zatrzymanie lub b\u0142\u0105d", + "life_test": "test", + "pause": "wstrzymanie", + "power_failure": "awaria zasilania", + "running_maincycle": "cykl g\u0142\u00f3wny", + "running_postcycle": "cykl ko\u0144cowy", + "service_diagnostic_mode": "tryb diagnostyki serwisowej", + "setting": "ustawianie", + "smart_delay": "inteligentne op\u00f3\u017anienie", + "smart_grid_pause": "inteligentne op\u00f3\u017anienie", + "standby": "tryb czuwania", + "system_initialize": "inicjalizacja systemu" + } + }, + "whirlpool_tank": { + "state": { + "100": "100%", + "25": "25%", + "50": "50%", + "active": "aktywny", + "empty": "pusty", + "unknown": "nieznany" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/pt-BR.json b/homeassistant/components/whirlpool/translations/pt-BR.json index efdc82ab438..fe81016da5d 100644 --- a/homeassistant/components/whirlpool/translations/pt-BR.json +++ b/homeassistant/components/whirlpool/translations/pt-BR.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Falha ao conectar", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_appliances": "Nenhum dispositivo compat\u00edvel encontrado", "unknown": "Erro inesperado" }, "step": { @@ -13,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Completo", + "customer_focus_mode": "Modo de foco no cliente", + "cycle_filling": "Ciclo de enchimento", + "cycle_rinsing": "Ciclo de enx\u00e1gue", + "cycle_sensing": "Detec\u00e7\u00e3o de ciclo", + "cycle_soaking": "Ciclo de Imers\u00e3o", + "cycle_spinning": "Ciclo girando", + "cycle_washing": "Ciclo de lavagem", + "delay_countdown": "Atraso na contagem regressiva", + "delay_paused": "Atraso pausado", + "demo_mode": "Modo de demonstra\u00e7\u00e3o", + "door_open": "Porta aberta", + "exception": "Exce\u00e7\u00e3o", + "factory_diagnostic_mode": "Modo de diagn\u00f3stico de f\u00e1brica", + "hard_stop_or_error": "Parada brusca ou erro", + "life_test": "Teste de Vida", + "pause": "Pausado", + "power_failure": "Falha de energia", + "running_maincycle": "Executando o Maincycle", + "running_postcycle": "Executando o Postcycle", + "service_diagnostic_mode": "Modo de diagn\u00f3stico de servi\u00e7o", + "setting": "Configura\u00e7\u00e3o", + "smart_delay": "Atraso Inteligente", + "smart_grid_pause": "Atraso Inteligente", + "standby": "Em espera", + "system_initialize": "Inicializa\u00e7\u00e3o do sistema" + } + }, + "whirlpool_tank": { + "state": { + "100": "100%", + "25": "25%", + "50": "50%", + "active": "Ativo", + "empty": "Vazio", + "unknown": "Desconhecido" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/pt.json b/homeassistant/components/whirlpool/translations/pt.json index ce1bf4bb4b8..4c010d713e6 100644 --- a/homeassistant/components/whirlpool/translations/pt.json +++ b/homeassistant/components/whirlpool/translations/pt.json @@ -3,5 +3,15 @@ "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" } + }, + "entity": { + "sensor": { + "whirlpool_tank": { + "state": { + "empty": "Vazio", + "unknown": "Desconhecido" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/ru.json b/homeassistant/components/whirlpool/translations/ru.json index 994a287efd7..fe8b177efbd 100644 --- a/homeassistant/components/whirlpool/translations/ru.json +++ b/homeassistant/components/whirlpool/translations/ru.json @@ -1,8 +1,12 @@ { "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.", + "no_appliances": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { @@ -13,5 +17,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e", + "customer_focus_mode": "\u0420\u0435\u0436\u0438\u043c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u043e\u0440\u0438\u0435\u043d\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0441\u0442\u0438", + "cycle_filling": "\u0426\u0438\u043a\u043b \u043d\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f", + "cycle_rinsing": "\u0426\u0438\u043a\u043b \u043f\u043e\u043b\u043e\u0441\u043a\u0430\u043d\u0438\u044f", + "cycle_sensing": "\u0426\u0438\u043a\u043b \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f", + "cycle_soaking": "\u0426\u0438\u043a\u043b \u0437\u0430\u043c\u0430\u0447\u0438\u0432\u0430\u043d\u0438\u044f", + "cycle_spinning": "\u0426\u0438\u043a\u043b \u043e\u0442\u0436\u0438\u043c\u0430", + "cycle_washing": "\u0426\u0438\u043a\u043b \u0441\u0442\u0438\u0440\u043a\u0438", + "delay_countdown": "\u041e\u0442\u0441\u0440\u043e\u0447\u043a\u0430 \u0441\u0442\u0430\u0440\u0442\u0430", + "delay_paused": "\u041e\u0442\u0441\u0440\u043e\u0447\u043a\u0430 \u0441\u0442\u0430\u0440\u0442\u0430 \u043f\u0440\u0438\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0430", + "demo_mode": "\u0414\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c", + "door_open": "\u0414\u0432\u0435\u0440\u0446\u0430 \u043e\u0442\u043a\u0440\u044b\u0442\u0430", + "exception": "\u0418\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "factory_diagnostic_mode": "\u0420\u0435\u0436\u0438\u043c \u0437\u0430\u0432\u043e\u0434\u0441\u043a\u043e\u0439 \u0434\u0438\u0430\u0433\u043d\u043e\u0441\u0442\u0438\u043a\u0438", + "hard_stop_or_error": "\u041f\u0440\u0438\u043d\u0443\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0438\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0430", + "life_test": "\u0422\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435", + "pause": "\u041f\u0430\u0443\u0437\u0430", + "power_failure": "\u0421\u0431\u043e\u0439 \u043f\u0438\u0442\u0430\u043d\u0438\u044f", + "running_maincycle": "\u0417\u0430\u043f\u0443\u0441\u043a \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e \u0446\u0438\u043a\u043b\u0430", + "running_postcycle": "\u0417\u0430\u043f\u0443\u0441\u043a \u043f\u043e\u0441\u0442\u0446\u0438\u043a\u043b\u0430", + "service_diagnostic_mode": "\u0420\u0435\u0436\u0438\u043c \u0441\u0435\u0440\u0432\u0438\u0441\u043d\u043e\u0439 \u0434\u0438\u0430\u0433\u043d\u043e\u0441\u0442\u0438\u043a\u0438", + "setting": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", + "smart_delay": "\u0418\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u0430\u044f \u0437\u0430\u0434\u0435\u0440\u0436\u043a\u0430", + "smart_grid_pause": "\u0418\u043d\u0442\u0435\u043b\u043b\u0435\u043a\u0442\u0443\u0430\u043b\u044c\u043d\u0430\u044f \u0437\u0430\u0434\u0435\u0440\u0436\u043a\u0430", + "standby": "\u041e\u0436\u0438\u0434\u0430\u043d\u0438\u0435", + "system_initialize": "\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u044b" + } + }, + "whirlpool_tank": { + "state": { + "100": "100%", + "25": "25%", + "50": "50%", + "active": "\u0410\u043a\u0442\u0438\u0432\u043d\u043e", + "empty": "\u041f\u0443\u0441\u0442\u043e", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/sk.json b/homeassistant/components/whirlpool/translations/sk.json index 8beea53674a..eff41038b9a 100644 --- a/homeassistant/components/whirlpool/translations/sk.json +++ b/homeassistant/components/whirlpool/translations/sk.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Nepodarilo sa pripoji\u0165", "invalid_auth": "Neplatn\u00e9 overenie", + "no_appliances": "Nena\u0161li sa \u017eiadne podporovan\u00e9 zariadenia", "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" }, "step": { @@ -13,5 +14,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Kompletn\u00e9", + "customer_focus_mode": "Re\u017eim zamerania na z\u00e1kazn\u00edka", + "cycle_filling": "Cyklus plnenia", + "cycle_rinsing": "Cyklus oplachovania", + "cycle_sensing": "Cyklus sn\u00edmania", + "cycle_soaking": "Cyklus nam\u00e1\u010dania", + "cycle_spinning": "Cyklus Spinning", + "cycle_washing": "Cyklus prania", + "delay_countdown": "Oneskoren\u00e9 odpo\u010d\u00edtavanie", + "delay_paused": "Oneskorenie pozastaven\u00e9", + "demo_mode": "Demo re\u017eim", + "door_open": "Dvere otvoren\u00e9", + "exception": "V\u00fdnimka", + "factory_diagnostic_mode": "Tov\u00e1rensk\u00fd diagnostick\u00fd re\u017eim", + "hard_stop_or_error": "Hard Stop alebo chyba", + "life_test": "\u017divotn\u00e1 sk\u00fa\u0161ka", + "pause": "Pozastaven\u00fd", + "power_failure": "V\u00fdpadok nap\u00e1jania", + "running_maincycle": "Spustenie hlavn\u00e9ho cyklu", + "running_postcycle": "Spustenie postcyklu", + "service_diagnostic_mode": "Servisn\u00fd diagnostick\u00fd re\u017eim", + "setting": "Nastavenie", + "smart_delay": "Inteligentn\u00e9 oneskorenie", + "smart_grid_pause": "Inteligentn\u00e9 oneskorenie", + "standby": "Pohotovostn\u00fd re\u017eim", + "system_initialize": "Inicializ\u00e1cia syst\u00e9mu" + } + }, + "whirlpool_tank": { + "state": { + "100": "100%", + "25": "25%", + "50": "50%", + "active": "akt\u00edvny", + "empty": "Pr\u00e1zdny", + "unknown": "Nezn\u00e1my" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/tr.json b/homeassistant/components/whirlpool/translations/tr.json index 6bb59e7943a..f3761ef65ad 100644 --- a/homeassistant/components/whirlpool/translations/tr.json +++ b/homeassistant/components/whirlpool/translations/tr.json @@ -3,6 +3,7 @@ "error": { "cannot_connect": "Ba\u011flanma hatas\u0131", "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "no_appliances": "Desteklenen cihaz bulunamad\u0131", "unknown": "Beklenmeyen hata" }, "step": { @@ -13,5 +14,46 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "Tamamland\u0131", + "customer_focus_mode": "M\u00fc\u015fteri Odak Modu", + "cycle_filling": "Dolduruluyor", + "cycle_rinsing": "Durulan\u0131yor", + "cycle_sensing": "Alg\u0131lan\u0131yor", + "cycle_soaking": "Islat\u0131l\u0131yor", + "cycle_spinning": "Kurutuluyor", + "cycle_washing": "Y\u0131kan\u0131yor", + "delay_countdown": "Gecikme Geri Say\u0131m\u0131", + "delay_paused": "Gecikme Duraklat\u0131ld\u0131", + "demo_mode": "Tan\u0131t\u0131m Modu", + "door_open": "Kap\u0131 A\u00e7\u0131k", + "exception": "\u0130stisna", + "factory_diagnostic_mode": "Fabrika Te\u015fhis Modu", + "hard_stop_or_error": "Sert Duru\u015f veya Hata", + "life_test": "Ya\u015fam Testi", + "pause": "Durduruldu", + "power_failure": "Elektrik Kesintisi", + "running_maincycle": "Ana y\u0131kama \u00e7al\u0131\u015f\u0131yor", + "running_postcycle": "Kurutma \u00e7al\u0131\u015f\u0131yor", + "service_diagnostic_mode": "Servis Te\u015fhis Modu", + "setting": "Ayar", + "smart_delay": "Ak\u0131ll\u0131 Gecikme", + "smart_grid_pause": "Ak\u0131ll\u0131 Gecikme", + "standby": "Bekleme modu", + "system_initialize": "Sistem Ba\u015flatma" + } + }, + "whirlpool_tank": { + "state": { + "active": "Etkin", + "empty": "Bo\u015f", + "unknown": "Bilinmeyen" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/uk.json b/homeassistant/components/whirlpool/translations/uk.json new file mode 100644 index 00000000000..bd8dc673c82 --- /dev/null +++ b/homeassistant/components/whirlpool/translations/uk.json @@ -0,0 +1,39 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "cycle_washing": "\u0426\u0438\u043a\u043b \u043f\u0440\u0430\u043d\u043d\u044f", + "delay_paused": "Delay Paused", + "door_open": "\u0412\u0456\u0434\u0447\u0438\u043d\u0435\u043d\u0456 \u0434\u0432\u0435\u0440\u0456", + "exception": "\u0412\u0438\u043d\u044f\u0442\u043e\u043a", + "pause": "\u041f\u0440\u0438\u0437\u0443\u043f\u0438\u043d\u0435\u043d\u043e", + "power_failure": "\u0417\u0431\u0456\u0439 \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f", + "setting": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f", + "standby": "\u0420\u0435\u0436\u0438\u043c \u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f", + "system_initialize": "\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0438" + } + }, + "whirlpool_tank": { + "state": { + "100": "100%", + "25": "25%", + "50": "50%", + "active": "\u0410\u043a\u0442\u0438\u0432\u043d\u0438\u0439", + "empty": "\u041f\u043e\u0440\u043e\u0436\u043d\u0456\u0439", + "unknown": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u043e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/whirlpool/translations/zh-Hant.json b/homeassistant/components/whirlpool/translations/zh-Hant.json index a3784595b65..a56b63a9b3a 100644 --- a/homeassistant/components/whirlpool/translations/zh-Hant.json +++ b/homeassistant/components/whirlpool/translations/zh-Hant.json @@ -1,8 +1,12 @@ { "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", + "no_appliances": "\u627e\u4e0d\u5230\u652f\u63f4\u8a2d\u5099\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { @@ -13,5 +17,49 @@ } } } + }, + "entity": { + "sensor": { + "whirlpool_machine": { + "state": { + "complete": "\u5b8c\u6210", + "customer_focus_mode": "\u7528\u6236\u5c08\u6ce8\u6a21\u5f0f", + "cycle_filling": "\u5faa\u74b0\u586b\u5145", + "cycle_rinsing": "\u5faa\u74b0\u6c96\u6d17", + "cycle_sensing": "\u5faa\u74b0\u611f\u61c9", + "cycle_soaking": "\u5faa\u74b0\u6d78\u6ce1", + "cycle_spinning": "\u5faa\u74b0\u812b\u6c34", + "cycle_washing": "\u5faa\u74b0\u6e05\u6d17", + "delay_countdown": "\u5ef6\u9072\u5012\u6578", + "delay_paused": "\u5ef6\u9072\u66ab\u505c", + "demo_mode": "\u5c55\u793a\u6a21\u5f0f", + "door_open": "\u9580\u958b\u555f", + "exception": "\u4f8b\u5916", + "factory_diagnostic_mode": "\u5de5\u5ee0\u8a3a\u65b7\u6a21\u5f0f", + "hard_stop_or_error": "\u9000\u51fa\u6216\u932f\u8aa4", + "life_test": "\u58fd\u547d\u6e2c\u8a66", + "pause": "\u5df2\u66ab\u505c", + "power_failure": "\u96fb\u6e90\u6545\u969c", + "running_maincycle": "\u57f7\u884c\u4e3b\u5faa\u74b0", + "running_postcycle": "\u57f7\u884c\u5f8c\u5faa\u74b0", + "service_diagnostic_mode": "\u670d\u52d9\u8a3a\u65b7\u6a21\u5f0f", + "setting": "\u8a2d\u5b9a", + "smart_delay": "\u667a\u80fd\u5ef6\u9072", + "smart_grid_pause": "\u667a\u80fd\u5ef6\u9072", + "standby": "\u5f85\u547d", + "system_initialize": "\u7cfb\u7d71\u521d\u59cb\u5316" + } + }, + "whirlpool_tank": { + "state": { + "100": "100%", + "25": "25%", + "50": "50%", + "active": "\u555f\u7528", + "empty": "\u7a7a\u767d", + "unknown": "\u672a\u77e5" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/whirlpool/util.py b/homeassistant/components/whirlpool/util.py new file mode 100644 index 00000000000..55b094f76ac --- /dev/null +++ b/homeassistant/components/whirlpool/util.py @@ -0,0 +1,8 @@ +"""Utility functions for the Whirlpool Sixth Sense integration.""" + +from whirlpool.backendselector import Brand, Region + + +def get_brand_for_region(region: Region) -> Brand: + """Get the correct brand for each region.""" + return Brand.Maytag if region == Region.US else Brand.Whirlpool diff --git a/homeassistant/components/whois/manifest.json b/homeassistant/components/whois/manifest.json index 104b583ea3a..3977dde00d0 100644 --- a/homeassistant/components/whois/manifest.json +++ b/homeassistant/components/whois/manifest.json @@ -2,7 +2,7 @@ "domain": "whois", "name": "Whois", "documentation": "https://www.home-assistant.io/integrations/whois", - "requirements": ["whois==0.9.16"], + "requirements": ["whois==0.9.23"], "config_flow": true, "codeowners": ["@frenck"], "iot_class": "cloud_polling", diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 6bf2b9d2f02..9f4d7f07d52 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -139,7 +139,9 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up the platform from config_entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: DataUpdateCoordinator[Domain | None] = hass.data[DOMAIN][ + entry.entry_id + ] async_add_entities( [ WhoisSensorEntity( @@ -152,7 +154,9 @@ async def async_setup_entry( ) -class WhoisSensorEntity(CoordinatorEntity, SensorEntity): +class WhoisSensorEntity( + CoordinatorEntity[DataUpdateCoordinator[Domain | None]], SensorEntity +): """Implementation of a WHOIS sensor.""" entity_description: WhoisSensorEntityDescription @@ -160,7 +164,7 @@ class WhoisSensorEntity(CoordinatorEntity, SensorEntity): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[Domain | None], description: WhoisSensorEntityDescription, domain: str, ) -> None: diff --git a/homeassistant/components/wiffi/translations/tr.json b/homeassistant/components/wiffi/translations/tr.json index c1efc71bc1b..b5620b81795 100644 --- a/homeassistant/components/wiffi/translations/tr.json +++ b/homeassistant/components/wiffi/translations/tr.json @@ -10,7 +10,7 @@ "data": { "port": "Port" }, - "title": "WIFI cihazlar\u0131 i\u00e7in TCP sunucusunu kurun" + "title": "WIFFI cihazlar\u0131 i\u00e7in TCP sunucusunu kurun" } } }, diff --git a/homeassistant/components/wilight/translations/el.json b/homeassistant/components/wilight/translations/el.json index 79bb1d443c4..168d8d5649e 100644 --- a/homeassistant/components/wilight/translations/el.json +++ b/homeassistant/components/wilight/translations/el.json @@ -8,7 +8,7 @@ "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 WiLight {name} ;\n\n \u03a5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9: {components}" + "description": "\u03a5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c4\u03b1 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1: {components}" } } } diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index c1616a062ca..9b12c825a20 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -10,7 +10,7 @@ from enum import IntEnum from http import HTTPStatus import logging import re -from typing import Any, Union +from typing import Any from aiohttp.web import Response import requests @@ -241,7 +241,7 @@ class DataManager: update_method=self.async_subscribe_webhook, ) self.poll_data_update_coordinator = DataUpdateCoordinator[ - Union[dict[MeasureType, Any], None] + dict[MeasureType, Any] | None ]( hass, _LOGGER, diff --git a/homeassistant/components/withings/const.py b/homeassistant/components/withings/const.py index 20ad91f16cf..1193b6f612a 100644 --- a/homeassistant/components/withings/const.py +++ b/homeassistant/components/withings/const.py @@ -1,5 +1,4 @@ """Constants used by the Withings component.""" -from homeassistant import const from homeassistant.backports.enum import StrEnum CONF_PROFILES = "profiles" @@ -55,6 +54,6 @@ class Measurement(StrEnum): SCORE_POINTS = "points" UOM_BEATS_PER_MINUTE = "bpm" -UOM_BREATHS_PER_MINUTE = f"br/{const.TIME_MINUTES}" +UOM_BREATHS_PER_MINUTE = "br/min" UOM_FREQUENCY = "times" UOM_MMHG = "mmhg" diff --git a/homeassistant/components/withings/translations/sk.json b/homeassistant/components/withings/translations/sk.json index c964e8a23fb..74162f20eb5 100644 --- a/homeassistant/components/withings/translations/sk.json +++ b/homeassistant/components/withings/translations/sk.json @@ -25,7 +25,7 @@ "title": "U\u017e\u00edvate\u013esk\u00fd profil." }, "reauth_confirm": { - "description": "Ak chcete na\u010falej dost\u00e1va\u0165 \u00fadaje Withings, profil \u201e{profile}\u201c sa mus\u00ed znova overi\u0165.", + "description": "Ak chcete na\u010falej dost\u00e1va\u0165 \u00fadaje Withings, profil \"{profile}\" sa mus\u00ed znova overi\u0165.", "title": "Znova overi\u0165 integr\u00e1ciu" } } diff --git a/homeassistant/components/wiz/config_flow.py b/homeassistant/components/wiz/config_flow.py index b1bce3eda0d..f2d109bd6bb 100644 --- a/homeassistant/components/wiz/config_flow.py +++ b/homeassistant/components/wiz/config_flow.py @@ -114,11 +114,11 @@ class WizConfigFlow(ConfigFlow, domain=DOMAIN): 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}, - ) + + 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 = { diff --git a/homeassistant/components/wiz/entity.py b/homeassistant/components/wiz/entity.py index 633fb71f165..67608db157a 100644 --- a/homeassistant/components/wiz/entity.py +++ b/homeassistant/components/wiz/entity.py @@ -2,7 +2,7 @@ from __future__ import annotations from abc import abstractmethod -from typing import Any, Optional +from typing import Any from pywizlight.bulblibrary import BulbType @@ -18,7 +18,7 @@ from homeassistant.helpers.update_coordinator import ( from .models import WizData -class WizEntity(CoordinatorEntity[DataUpdateCoordinator[Optional[float]]], Entity): +class WizEntity(CoordinatorEntity[DataUpdateCoordinator[float | None]], Entity): """Representation of WiZ entity.""" _attr_has_entity_name = True diff --git a/homeassistant/components/wiz/models.py b/homeassistant/components/wiz/models.py index efbb2a664b1..547ff830303 100644 --- a/homeassistant/components/wiz/models.py +++ b/homeassistant/components/wiz/models.py @@ -1,4 +1,6 @@ """WiZ integration models.""" +from __future__ import annotations + from dataclasses import dataclass from pywizlight import wizlight @@ -10,6 +12,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator class WizData: """Data for the wiz integration.""" - coordinator: DataUpdateCoordinator + coordinator: DataUpdateCoordinator[float | None] bulb: wizlight scenes: list diff --git a/homeassistant/components/wiz/number.py b/homeassistant/components/wiz/number.py index fc855e410fe..a57e4f4fd22 100644 --- a/homeassistant/components/wiz/number.py +++ b/homeassistant/components/wiz/number.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Coroutine from dataclasses import dataclass -from typing import Optional, cast +from typing import cast from pywizlight import wizlight @@ -54,7 +54,7 @@ NUMBERS: tuple[WizNumberEntityDescription, ...] = ( native_step=1, icon="mdi:speedometer", name="Effect speed", - value_fn=lambda device: cast(Optional[int], device.state.get_speed()), + value_fn=lambda device: cast(int | None, device.state.get_speed()), set_value_fn=_async_set_speed, required_feature="effect", entity_category=EntityCategory.CONFIG, @@ -66,7 +66,7 @@ NUMBERS: tuple[WizNumberEntityDescription, ...] = ( native_step=1, icon="mdi:floor-lamp-dual", name="Dual head ratio", - value_fn=lambda device: cast(Optional[int], device.state.get_ratio()), + value_fn=lambda device: cast(int | None, device.state.get_ratio()), set_value_fn=_async_set_ratio, required_feature="dual_head", entity_category=EntityCategory.CONFIG, diff --git a/homeassistant/components/wiz/translations/lv.json b/homeassistant/components/wiz/translations/lv.json index dcf6c75a653..cfbcecbfa98 100644 --- a/homeassistant/components/wiz/translations/lv.json +++ b/homeassistant/components/wiz/translations/lv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + }, "step": { "pick_device": { "data": { diff --git a/homeassistant/components/wiz/translations/tr.json b/homeassistant/components/wiz/translations/tr.json index 15b2b683a50..af2c6f0932f 100644 --- a/homeassistant/components/wiz/translations/tr.json +++ b/homeassistant/components/wiz/translations/tr.json @@ -15,7 +15,7 @@ "flow_title": "{name} ({host})", "step": { "discovery_confirm": { - "description": "{name} ( {host} ) kurulumu yapmak istiyor musunuz?" + "description": "{name} ( {host} ) kurmak istiyor musunuz?" }, "pick_device": { "data": { diff --git a/homeassistant/components/wiz/translations/uk.json b/homeassistant/components/wiz/translations/uk.json new file mode 100644 index 00000000000..dc71b300685 --- /dev/null +++ b/homeassistant/components/wiz/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423 \u043c\u0435\u0440\u0435\u0436\u0456 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "pick_device": { + "data": { + "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/helpers.py b/homeassistant/components/wled/helpers.py index 32503383b07..85dcf9ca800 100644 --- a/homeassistant/components/wled/helpers.py +++ b/homeassistant/components/wled/helpers.py @@ -2,9 +2,8 @@ from __future__ import annotations from collections.abc import Callable, Coroutine -from typing import Any, TypeVar +from typing import Any, Concatenate, ParamSpec, TypeVar -from typing_extensions import Concatenate, ParamSpec from wled import WLEDConnectionError, WLEDError from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index 9f241756e90..4a36fededbe 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -65,10 +65,11 @@ class WLEDNightlightSwitch(WLEDEntity, SwitchEntity): @property def extra_state_attributes(self) -> dict[str, Any] | None: """Return the state attributes of the entity.""" + state = self.coordinator.data.state return { - ATTR_DURATION: self.coordinator.data.state.nightlight.duration, - ATTR_FADE: self.coordinator.data.state.nightlight.fade, - ATTR_TARGET_BRIGHTNESS: self.coordinator.data.state.nightlight.target_brightness, + ATTR_DURATION: state.nightlight.duration, + ATTR_FADE: state.nightlight.fade, + ATTR_TARGET_BRIGHTNESS: state.nightlight.target_brightness, } @property diff --git a/homeassistant/components/wled/translations/lv.json b/homeassistant/components/wled/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/wled/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/ru.json b/homeassistant/components/wled/translations/ru.json index 96b37768f02..360d0037629 100644 --- a/homeassistant/components/wled/translations/ru.json +++ b/homeassistant/components/wled/translations/ru.json @@ -37,7 +37,7 @@ "step": { "init": { "data": { - "keep_master_light": "\u0414\u0435\u0440\u0436\u0430\u0442\u044c \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0441\u0432\u0435\u0442 \u0434\u0430\u0436\u0435 \u0441 \u043e\u0434\u043d\u0438\u043c \u0441\u0432\u0435\u0442\u043e\u0434\u0438\u043e\u0434\u043d\u044b\u043c \u0441\u0435\u0433\u043c\u0435\u043d\u0442\u043e\u043c." + "keep_master_light": "\u0414\u043e\u0431\u0430\u0432\u043b\u044f\u0442\u044c \u043c\u0430\u0441\u0442\u0435\u0440-\u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u043f\u0440\u0438 \u043b\u044e\u0431\u043e\u043c \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u0435 \u0441\u0435\u0433\u043c\u0435\u043d\u0442\u043e\u0432" } } } diff --git a/homeassistant/components/wled/translations/tr.json b/homeassistant/components/wled/translations/tr.json index 5c8b3b0f135..636278cccac 100644 --- a/homeassistant/components/wled/translations/tr.json +++ b/homeassistant/components/wled/translations/tr.json @@ -22,6 +22,17 @@ } } }, + "entity": { + "select": { + "live_override": { + "state": { + "0": "Kapal\u0131", + "1": "A\u00e7\u0131k", + "2": "Cihaz yeniden ba\u015flat\u0131lana kadar" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/wolflink/translations/bg.json b/homeassistant/components/wolflink/translations/bg.json index 84521f7891f..e578f506e1b 100644 --- a/homeassistant/components/wolflink/translations/bg.json +++ b/homeassistant/components/wolflink/translations/bg.json @@ -28,6 +28,8 @@ "state": { "1_x_warmwasser": "1 x DHW", "cooling": "\u041e\u0445\u043b\u0430\u0436\u0434\u0430\u043d\u0435", + "heizbetrieb": "\u0420\u0435\u0436\u0438\u043c \u043d\u0430 \u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435", + "heizung": "\u041e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435", "initialisierung": "\u0418\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435", "kalibration": "\u041a\u0430\u043b\u0438\u0431\u0440\u0438\u0440\u0430\u043d\u0435", "test": "\u0422\u0435\u0441\u0442", diff --git a/homeassistant/components/wolflink/translations/lt.json b/homeassistant/components/wolflink/translations/lt.json new file mode 100644 index 00000000000..595b1233573 --- /dev/null +++ b/homeassistant/components/wolflink/translations/lt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/nl.json b/homeassistant/components/wolflink/translations/nl.json index b2b792070f8..b664501673e 100644 --- a/homeassistant/components/wolflink/translations/nl.json +++ b/homeassistant/components/wolflink/translations/nl.json @@ -30,8 +30,11 @@ "state": { "aktiviert": "Geactiveerd", "aus": "Uitgeschakeld", + "automatik_aus": "Automatisch UIT", + "automatik_ein": "Automatisch AAN", "deaktiviert": "Inactief", - "ein": "Ingeschakeld" + "ein": "Ingeschakeld", + "storung": "Fout" } } } diff --git a/homeassistant/components/wolflink/translations/sensor.bg.json b/homeassistant/components/wolflink/translations/sensor.bg.json index 52d381f1b26..6aadf5958d6 100644 --- a/homeassistant/components/wolflink/translations/sensor.bg.json +++ b/homeassistant/components/wolflink/translations/sensor.bg.json @@ -3,9 +3,11 @@ "wolflink__state": { "1_x_warmwasser": "1 x DHW", "auto": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u043d", + "cooling": "\u041e\u0445\u043b\u0430\u0436\u0434\u0430\u043d\u0435", "deaktiviert": "\u041d\u0435\u0430\u043a\u0442\u0438\u0432\u0435\u043d", "dhw_prior": "DHWPrior", "gasdruck": "\u041d\u0430\u043b\u044f\u0433\u0430\u043d\u0435 \u043d\u0430 \u0433\u0430\u0437\u0430", + "heizbetrieb": "\u0420\u0435\u0436\u0438\u043c \u043d\u0430 \u043e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435", "heizung": "\u041e\u0442\u043e\u043f\u043b\u0435\u043d\u0438\u0435", "kalibration": "\u041a\u0430\u043b\u0438\u0431\u0440\u0438\u0440\u0430\u043d\u0435", "test": "\u0422\u0435\u0441\u0442", diff --git a/homeassistant/components/wolflink/translations/tr.json b/homeassistant/components/wolflink/translations/tr.json index aa7dbb1747a..eb90d807f40 100644 --- a/homeassistant/components/wolflink/translations/tr.json +++ b/homeassistant/components/wolflink/translations/tr.json @@ -23,5 +23,94 @@ "title": "KURT SmartSet ba\u011flant\u0131s\u0131" } } + }, + "entity": { + "sensor": { + "state": { + "state": { + "1_x_warmwasser": "1 x DHW", + "abgasklappe": "Baca gaz\u0131 damperi", + "absenkbetrieb": "Gerileme modu", + "absenkstop": "Gerileme durdurma", + "aktiviert": "Aktif", + "antilegionellenfunktion": "Anti-lejyonella Fonksiyonu", + "at_abschaltung": "OT kapatma", + "at_frostschutz": "OT donma korumas\u0131", + "aus": "Devre d\u0131\u015f\u0131", + "auto": "Otomatik", + "auto_off_cool": "Otomatik Kapal\u0131So\u011futma", + "auto_on_cool": "Otomatik A\u00e7\u0131kSo\u011futma", + "automatik_aus": "Otomatik KAPALI", + "automatik_ein": "Otomatik A\u00c7IK", + "bereit_keine_ladung": "Haz\u0131r, y\u00fcklenmiyor", + "betrieb_ohne_brenner": "Br\u00fcl\u00f6rs\u00fcz \u00e7al\u0131\u015fma", + "cooling": "So\u011futuluyor", + "deaktiviert": "Etkin de\u011fil", + "dhw_prior": "DHWPrior", + "eco": "Eko", + "ein": "Etkin", + "estrichtrocknung": "\u015eap kurutma", + "externe_deaktivierung": "Harici devre d\u0131\u015f\u0131 b\u0131rakma", + "fernschalter_ein": "Uzaktan kumanda etkin", + "frost_heizkreis": "Is\u0131tma devresi donmas\u0131", + "frost_warmwasser": "DHW donma koruma", + "frostschutz": "Donma korumas\u0131", + "gasdruck": "Gaz bas\u0131nc\u0131", + "glt_betrieb": "BMS modu", + "gradienten_uberwachung": "Gradyan izleme", + "heizbetrieb": "Is\u0131tma modu", + "heizgerat_mit_speicher": "Silindirli kazan", + "heizung": "Is\u0131t\u0131l\u0131yor", + "initialisierung": "Ba\u015flatma", + "kalibration": "Kalibrasyon", + "kalibration_heizbetrieb": "Is\u0131tma modu kalibrasyonu", + "kalibration_kombibetrieb": "Kombi modu kalibrasyonu", + "kalibration_warmwasserbetrieb": "DHW kalibrasyonu", + "kaskadenbetrieb": "Kademeli i\u015flem", + "kombibetrieb": "Kombi modu", + "kombigerat": "Kombi", + "kombigerat_mit_solareinbindung": "G\u00fcne\u015f enerjisi entegreli kombi", + "mindest_kombizeit": "Minimum kombi s\u00fcresi", + "nachlauf_heizkreispumpe": "Is\u0131tma devresi pompas\u0131 \u00e7al\u0131\u015fmas\u0131", + "nachspulen": "Temizleme sonras\u0131", + "nur_heizgerat": "Sadece kazan", + "parallelbetrieb": "Paralel mod", + "partymodus": "Parti modu", + "perm_cooling": "PermSo\u011futma", + "permanent": "Kal\u0131c\u0131", + "permanentbetrieb": "Kal\u0131c\u0131 mod", + "reduzierter_betrieb": "S\u0131n\u0131rl\u0131 mod", + "rt_abschaltung": "RT kapatma", + "rt_frostschutz": "RT donma korumas\u0131", + "ruhekontakt": "Dinlenme konta\u011f\u0131", + "schornsteinfeger": "Emisyon testi", + "smart_grid": "SmartGrid", + "smart_home": "Ak\u0131ll\u0131 Ev", + "softstart": "Yumu\u015fak ba\u015flang\u0131\u00e7", + "solarbetrieb": "G\u00fcne\u015f modu", + "sparbetrieb": "Ekonomi modu", + "sparen": "Ekonomi", + "spreizung_hoch": "dT \u00e7ok geni\u015f", + "spreizung_kf": "KF'yi yay\u0131n", + "stabilisierung": "Stabilizasyon", + "standby": "Bekleme modu", + "start": "Ba\u015flat", + "storung": "Hata", + "taktsperre": "Anti-d\u00f6ng\u00fc", + "telefonfernschalter": "Telefon uzaktan anahtar\u0131", + "test": "Test", + "tpw": "TPW", + "urlaubsmodus": "Tatil modu", + "ventilprufung": "Valf testi", + "vorspulen": "Giri\u015f durulama", + "warmwasser": "DHW", + "warmwasser_schnellstart": "DHW h\u0131zl\u0131 ba\u015flang\u0131\u00e7", + "warmwasserbetrieb": "DHW modu", + "warmwassernachlauf": "DHW \u00e7al\u0131\u015fmas\u0131", + "warmwasservorrang": "DHW \u00f6nceli\u011fi", + "zunden": "Ate\u015fleme" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index fa94319772c..8cd59d36ae7 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -2,7 +2,7 @@ "domain": "workday", "name": "Workday", "documentation": "https://www.home-assistant.io/integrations/workday", - "requirements": ["holidays==0.17.2"], + "requirements": ["holidays==0.18.0"], "codeowners": ["@fabaff"], "quality_scale": "internal", "iot_class": "local_polling", diff --git a/homeassistant/components/xbox_live/translations/ca.json b/homeassistant/components/xbox_live/translations/ca.json index adca372177a..504ba6d258b 100644 --- a/homeassistant/components/xbox_live/translations/ca.json +++ b/homeassistant/components/xbox_live/translations/ca.json @@ -1,7 +1,7 @@ { "issues": { "pending_removal": { - "description": "La integraci\u00f3 Xbox Live est\u00e0 pendent d'eliminar-se de Home Assistant i ja no estar\u00e0 disponible a partir de Home Assistant 2023.2. \n\nLa integraci\u00f3 s'est\u00e0 eliminant, perqu\u00e8 nom\u00e9s \u00e9s \u00fatil per dispositius heretats Xbox 360 i l'API actual necessita una subscripci\u00f3 de pagament. Les consoles m\u00e9s noves s\u00f3n compatibles amb la integraci\u00f3 Xbox de forma gratu\u00efta.\n\nElimina la configuraci\u00f3 YAML d'Xbox Live del fitxer configuration.yaml i reinicia Home Assistant per arreglar aquest error.", + "description": "La integraci\u00f3 Xbox Live est\u00e0 pendent d'eliminar-se de Home Assistant i ja no estar\u00e0 disponible a partir de Home Assistant 2023.2. \n\nLa integraci\u00f3 s'est\u00e0 eliminant, perqu\u00e8 nom\u00e9s \u00e9s \u00fatil per dispositius heretats Xbox 360 i l'API actual necessita una subscripci\u00f3 de pagament. Les consoles m\u00e9s noves s\u00f3n compatibles amb la integraci\u00f3 Xbox de forma gratu\u00efta.\n\nElimineu la configuraci\u00f3 YAML d'Xbox Live del fitxer configuration.yaml i reinicieu Home Assistant per esmenar aquesta incid\u00e8ncia.", "title": "La integraci\u00f3 Xbox Live est\u00e0 sent eliminada" } } diff --git a/homeassistant/components/xbox_live/translations/tr.json b/homeassistant/components/xbox_live/translations/tr.json new file mode 100644 index 00000000000..69cbb2d2205 --- /dev/null +++ b/homeassistant/components/xbox_live/translations/tr.json @@ -0,0 +1,8 @@ +{ + "issues": { + "pending_removal": { + "description": "Xbox Live entegrasyonu, Home Assistant'tan kald\u0131r\u0131lmay\u0131 bekliyor ve Home Assistant 2023.2'den itibaren kullan\u0131lamayacak. \n\n Yaln\u0131zca eski Xbox 360 cihaz\u0131 i\u00e7in kullan\u0131\u015fl\u0131 oldu\u011fundan ve yukar\u0131 ak\u0131\u015f API'si art\u0131k \u00fccretli bir abonelik gerektirdi\u011finden entegrasyon kald\u0131r\u0131l\u0131yor. Daha yeni konsollar, Xbox entegrasyonu taraf\u0131ndan \u00fccretsiz olarak desteklenir. \n\n Bu sorunu \u00e7\u00f6zmek i\u00e7in, configuration.yaml dosyan\u0131zdan Xbox Live YAML yap\u0131land\u0131rmas\u0131n\u0131 kald\u0131r\u0131n ve Home Assistant'\u0131 yeniden ba\u015flat\u0131n.", + "title": "Xbox Live entegrasyonu kald\u0131r\u0131l\u0131yor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/bg.json b/homeassistant/components/xiaomi_aqara/translations/bg.json index de2cba26ce2..450dd41126a 100644 --- a/homeassistant/components/xiaomi_aqara/translations/bg.json +++ b/homeassistant/components/xiaomi_aqara/translations/bg.json @@ -22,8 +22,10 @@ "user": { "data": { "host": "IP \u0430\u0434\u0440\u0435\u0441 (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)", + "interface": "\u041c\u0440\u0435\u0436\u043e\u0432\u0438\u044f\u0442 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441, \u043a\u043e\u0439\u0442\u043e \u0434\u0430 \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430", "mac": "Mac \u0430\u0434\u0440\u0435\u0441 (\u043f\u043e \u0438\u0437\u0431\u043e\u0440)" - } + }, + "description": "\u0410\u043a\u043e IP \u0438 MAC \u0430\u0434\u0440\u0435\u0441\u0438\u0442\u0435 \u0441\u0430 \u043e\u0441\u0442\u0430\u0432\u0435\u043d\u0438 \u043f\u0440\u0430\u0437\u043d\u0438, \u0441\u0435 \u0438\u0437\u043f\u043e\u043b\u0437\u0432\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435." } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/el.json b/homeassistant/components/xiaomi_aqara/translations/el.json index 8a2f85be99d..1fd90115c34 100644 --- a/homeassistant/components/xiaomi_aqara/translations/el.json +++ b/homeassistant/components/xiaomi_aqara/translations/el.json @@ -18,7 +18,7 @@ "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" + "description": "\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" }, "settings": { "data": { @@ -26,7 +26,7 @@ "name": "\u038c\u03bd\u03bf\u03bc\u03b1 \u03c4\u03b7\u03c2 \u03c0\u03cd\u03bb\u03b7\u03c2" }, "description": "\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af (\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 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03b1\u03c5\u03c4\u03cc \u03c4\u03bf \u03c3\u03b5\u03bc\u03b9\u03bd\u03ac\u03c1\u03b9\u03bf: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. \u0395\u03ac\u03bd \u03b4\u03b5\u03bd \u03b4\u03bf\u03b8\u03b5\u03af \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af, \u03bc\u03cc\u03bd\u03bf \u03bf\u03b9 \u03b1\u03b9\u03c3\u03b8\u03b7\u03c4\u03ae\u03c1\u03b5\u03c2 \u03b8\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03c0\u03c1\u03bf\u03c3\u03b2\u03ac\u03c3\u03b9\u03bc\u03bf\u03b9.", - "title": "\u03a0\u03cd\u03bb\u03b7 Xiaomi Aqara, \u03c0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ad\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2" + "title": "\u03a0\u03c1\u03bf\u03b1\u03b9\u03c1\u03b5\u03c4\u03b9\u03ba\u03ad\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2" }, "user": { "data": { @@ -34,7 +34,7 @@ "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)" }, - "description": "\u03a3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03b7\u03bd \u03c0\u03cd\u03bb\u03b7 Xiaomi Aqara Gateway, \u03b5\u03ac\u03bd \u03bf\u03b9 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 IP \u03ba\u03b1\u03b9 MAC \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03bd\u03bf\u03c5\u03bd \u03ba\u03b5\u03bd\u03ad\u03c2, \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." + "description": "\u0395\u03ac\u03bd \u03bf\u03b9 \u03b4\u03b9\u03b5\u03c5\u03b8\u03cd\u03bd\u03c3\u03b5\u03b9\u03c2 IP \u03ba\u03b1\u03b9 MAC \u03bc\u03b5\u03af\u03bd\u03bf\u03c5\u03bd \u03ba\u03b5\u03bd\u03ad\u03c2, \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" } } } diff --git a/homeassistant/components/xiaomi_aqara/translations/lv.json b/homeassistant/components/xiaomi_aqara/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/__init__.py b/homeassistant/components/xiaomi_ble/__init__.py index 8ed18c045d4..372afe4b3c5 100644 --- a/homeassistant/components/xiaomi_ble/__init__.py +++ b/homeassistant/components/xiaomi_ble/__init__.py @@ -8,6 +8,7 @@ from xiaomi_ble.parser import EncryptionScheme from homeassistant import config_entries from homeassistant.components.bluetooth import ( + DOMAIN as BLUETOOTH_DOMAIN, BluetoothScanningMode, BluetoothServiceInfoBleak, async_ble_device_from_address, @@ -18,8 +19,9 @@ from homeassistant.components.bluetooth.active_update_processor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import CoreState, HomeAssistant +from homeassistant.helpers.device_registry import DeviceRegistry, async_get -from .const import DOMAIN +from .const import DOMAIN, XIAOMI_BLE_EVENT, XiaomiBleEvent PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -31,9 +33,35 @@ def process_service_info( entry: config_entries.ConfigEntry, data: XiaomiBluetoothDeviceData, service_info: BluetoothServiceInfoBleak, + device_registry: DeviceRegistry, ) -> SensorUpdate: """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data.""" update = data.update(service_info) + if update.events: + address = service_info.device.address + for device_key, event in update.events.items(): + sensor_device_info = update.devices[device_key.device_id] + device = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(BLUETOOTH_DOMAIN, address)}, + manufacturer=sensor_device_info.manufacturer, + model=sensor_device_info.model, + name=sensor_device_info.name, + sw_version=sensor_device_info.sw_version, + hw_version=sensor_device_info.hw_version, + ) + + hass.bus.async_fire( + XIAOMI_BLE_EVENT, + dict( + XiaomiBleEvent( + device_id=device.id, + address=address, + event_type=event.event_type, + event_properties=event.event_properties, + ) + ), + ) # If device isn't pending we know it has seen at least one broadcast with a payload # If that payload was encrypted and the bindkey was not verified then we need to reauth @@ -91,6 +119,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) return await data.async_poll(connectable_device) + device_registry = async_get(hass) coordinator = hass.data.setdefault(DOMAIN, {})[ entry.entry_id ] = ActiveBluetoothProcessorCoordinator( @@ -99,7 +128,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: address=address, mode=BluetoothScanningMode.PASSIVE, update_method=lambda service_info: process_service_info( - hass, entry, data, service_info + hass, entry, data, service_info, device_registry ), needs_poll_method=_needs_poll, poll_method=_async_poll, diff --git a/homeassistant/components/xiaomi_ble/binary_sensor.py b/homeassistant/components/xiaomi_ble/binary_sensor.py index 6fc6c3c2761..1de3afff53f 100644 --- a/homeassistant/components/xiaomi_ble/binary_sensor.py +++ b/homeassistant/components/xiaomi_ble/binary_sensor.py @@ -1,10 +1,9 @@ """Support for Xiaomi binary sensors.""" from __future__ import annotations -from typing import Optional - from xiaomi_ble.parser import ( BinarySensorDeviceClass as XiaomiBinarySensorDeviceClass, + ExtendedBinarySensorDeviceClass, SensorUpdate, ) @@ -28,20 +27,48 @@ from .const import DOMAIN from .device import device_key_to_bluetooth_entity_key BINARY_SENSOR_DESCRIPTIONS = { - XiaomiBinarySensorDeviceClass.MOTION: BinarySensorEntityDescription( - key=XiaomiBinarySensorDeviceClass.MOTION, - device_class=BinarySensorDeviceClass.MOTION, + XiaomiBinarySensorDeviceClass.DOOR: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.DOOR, + device_class=BinarySensorDeviceClass.DOOR, ), XiaomiBinarySensorDeviceClass.LIGHT: BinarySensorEntityDescription( key=XiaomiBinarySensorDeviceClass.LIGHT, device_class=BinarySensorDeviceClass.LIGHT, ), + XiaomiBinarySensorDeviceClass.MOISTURE: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.MOISTURE, + device_class=BinarySensorDeviceClass.MOISTURE, + ), + XiaomiBinarySensorDeviceClass.MOTION: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.MOTION, + device_class=BinarySensorDeviceClass.MOTION, + ), + XiaomiBinarySensorDeviceClass.OPENING: BinarySensorEntityDescription( + key=XiaomiBinarySensorDeviceClass.OPENING, + device_class=BinarySensorDeviceClass.OPENING, + ), XiaomiBinarySensorDeviceClass.SMOKE: BinarySensorEntityDescription( key=XiaomiBinarySensorDeviceClass.SMOKE, device_class=BinarySensorDeviceClass.SMOKE, ), - XiaomiBinarySensorDeviceClass.MOISTURE: BinarySensorEntityDescription( - key=XiaomiBinarySensorDeviceClass.MOISTURE, + ExtendedBinarySensorDeviceClass.DEVICE_FORCIBLY_REMOVED: BinarySensorEntityDescription( + key=ExtendedBinarySensorDeviceClass.DEVICE_FORCIBLY_REMOVED, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ExtendedBinarySensorDeviceClass.DOOR_LEFT_OPEN: BinarySensorEntityDescription( + key=ExtendedBinarySensorDeviceClass.DOOR_LEFT_OPEN, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ExtendedBinarySensorDeviceClass.DOOR_STUCK: BinarySensorEntityDescription( + key=ExtendedBinarySensorDeviceClass.DOOR_STUCK, + device_class=BinarySensorDeviceClass.PROBLEM, + ), + ExtendedBinarySensorDeviceClass.KNOCK_ON_THE_DOOR: BinarySensorEntityDescription( + key=ExtendedBinarySensorDeviceClass.KNOCK_ON_THE_DOOR, + ), + ExtendedBinarySensorDeviceClass.PRY_THE_DOOR: BinarySensorEntityDescription( + key=ExtendedBinarySensorDeviceClass.PRY_THE_DOOR, + device_class=BinarySensorDeviceClass.TAMPER, ), } @@ -92,7 +119,7 @@ async def async_setup_entry( class XiaomiBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[Optional[bool]]], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[bool | None]], BinarySensorEntity, ): """Representation of a Xiaomi binary sensor.""" diff --git a/homeassistant/components/xiaomi_ble/const.py b/homeassistant/components/xiaomi_ble/const.py index 9a38c75c05f..dda6c61d8aa 100644 --- a/homeassistant/components/xiaomi_ble/const.py +++ b/homeassistant/components/xiaomi_ble/const.py @@ -1,3 +1,21 @@ """Constants for the Xiaomi Bluetooth integration.""" +from __future__ import annotations + +from typing import Final, TypedDict DOMAIN = "xiaomi_ble" + + +CONF_EVENT_PROPERTIES: Final = "event_properties" +EVENT_PROPERTIES: Final = "event_properties" +EVENT_TYPE: Final = "event_type" +XIAOMI_BLE_EVENT: Final = "xiaomi_ble_event" + + +class XiaomiBleEvent(TypedDict): + """Xiaomi BLE event data.""" + + device_id: str + address: str + event_type: str + event_properties: dict[str, str | int | float | None] | None diff --git a/homeassistant/components/xiaomi_ble/device_trigger.py b/homeassistant/components/xiaomi_ble/device_trigger.py new file mode 100644 index 00000000000..04239cee56d --- /dev/null +++ b/homeassistant/components/xiaomi_ble/device_trigger.py @@ -0,0 +1,127 @@ +"""Provides device triggers for Xiaomi BLE.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +import voluptuous as vol + +from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA +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.trigger import TriggerActionType, TriggerInfo +from homeassistant.helpers.typing import ConfigType + +from .const import ( + CONF_EVENT_PROPERTIES, + DOMAIN, + EVENT_PROPERTIES, + EVENT_TYPE, + XIAOMI_BLE_EVENT, +) + +MOTION_DEVICE_TRIGGERS = [ + {CONF_TYPE: "motion_detected", CONF_EVENT_PROPERTIES: None}, +] + +MOTION_DEVICE_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In( + [trigger[CONF_TYPE] for trigger in MOTION_DEVICE_TRIGGERS] + ), + vol.Optional(CONF_EVENT_PROPERTIES): vol.In( + [trigger[CONF_EVENT_PROPERTIES] for trigger in MOTION_DEVICE_TRIGGERS] + ), + } +) + + +@dataclass +class TriggerModelData: + """Data class for trigger model data.""" + + triggers: list[dict[str, Any]] + schema: vol.Schema + + +MODEL_DATA = { + "MUE4094RT": TriggerModelData( + triggers=MOTION_DEVICE_TRIGGERS, schema=MOTION_DEVICE_SCHEMA + ) +} + + +async def async_validate_trigger_config( + hass: HomeAssistant, config: ConfigType +) -> ConfigType: + """Validate trigger config.""" + device_id = config[CONF_DEVICE_ID] + if model_data := _async_trigger_model_data(hass, device_id): + return model_data.schema(config) + return config + + +async def async_get_triggers( + hass: HomeAssistant, device_id: str +) -> list[dict[str, Any]]: + """List a list of triggers for Xiaomi BLE devices.""" + + # Check if device is a model supporting device triggers. + if not (model_data := _async_trigger_model_data(hass, device_id)): + return [] + return [ + { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device_id, + **trigger, + } + for trigger in model_data.triggers + ] + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: TriggerActionType, + trigger_info: TriggerInfo, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + + event_data = { + CONF_DEVICE_ID: config[CONF_DEVICE_ID], + EVENT_TYPE: config[CONF_TYPE], + EVENT_PROPERTIES: config[CONF_EVENT_PROPERTIES], + } + return await event_trigger.async_attach_trigger( + hass, + event_trigger.TRIGGER_SCHEMA( + { + event_trigger.CONF_PLATFORM: CONF_EVENT, + event_trigger.CONF_EVENT_TYPE: XIAOMI_BLE_EVENT, + event_trigger.CONF_EVENT_DATA: event_data, + } + ), + action, + trigger_info, + platform_type="device", + ) + + +def _async_trigger_model_data( + hass: HomeAssistant, device_id: str +) -> TriggerModelData | None: + """Get available triggers for a given model.""" + device_registry = dr.async_get(hass) + device = device_registry.async_get(device_id) + if device and device.model and (model_data := MODEL_DATA.get(device.model)): + return model_data + return None diff --git a/homeassistant/components/xiaomi_ble/manifest.json b/homeassistant/components/xiaomi_ble/manifest.json index 3f02c4e8767..1f36ac10d10 100644 --- a/homeassistant/components/xiaomi_ble/manifest.json +++ b/homeassistant/components/xiaomi_ble/manifest.json @@ -13,8 +13,8 @@ "service_data_uuid": "0000fe95-0000-1000-8000-00805f9b34fb" } ], - "requirements": ["xiaomi-ble==0.12.2"], - "dependencies": ["bluetooth"], + "dependencies": ["bluetooth_adapters"], + "requirements": ["xiaomi-ble==0.15.0"], "codeowners": ["@Jc2k", "@Ernst79"], "iot_class": "local_push" } diff --git a/homeassistant/components/xiaomi_ble/sensor.py b/homeassistant/components/xiaomi_ble/sensor.py index 334099cdbb6..652ae335c00 100644 --- a/homeassistant/components/xiaomi_ble/sensor.py +++ b/homeassistant/components/xiaomi_ble/sensor.py @@ -1,8 +1,6 @@ """Support for xiaomi ble sensors.""" from __future__ import annotations -from typing import Optional, Union - from xiaomi_ble import DeviceClass, SensorUpdate, Units from homeassistant import config_entries @@ -162,9 +160,7 @@ async def async_setup_entry( class XiaomiBluetoothSensorEntity( - PassiveBluetoothProcessorEntity[ - PassiveBluetoothDataProcessor[Optional[Union[float, int]]] - ], + PassiveBluetoothProcessorEntity[PassiveBluetoothDataProcessor[float | int | None]], SensorEntity, ): """Representation of a xiaomi ble sensor.""" diff --git a/homeassistant/components/xiaomi_ble/strings.json b/homeassistant/components/xiaomi_ble/strings.json index 5ecbb8e1b88..970de13bcef 100644 --- a/homeassistant/components/xiaomi_ble/strings.json +++ b/homeassistant/components/xiaomi_ble/strings.json @@ -38,5 +38,10 @@ "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "device_automation": { + "trigger_type": { + "motion_detected": "Motion detected" + } } } diff --git a/homeassistant/components/xiaomi_ble/translations/en.json b/homeassistant/components/xiaomi_ble/translations/en.json index a66ee1d2f00..445701cff10 100644 --- a/homeassistant/components/xiaomi_ble/translations/en.json +++ b/homeassistant/components/xiaomi_ble/translations/en.json @@ -38,5 +38,10 @@ "description": "Choose a device to set up" } } + }, + "device_automation": { + "trigger_type": { + "motion_detected": "Motion detected" + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/et.json b/homeassistant/components/xiaomi_ble/translations/et.json index 49c7031a3a4..eedfa73e83f 100644 --- a/homeassistant/components/xiaomi_ble/translations/et.json +++ b/homeassistant/components/xiaomi_ble/translations/et.json @@ -38,5 +38,10 @@ "description": "Vali h\u00e4\u00e4lestatav seade" } } + }, + "device_automation": { + "trigger_type": { + "motion_detected": "Tuvastati liikumine" + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/hu.json b/homeassistant/components/xiaomi_ble/translations/hu.json index 95adee995dc..324179fb2e2 100644 --- a/homeassistant/components/xiaomi_ble/translations/hu.json +++ b/homeassistant/components/xiaomi_ble/translations/hu.json @@ -7,7 +7,7 @@ "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt." }, "error": { - "decryption_failed": "A megadott kulcs nem m\u0171k\u00f6d\u00f6tt, az \u00e9rz\u00e9kel\u0151adatokat nem lehetett kiolvasni. K\u00e9rj\u00fck, ellen\u0151rizze \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", + "decryption_failed": "A megadott kulcs nem m\u0171k\u00f6d\u00f6tt, az \u00e9rz\u00e9kel\u0151adatokat nem lehetett kiolvasni. K\u00e9rem, ellen\u0151rizze \u00e9s pr\u00f3b\u00e1lja meg \u00fajra.", "expected_24_characters": "24 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g.", "expected_32_characters": "32 karakterb\u0151l \u00e1ll\u00f3 hexadecim\u00e1lis kulcsra van sz\u00fcks\u00e9g." }, diff --git a/homeassistant/components/xiaomi_ble/translations/lv.json b/homeassistant/components/xiaomi_ble/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/xiaomi_ble/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_ble/translations/nl.json b/homeassistant/components/xiaomi_ble/translations/nl.json index de54d8aff8c..d87cc206729 100644 --- a/homeassistant/components/xiaomi_ble/translations/nl.json +++ b/homeassistant/components/xiaomi_ble/translations/nl.json @@ -20,9 +20,15 @@ "description": "Er is de laatste minuut geen aankondigingsbericht van dit apparaat ontvangen, dus we weten niet zeker of dit apparaat encryptie gebruikt of niet. Dit kan zijn omdat het apparaat een trage aankodigings interval heeft. Bevestig om dit apparaat hoe dan ook toe te voegen, dan zal wanneer binnenkort een aankodiging wordt ontvangen worden gevraagd om de `bindkey` als dat nodig is." }, "get_encryption_key_4_5": { + "data": { + "bindkey": "Bindkey" + }, "description": "De sensorgegevens van de sensor zijn versleuteld. Om te ontcijferen is een hexadecimale sleutel van 32 tekens nodig." }, "get_encryption_key_legacy": { + "data": { + "bindkey": "Bindkey" + }, "description": "De sensorgegevens van de sensor zijn versleuteld. Om te ontcijferen is een hexadecimale sleutel van 24 tekens nodig." }, "user": { diff --git a/homeassistant/components/xiaomi_ble/translations/tr.json b/homeassistant/components/xiaomi_ble/translations/tr.json index cd4f6f6772d..425c60f3697 100644 --- a/homeassistant/components/xiaomi_ble/translations/tr.json +++ b/homeassistant/components/xiaomi_ble/translations/tr.json @@ -14,7 +14,7 @@ "flow_title": "{name}", "step": { "bluetooth_confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "confirm_slow": { "description": "Son dakikada bu cihazdan bir yay\u0131n olmad\u0131\u011f\u0131 i\u00e7in bu cihaz\u0131n \u015fifreleme kullan\u0131p kullanmad\u0131\u011f\u0131ndan emin de\u011filiz. Bunun nedeni, cihaz\u0131n yava\u015f bir yay\u0131n aral\u0131\u011f\u0131 kullanmas\u0131 olabilir. Yine de bu cihaz\u0131 eklemeyi onaylay\u0131n, ard\u0131ndan bir sonraki yay\u0131n al\u0131nd\u0131\u011f\u0131nda gerekirse bindkey'i girmeniz istenecektir." @@ -35,7 +35,7 @@ "data": { "address": "Cihaz" }, - "description": "Kurulum i\u00e7in bir cihaz se\u00e7in" + "description": "Kurmak i\u00e7in bir cihaz se\u00e7in" } } } diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index 6e8d0831445..fd5c8b80c26 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta import logging +from typing import Any import async_timeout from miio import ( @@ -289,7 +290,7 @@ async def async_create_miio_device_and_coordinator( device: MiioDevice | None = None migrate = False update_method = _async_update_data_default - coordinator_class: type[DataUpdateCoordinator] = DataUpdateCoordinator + coordinator_class: type[DataUpdateCoordinator[Any]] = DataUpdateCoordinator if ( model not in MODELS_HUMIDIFIER diff --git a/homeassistant/components/xiaomi_miio/translations/el.json b/homeassistant/components/xiaomi_miio/translations/el.json index da3ddb4013c..67dd8cebf12 100644 --- a/homeassistant/components/xiaomi_miio/translations/el.json +++ b/homeassistant/components/xiaomi_miio/translations/el.json @@ -3,7 +3,7 @@ "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.", + "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 \u03c0\u03b1\u03c1\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03ba\u03b5\u03bd\u03c4\u03c1\u03b9\u03ba\u03cc\u03c2 \u03c5\u03c0\u03bf\u03bb\u03bf\u03b3\u03b9\u03c3\u03c4\u03ae\u03c2 \u03ae \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc.", "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", "unknown": "\u0391\u03c0\u03c1\u03cc\u03c3\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1" @@ -13,7 +13,7 @@ "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.", - "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 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b7 \u03c1\u03bf\u03ae \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7\u03c2.", "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}", diff --git a/homeassistant/components/xiaomi_miio/translations/he.json b/homeassistant/components/xiaomi_miio/translations/he.json index 835cba4c125..6df42b877fa 100644 --- a/homeassistant/components/xiaomi_miio/translations/he.json +++ b/homeassistant/components/xiaomi_miio/translations/he.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u05ea\u05e6\u05d5\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4", "already_in_progress": "\u05d6\u05e8\u05d9\u05de\u05ea \u05d4\u05ea\u05e6\u05d5\u05e8\u05d4 \u05db\u05d1\u05e8 \u05de\u05ea\u05d1\u05e6\u05e2\u05ea", - "incomplete_info": "\u05de\u05d9\u05d3\u05e2 \u05dc\u05d0 \u05e9\u05dc\u05dd \u05dc\u05d4\u05ea\u05e7\u05e0\u05ea \u05d4\u05d4\u05ea\u05e7\u05df, \u05dc\u05d0 \u05e1\u05d5\u05e4\u05e7\u05d5 \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05d0\u05e1\u05d9\u05de\u05d5\u05df.", + "incomplete_info": "\u05de\u05d9\u05d3\u05e2 \u05dc\u05d0 \u05e9\u05dc\u05dd \u05dc\u05d4\u05d2\u05d3\u05e8\u05ea \u05d4\u05d4\u05ea\u05e7\u05df, \u05dc\u05d0 \u05e1\u05d5\u05e4\u05e7\u05d5 \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05d0\u05e1\u05d9\u05de\u05d5\u05df.", "not_xiaomi_miio": "\u05d4\u05d4\u05ea\u05e7\u05df \u05d0\u05d9\u05e0\u05d5 \u05e0\u05ea\u05de\u05da (\u05e2\u05d3\u05d9\u05d9\u05df) \u05e2\u05dc \u05d9\u05d3\u05d9 \u05e9\u05d9\u05d0\u05d5\u05de\u05d9 \u05de\u05d9\u05d5.", "reauth_successful": "\u05d4\u05d0\u05d9\u05de\u05d5\u05ea \u05de\u05d7\u05d3\u05e9 \u05d4\u05e6\u05dc\u05d9\u05d7", "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" @@ -47,7 +47,7 @@ "data": { "select_device": "\u05d4\u05ea\u05e7\u05df \u05de\u05d9\u05d5" }, - "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05de\u05d9\u05d5 \u05dc\u05d4\u05ea\u05e7\u05e0\u05d4." + "description": "\u05d1\u05d7\u05d9\u05e8\u05ea \u05d4\u05ea\u05e7\u05df \u05e9\u05d9\u05d5\u05d0\u05de\u05d9 \u05de\u05d9\u05d5 \u05dc\u05d4\u05d2\u05d3\u05e8\u05d4." } } }, diff --git a/homeassistant/components/xiaomi_miio/translations/hu.json b/homeassistant/components/xiaomi_miio/translations/hu.json index b57417dd665..3cbb516bc4f 100644 --- a/homeassistant/components/xiaomi_miio/translations/hu.json +++ b/homeassistant/components/xiaomi_miio/translations/hu.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A be\u00e1ll\u00edt\u00e1si folyamat m\u00e1r el lett kezdve", - "incomplete_info": "Az eszk\u00f6z be\u00e1ll\u00edt\u00e1s\u00e1hoz sz\u00fcks\u00e9ges inform\u00e1ci\u00f3k hi\u00e1nyosak, nincs megadva \u00e1llom\u00e1s vagy token.", + "incomplete_info": "Az eszk\u00f6z be\u00e1ll\u00edt\u00e1s\u00e1hoz sz\u00fcks\u00e9ges inform\u00e1ci\u00f3k hi\u00e1nyosak, nincs megadva c\u00edm vagy token.", "not_xiaomi_miio": "Az eszk\u00f6zt (m\u00e9g) nem t\u00e1mogatja a Xiaomi Miio integr\u00e1ci\u00f3.", "reauth_successful": "Az \u00fajrahiteles\u00edt\u00e9s sikeres volt.", "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" diff --git a/homeassistant/components/xiaomi_miio/translations/it.json b/homeassistant/components/xiaomi_miio/translations/it.json index eca534edeaf..b56c0b2e79b 100644 --- a/homeassistant/components/xiaomi_miio/translations/it.json +++ b/homeassistant/components/xiaomi_miio/translations/it.json @@ -63,7 +63,7 @@ "led_brightness": { "state": { "bright": "Luminoso", - "dim": "Scuro", + "dim": "Attenuata", "off": "Spento" } }, diff --git a/homeassistant/components/xiaomi_miio/translations/lv.json b/homeassistant/components/xiaomi_miio/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/nl.json b/homeassistant/components/xiaomi_miio/translations/nl.json index 290412f048e..4bae0af43c8 100644 --- a/homeassistant/components/xiaomi_miio/translations/nl.json +++ b/homeassistant/components/xiaomi_miio/translations/nl.json @@ -58,6 +58,20 @@ "left": "Links", "right": "Rechts" } + }, + "led_brightness": { + "state": { + "bright": "Helder", + "dim": "Dimmen", + "off": "Uit" + } + }, + "ptc_level": { + "state": { + "high": "Hoog", + "low": "Laag", + "medium": "Medium" + } } } }, diff --git a/homeassistant/components/xiaomi_miio/translations/select.it.json b/homeassistant/components/xiaomi_miio/translations/select.it.json index dd755b4cf27..3ad8df04934 100644 --- a/homeassistant/components/xiaomi_miio/translations/select.it.json +++ b/homeassistant/components/xiaomi_miio/translations/select.it.json @@ -7,7 +7,7 @@ }, "xiaomi_miio__led_brightness": { "bright": "Brillante", - "dim": "Fioca", + "dim": "Attenuata", "off": "Spento" }, "xiaomi_miio__ptc_level": { diff --git a/homeassistant/components/xiaomi_miio/translations/tr.json b/homeassistant/components/xiaomi_miio/translations/tr.json index ebd9ce377c8..17a0e52ec8b 100644 --- a/homeassistant/components/xiaomi_miio/translations/tr.json +++ b/homeassistant/components/xiaomi_miio/translations/tr.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", - "incomplete_info": "Kurulum cihaz\u0131 i\u00e7in eksik bilgi, ana bilgisayar veya anahtar sa\u011flanmad\u0131.", + "incomplete_info": "Cihaz\u0131 kurmak i\u00e7in eksik bilgi, sa\u011flanan ana bilgisayar veya belirte\u00e7 yok.", "not_xiaomi_miio": "Cihaz (hen\u00fcz) Xiaomi Miio taraf\u0131ndan desteklenmiyor.", "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", "unknown": "Beklenmeyen hata" @@ -13,7 +13,7 @@ "cloud_credentials_incomplete": "Bulut kimlik bilgileri eksik, l\u00fctfen kullan\u0131c\u0131 ad\u0131n\u0131, \u015fifreyi ve \u00fclkeyi girin", "cloud_login_error": "Xiaomi Miio Cloud'da oturum a\u00e7\u0131lamad\u0131, kimlik bilgilerini kontrol edin.", "cloud_no_devices": "Bu Xiaomi Miio bulut hesab\u0131nda cihaz bulunamad\u0131.", - "unknown_device": "Cihaz modeli bilinmiyor, cihaz yap\u0131land\u0131rma ak\u0131\u015f\u0131n\u0131 kullanarak kurulam\u0131yor.", + "unknown_device": "Cihaz modeli bilinmiyor, konfig\u00fcrasyon ak\u0131\u015f\u0131 kullan\u0131larak cihaz kurulam\u0131yor.", "wrong_token": "Sa\u011flama toplam\u0131 hatas\u0131, yanl\u0131\u015f anahtar" }, "flow_title": "{name}", @@ -47,7 +47,32 @@ "data": { "select_device": "Miio cihaz\u0131" }, - "description": "Kurulumu i\u00e7in Xiaomi Miio cihaz\u0131n\u0131 se\u00e7in." + "description": "Kurulum i\u00e7in Xiaomi Miio cihaz\u0131n\u0131 se\u00e7in." + } + } + }, + "entity": { + "select": { + "display_orientation": { + "state": { + "forward": "\u0130leri", + "left": "Sol", + "right": "Sa\u011f" + } + }, + "led_brightness": { + "state": { + "bright": "Ayd\u0131nl\u0131k", + "dim": "Dim", + "off": "Kapal\u0131" + } + }, + "ptc_level": { + "state": { + "high": "Y\u00fcksek", + "low": "D\u00fc\u015f\u00fck", + "medium": "Orta" + } } } }, diff --git a/homeassistant/components/xiaomi_miio/translations/uk.json b/homeassistant/components/xiaomi_miio/translations/uk.json index bf1b8126e38..dab8ce8d6ba 100644 --- a/homeassistant/components/xiaomi_miio/translations/uk.json +++ b/homeassistant/components/xiaomi_miio/translations/uk.json @@ -7,6 +7,13 @@ "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" }, - "flow_title": "Xiaomi Miio: {name}" + "flow_title": "Xiaomi Miio: {name}", + "step": { + "connect": { + "data": { + "model": "\u041c\u043e\u0434\u0435\u043b\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json index b92b25aa002..a2e3b12a199 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json @@ -23,7 +23,7 @@ "cloud_country": "\u96f2\u7aef\u670d\u52d9\u4f3a\u670d\u5668\u570b\u5bb6", "cloud_password": "\u96f2\u7aef\u670d\u52d9\u5bc6\u78bc", "cloud_username": "\u96f2\u7aef\u670d\u52d9\u4f7f\u7528\u8005\u540d\u7a31", - "manual": "\u624b\u52d5\u8a2d\u5b9a (\u4e0d\u5efa\u8b70)" + "manual": "\u624b\u52d5\u8a2d\u5b9a\uff08\u4e0d\u63a8\u85a6\uff09" }, "description": "\u767b\u5165\u81f3\u5c0f\u7c73 Miio \u96f2\u670d\u52d9\uff0c\u8acb\u53c3\u95b1 https://www.openhab.org/addons/bindings/miio/#country-servers \u4ee5\u4e86\u89e3\u9078\u64c7\u54ea\u4e00\u7d44\u96f2\u7aef\u4f3a\u670d\u5668\u3002" }, diff --git a/homeassistant/components/yale_smart_alarm/translations/lt.json b/homeassistant/components/yale_smart_alarm/translations/lt.json new file mode 100644 index 00000000000..e25b890db12 --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/lt.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Paskyra jau sukonfig\u016bruota" + }, + "step": { + "user": { + "data": { + "password": "Slapta\u017eodis" + } + } + } + }, + "options": { + "error": { + "code_format_mismatch": "Kodas neatitinka reikalaujamo skaitmen\u0173 skai\u010diaus" + }, + "step": { + "init": { + "data": { + "code": "Numatytasis u\u017erakt\u0173 kodas, naudojamas, jei nenurodytas joks kodas", + "lock_code_digits": "U\u017erakt\u0173 PIN kodo skaitmen\u0173 skai\u010dius" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yale_smart_alarm/translations/uk.json b/homeassistant/components/yale_smart_alarm/translations/uk.json new file mode 100644 index 00000000000..389cb28ca0c --- /dev/null +++ b/homeassistant/components/yale_smart_alarm/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/manifest.json b/homeassistant/components/yalexs_ble/manifest.json index 82d8f0eb228..866f5eca6bc 100644 --- a/homeassistant/components/yalexs_ble/manifest.json +++ b/homeassistant/components/yalexs_ble/manifest.json @@ -3,8 +3,8 @@ "name": "Yale Access Bluetooth", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yalexs_ble", - "requirements": ["yalexs-ble==1.12.5"], - "dependencies": ["bluetooth"], + "requirements": ["yalexs-ble==1.12.8"], + "dependencies": ["bluetooth_adapters"], "codeowners": ["@bdraco"], "bluetooth": [ { diff --git a/homeassistant/components/yalexs_ble/translations/el.json b/homeassistant/components/yalexs_ble/translations/el.json index 3f6ae763e5e..d0e6cb74bf7 100644 --- a/homeassistant/components/yalexs_ble/translations/el.json +++ b/homeassistant/components/yalexs_ble/translations/el.json @@ -24,7 +24,7 @@ "key": "\u039a\u03bb\u03b5\u03b9\u03b4\u03af \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 (\u03b4\u03b5\u03ba\u03b1\u03b5\u03be\u03b1\u03b4\u03b9\u03ba\u03ae \u03c3\u03c5\u03bc\u03b2\u03bf\u03bb\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac 32 byte)", "slot": "\u03a5\u03c0\u03bf\u03b4\u03bf\u03c7\u03ae \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 (\u0391\u03ba\u03ad\u03c1\u03b1\u03b9\u03bf\u03c2 \u03b1\u03c1\u03b9\u03b8\u03bc\u03cc\u03c2 \u03bc\u03b5\u03c4\u03b1\u03be\u03cd 0 \u03ba\u03b1\u03b9 255)" }, - "description": "\u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \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 {docs_url} \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2." + "description": "\u0395\u03bb\u03ad\u03b3\u03be\u03c4\u03b5 \u03c4\u03b7\u03bd \u03c4\u03b5\u03ba\u03bc\u03b7\u03c1\u03af\u03c9\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03c4\u03c1\u03cc\u03c0\u03bf \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03bf\u03cd \u03b5\u03ba\u03c4\u03cc\u03c2 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2." } } } diff --git a/homeassistant/components/yalexs_ble/translations/hu.json b/homeassistant/components/yalexs_ble/translations/hu.json index b0ca6ccdac4..c15b18d8880 100644 --- a/homeassistant/components/yalexs_ble/translations/hu.json +++ b/homeassistant/components/yalexs_ble/translations/hu.json @@ -16,7 +16,7 @@ "flow_title": "{name}", "step": { "integration_discovery_confirm": { - "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {nane}, Bluetooth-on kereszt\u00fcl, {address} c\u00edmmel?" + "description": "Szeretn\u00e9 be\u00e1ll\u00edtani: {name}, Bluetooth-on kereszt\u00fcl, {address} c\u00edmmel?" }, "user": { "data": { diff --git a/homeassistant/components/yalexs_ble/translations/lv.json b/homeassistant/components/yalexs_ble/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/yalexs_ble/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yalexs_ble/translations/tr.json b/homeassistant/components/yalexs_ble/translations/tr.json index 15711050225..f785861bb89 100644 --- a/homeassistant/components/yalexs_ble/translations/tr.json +++ b/homeassistant/components/yalexs_ble/translations/tr.json @@ -16,7 +16,7 @@ "flow_title": "{name}", "step": { "integration_discovery_confirm": { - "description": "{address} adresiyle Bluetooth \u00fczerinden {name} kurmak istiyor musunuz?" + "description": "{name} adresini Bluetooth \u00fczerinden {address} adresiyle kurmak istiyor musunuz?" }, "user": { "data": { diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 86bb0b11ec0..aeb38c0faac 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -205,6 +205,11 @@ class YamahaDevice(MediaPlayerEntity): self._play_status = None self._name = name self._zone = receiver.zone + if self.receiver.serial_number is not None: + # Since not all receivers will have a serial number and set a unique id + # the default name of the integration may not be changed + # to avoid a breaking change. + self._attr_unique_id = f"{self.receiver.serial_number}_{self._zone}" def update(self) -> None: """Get the latest details from the device.""" @@ -212,8 +217,10 @@ class YamahaDevice(MediaPlayerEntity): self._play_status = self.receiver.play_status() except requests.exceptions.ConnectionError: _LOGGER.info("Receiver is offline: %s", self._name) + self._attr_available = False return + self._attr_available = True if self.receiver.on: if self._play_status is None: self._attr_state = MediaPlayerState.ON diff --git a/homeassistant/components/yamaha_musiccast/manifest.json b/homeassistant/components/yamaha_musiccast/manifest.json index 8c0b55def69..afcc64985dc 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -3,7 +3,7 @@ "name": "MusicCast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast", - "requirements": ["aiomusiccast==0.14.4"], + "requirements": ["aiomusiccast==0.14.7"], "ssdp": [ { "manufacturer": "Yamaha Corporation" diff --git a/homeassistant/components/yamaha_musiccast/strings.json b/homeassistant/components/yamaha_musiccast/strings.json index 0b3a51989b8..9905a8af74b 100644 --- a/homeassistant/components/yamaha_musiccast/strings.json +++ b/homeassistant/components/yamaha_musiccast/strings.json @@ -30,10 +30,10 @@ "zone_sleep": { "state": { "off": "Off", - "30 min": "30 Minutes", - "60 min": "60 Minutes", - "90 min": "90 Minutes", - "120 min": "120 Minutes" + "30_min": "30 Minutes", + "60_min": "60 Minutes", + "90_min": "90 Minutes", + "120_min": "120 Minutes" } }, "zone_tone_control_mode": { diff --git a/homeassistant/components/yamaha_musiccast/translations/bg.json b/homeassistant/components/yamaha_musiccast/translations/bg.json index ecc1ff25eae..08957424b89 100644 --- a/homeassistant/components/yamaha_musiccast/translations/bg.json +++ b/homeassistant/components/yamaha_musiccast/translations/bg.json @@ -23,10 +23,10 @@ }, "zone_sleep": { "state": { - "120 min": "120 \u043c\u0438\u043d\u0443\u0442\u0438", - "30 min": "30 \u043c\u0438\u043d\u0443\u0442\u0438", - "60 min": "60 \u043c\u0438\u043d\u0443\u0442\u0438", - "90 min": "90 \u043c\u0438\u043d\u0443\u0442\u0438", + "120_min": "120 \u043c\u0438\u043d\u0443\u0442\u0438", + "30_min": "30 \u043c\u0438\u043d\u0443\u0442\u0438", + "60_min": "60 \u043c\u0438\u043d\u0443\u0442\u0438", + "90_min": "90 \u043c\u0438\u043d\u0443\u0442\u0438", "off": "\u0418\u0437\u043a\u043b." } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/ca.json b/homeassistant/components/yamaha_musiccast/translations/ca.json index 977ff6f1759..8af45406500 100644 --- a/homeassistant/components/yamaha_musiccast/translations/ca.json +++ b/homeassistant/components/yamaha_musiccast/translations/ca.json @@ -37,10 +37,10 @@ "zone_link_audio_delay": { "state": { "audio_sync": "Sincronitzaci\u00f3 d'\u00e0udio", - "audio_sync_off": "Sincronitzaci\u00f3 d'\u00e0udio OFF", - "audio_sync_on": "Sincronitzaci\u00f3 d'\u00e0udio ON", + "audio_sync_off": "Sincronitzaci\u00f3 d'\u00e0udio Desactivada", + "audio_sync_on": "Sincronitzaci\u00f3 d'\u00e0udio Activada", "balanced": "Equilibrat", - "lip_sync": "Sincronitzaci\u00f3 Lip" + "lip_sync": "Sincronitzaci\u00f3 dels llavis" } }, "zone_link_audio_quality": { @@ -58,11 +58,11 @@ }, "zone_sleep": { "state": { - "120 min": "120 minuts", - "30 min": "30 minuts", - "60 min": "60 minuts", - "90 min": "90 minuts", - "off": "OFF" + "120_min": "120 minuts", + "30_min": "30 minuts", + "60_min": "60 minuts", + "90_min": "90 minuts", + "off": "Desactivat" } }, "zone_surr_decoder_type": { diff --git a/homeassistant/components/yamaha_musiccast/translations/de.json b/homeassistant/components/yamaha_musiccast/translations/de.json index 9c68fc00e1f..7cf900e09ae 100644 --- a/homeassistant/components/yamaha_musiccast/translations/de.json +++ b/homeassistant/components/yamaha_musiccast/translations/de.json @@ -58,10 +58,10 @@ }, "zone_sleep": { "state": { - "120 min": "120 Minuten", - "30 min": "30 Minuten", - "60 min": "60 Minuten", - "90 min": "90 Minuten", + "120_min": "120 Minuten", + "30_min": "30 Minuten", + "60_min": "60 Minuten", + "90_min": "90 Minuten", "off": "Aus" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/el.json b/homeassistant/components/yamaha_musiccast/translations/el.json index 21a602c2505..6d8c18e4dfe 100644 --- a/homeassistant/components/yamaha_musiccast/translations/el.json +++ b/homeassistant/components/yamaha_musiccast/translations/el.json @@ -58,10 +58,10 @@ }, "zone_sleep": { "state": { - "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", + "120_min": "120 \u039b\u03b5\u03c0\u03c4\u03ac", + "30_min": "30 \u03bb\u03b5\u03c0\u03c4\u03ac", + "60_min": "60 \u039b\u03b5\u03c0\u03c4\u03ac", + "90_min": "90 \u039b\u03b5\u03c0\u03c4\u03ac", "off": "\u0391\u03bd\u03b5\u03bd\u03b5\u03c1\u03b3\u03cc" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/en.json b/homeassistant/components/yamaha_musiccast/translations/en.json index f15b986ff8d..5b41f24a24e 100644 --- a/homeassistant/components/yamaha_musiccast/translations/en.json +++ b/homeassistant/components/yamaha_musiccast/translations/en.json @@ -58,10 +58,10 @@ }, "zone_sleep": { "state": { - "120 min": "120 Minutes", - "30 min": "30 Minutes", - "60 min": "60 Minutes", - "90 min": "90 Minutes", + "120_min": "120 Minutes", + "30_min": "30 Minutes", + "60_min": "60 Minutes", + "90_min": "90 Minutes", "off": "Off" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/es.json b/homeassistant/components/yamaha_musiccast/translations/es.json index 2c9649e8a2e..a5cdd9011e2 100644 --- a/homeassistant/components/yamaha_musiccast/translations/es.json +++ b/homeassistant/components/yamaha_musiccast/translations/es.json @@ -58,10 +58,10 @@ }, "zone_sleep": { "state": { - "120 min": "120 minutos", - "30 min": "30 minutos", - "60 min": "60 minutos", - "90 min": "90 minutos", + "120_min": "120 minutos", + "30_min": "30 minutos", + "60_min": "60 minutos", + "90_min": "90 minutos", "off": "Apagado" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/et.json b/homeassistant/components/yamaha_musiccast/translations/et.json index 561cf8acc64..8dcc5a2829d 100644 --- a/homeassistant/components/yamaha_musiccast/translations/et.json +++ b/homeassistant/components/yamaha_musiccast/translations/et.json @@ -58,10 +58,10 @@ }, "zone_sleep": { "state": { - "120 min": "120 minutit", - "30 min": "30 minutit", - "60 min": "60 minutit", - "90 min": "90 minutit", + "120_min": "120 minutit", + "30_min": "30 minutit", + "60_min": "60 minutit", + "90_min": "90 minutit", "off": "V\u00e4ljas" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/hu.json b/homeassistant/components/yamaha_musiccast/translations/hu.json index ad078116f84..8470e26ebdc 100644 --- a/homeassistant/components/yamaha_musiccast/translations/hu.json +++ b/homeassistant/components/yamaha_musiccast/translations/hu.json @@ -58,10 +58,10 @@ }, "zone_sleep": { "state": { - "120 min": "120 perc", - "30 min": "30 perc", - "60 min": "60 perc", - "90 min": "90 perc", + "120_min": "120 perc", + "30_min": "30 perc", + "60_min": "60 perc", + "90_min": "90 perc", "off": "Ki" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/id.json b/homeassistant/components/yamaha_musiccast/translations/id.json index 1fb8c36d91e..d2a80ed3938 100644 --- a/homeassistant/components/yamaha_musiccast/translations/id.json +++ b/homeassistant/components/yamaha_musiccast/translations/id.json @@ -58,10 +58,10 @@ }, "zone_sleep": { "state": { - "120 min": "120 Menit", - "30 min": "30 Menit", - "60 min": "60 Menit", - "90 min": "90 Menit", + "120_min": "120 Menit", + "30_min": "30 Menit", + "60_min": "60 Menit", + "90_min": "90 Menit", "off": "Mati" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/it.json b/homeassistant/components/yamaha_musiccast/translations/it.json index 095a98379f0..c143a984b73 100644 --- a/homeassistant/components/yamaha_musiccast/translations/it.json +++ b/homeassistant/components/yamaha_musiccast/translations/it.json @@ -58,10 +58,10 @@ }, "zone_sleep": { "state": { - "120 min": "120 minuti", - "30 min": "30 minuti", - "60 min": "60 minuti", - "90 min": "90 minuti", + "120_min": "120 minuti", + "30_min": "30 minuti", + "60_min": "60 minuti", + "90_min": "90 minuti", "off": "Spento" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/lv.json b/homeassistant/components/yamaha_musiccast/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/nl.json b/homeassistant/components/yamaha_musiccast/translations/nl.json index 0cafd20a62e..1862ba1dce8 100644 --- a/homeassistant/components/yamaha_musiccast/translations/nl.json +++ b/homeassistant/components/yamaha_musiccast/translations/nl.json @@ -27,24 +27,34 @@ "manual": "Handmatig" } }, + "zone_link_audio_delay": { + "state": { + "audio_sync": "Audiosynchronisatie", + "audio_sync_off": "Audiosynchronisatie uit", + "audio_sync_on": "Audiosynchronisatie aan", + "balanced": "Gebalanceerd" + } + }, "zone_link_control": { "state": { - "speed": "Snelheid" + "speed": "Snelheid", + "standard": "Standaard" } }, "zone_sleep": { "state": { - "120 min": "120 minuten", - "30 min": "30 minuten", - "60 min": "60 minuten", - "90 min": "90 minuten", + "120_min": "120 minuten", + "30_min": "30 minuten", + "60_min": "60 minuten", + "90_min": "90 minuten", "off": "Uit" } }, "zone_surr_decoder_type": { "state": { "dolby_pl": "Dolby ProLogic", - "dolby_surround": "Dolby Surround" + "dolby_surround": "Dolby Surround", + "toggle": "Omschakelen" } }, "zone_tone_control_mode": { diff --git a/homeassistant/components/yamaha_musiccast/translations/no.json b/homeassistant/components/yamaha_musiccast/translations/no.json index b7ff15aa85f..3da2c4b33e3 100644 --- a/homeassistant/components/yamaha_musiccast/translations/no.json +++ b/homeassistant/components/yamaha_musiccast/translations/no.json @@ -58,10 +58,10 @@ }, "zone_sleep": { "state": { - "120 min": "120 minutter", - "30 min": "30 minutter", - "60 min": "60 minutter", - "90 min": "90 minutter", + "120_min": "120 minutter", + "30_min": "30 minutter", + "60_min": "60 minutter", + "90_min": "90 minutter", "off": "Av" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/pl.json b/homeassistant/components/yamaha_musiccast/translations/pl.json index 07c6f67f91c..9d0db0226fd 100644 --- a/homeassistant/components/yamaha_musiccast/translations/pl.json +++ b/homeassistant/components/yamaha_musiccast/translations/pl.json @@ -58,10 +58,10 @@ }, "zone_sleep": { "state": { - "120 min": "120 minut", - "30 min": "30 minut", - "60 min": "60 minut", - "90 min": "90 minut", + "120_min": "120 minut", + "30_min": "30 minut", + "60_min": "60 minut", + "90_min": "90 minut", "off": "wy\u0142\u0105czone" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/pt-BR.json b/homeassistant/components/yamaha_musiccast/translations/pt-BR.json index 615fc15c426..5c7329b3013 100644 --- a/homeassistant/components/yamaha_musiccast/translations/pt-BR.json +++ b/homeassistant/components/yamaha_musiccast/translations/pt-BR.json @@ -58,10 +58,10 @@ }, "zone_sleep": { "state": { - "120 min": "120 minutos", - "30 min": "30 minutos", - "60 min": "60 minutos", - "90 min": "90 minutos", + "120_min": "120 minutos", + "30_min": "30 minutos", + "60_min": "60 minutos", + "90_min": "90 minutos", "off": "Desligado" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/ru.json b/homeassistant/components/yamaha_musiccast/translations/ru.json index d55bb5e6a96..1da11df4d16 100644 --- a/homeassistant/components/yamaha_musiccast/translations/ru.json +++ b/homeassistant/components/yamaha_musiccast/translations/ru.json @@ -58,10 +58,10 @@ }, "zone_sleep": { "state": { - "120 min": "120 \u043c\u0438\u043d\u0443\u0442", - "30 min": "30 \u043c\u0438\u043d\u0443\u0442", - "60 min": "60 \u043c\u0438\u043d\u0443\u0442", - "90 min": "90 \u043c\u0438\u043d\u0443\u0442", + "120_min": "120 \u043c\u0438\u043d\u0443\u0442", + "30_min": "30 \u043c\u0438\u043d\u0443\u0442", + "60_min": "60 \u043c\u0438\u043d\u0443\u0442", + "90_min": "90 \u043c\u0438\u043d\u0443\u0442", "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/select.uk.json b/homeassistant/components/yamaha_musiccast/translations/select.uk.json new file mode 100644 index 00000000000..bba8f5e2a2b --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/select.uk.json @@ -0,0 +1,24 @@ +{ + "state": { + "yamaha_musiccast__zone_link_audio_quality": { + "compressed": "\u0421\u0442\u0438\u0441\u043d\u0435\u043d\u0438\u0439", + "uncompressed": "\u0411\u0435\u0437 \u0441\u0442\u0438\u0441\u043d\u0435\u043d\u043d\u044f" + }, + "yamaha_musiccast__zone_link_control": { + "speed": "\u0428\u0432\u0438\u0434\u043a\u0456\u0441\u0442\u044c" + }, + "yamaha_musiccast__zone_sleep": { + "120 min": "120 \u0425\u0432\u0438\u043b\u0438\u043d", + "30 min": "30 \u0425\u0432\u0438\u043b\u0438\u043d", + "60 min": "60 \u0425\u0432\u0438\u043b\u0438\u043d", + "90 min": "90 \u0425\u0432\u0438\u043b\u0438\u043d", + "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e" + }, + "yamaha_musiccast__zone_surr_decoder_type": { + "auto": "\u0410\u0432\u0442\u043e" + }, + "yamaha_musiccast__zone_tone_control_mode": { + "auto": "\u0410\u0432\u0442\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/sk.json b/homeassistant/components/yamaha_musiccast/translations/sk.json index 96f9dd46b27..117b10bbe8f 100644 --- a/homeassistant/components/yamaha_musiccast/translations/sk.json +++ b/homeassistant/components/yamaha_musiccast/translations/sk.json @@ -58,10 +58,10 @@ }, "zone_sleep": { "state": { - "120 min": "120 min\u00fat", - "30 min": "30 min\u00fat", - "60 min": "60 min\u00fat", - "90 min": "90 min\u00fat", + "120_min": "120 min\u00fat", + "30_min": "30 min\u00fat", + "60_min": "60 min\u00fat", + "90_min": "90 min\u00fat", "off": "Vypnut\u00e9" } }, diff --git a/homeassistant/components/yamaha_musiccast/translations/sv.json b/homeassistant/components/yamaha_musiccast/translations/sv.json index 7326abb364a..36c7d0e0ca8 100644 --- a/homeassistant/components/yamaha_musiccast/translations/sv.json +++ b/homeassistant/components/yamaha_musiccast/translations/sv.json @@ -19,5 +19,14 @@ "description": "Konfigurera MusicCast f\u00f6r att integrera med Home Assistant." } } + }, + "entity": { + "select": { + "zone_sleep": { + "state": { + "60_min": "60 minuter" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/tr.json b/homeassistant/components/yamaha_musiccast/translations/tr.json index 955ef6646c4..1778ba1a6f9 100644 --- a/homeassistant/components/yamaha_musiccast/translations/tr.json +++ b/homeassistant/components/yamaha_musiccast/translations/tr.json @@ -10,7 +10,7 @@ "flow_title": "MusicCast: {name}", "step": { "confirm": { - "description": "Kuruluma ba\u015flamak ister misiniz?" + "description": "Kurulumu ba\u015flatmak istiyor musunuz?" }, "user": { "data": { @@ -19,5 +19,73 @@ "description": "Home Assistant ile entegre etmek i\u00e7in MusicCast'i kurun." } } + }, + "entity": { + "select": { + "dimmer": { + "state": { + "auto": "Otomatik" + } + }, + "zone_equalizer_mode": { + "state": { + "auto": "Otomatik", + "bypass": "Atlatma", + "manual": "Manuel" + } + }, + "zone_link_audio_delay": { + "state": { + "audio_sync": "Ses Senkronizasyonu", + "audio_sync_off": "Ses Senkronizasyonu Kapal\u0131", + "audio_sync_on": "Ses Senkronizasyonu A\u00e7\u0131k", + "balanced": "Dengeli", + "lip_sync": "Dudak Senkronizasyonu" + } + }, + "zone_link_audio_quality": { + "state": { + "compressed": "S\u0131k\u0131\u015ft\u0131r\u0131lm\u0131\u015f", + "uncompressed": "S\u0131k\u0131\u015ft\u0131r\u0131lmam\u0131\u015f" + } + }, + "zone_link_control": { + "state": { + "speed": "H\u0131z", + "stability": "Stabilite", + "standard": "Standart" + } + }, + "zone_sleep": { + "state": { + "120_min": "120 Dakika", + "30_min": "30 Dakika", + "60_min": "60 Dakika", + "90_min": "90 Dakika", + "off": "Kapal\u0131" + } + }, + "zone_surr_decoder_type": { + "state": { + "auto": "Otomatik", + "dolby_pl": "Dolby ProLogic", + "dolby_pl2x_game": "Dolby ProLogic 2x Oyun", + "dolby_pl2x_movie": "Dolby ProLogic 2x Film", + "dolby_pl2x_music": "Dolby ProLogic 2x M\u00fczik", + "dolby_surround": "Dolby Surround", + "dts_neo6_cinema": "DTS Neo:6 Sinema", + "dts_neo6_music": "DTS Neo:6 M\u00fczik", + "dts_neural_x": "DTS Neural:X", + "toggle": "De\u011fi\u015ftir" + } + }, + "zone_tone_control_mode": { + "state": { + "auto": "Otomatik", + "bypass": "Atlatma", + "manual": "Manuel" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/uk.json b/homeassistant/components/yamaha_musiccast/translations/uk.json new file mode 100644 index 00000000000..d127a2360ff --- /dev/null +++ b/homeassistant/components/yamaha_musiccast/translations/uk.json @@ -0,0 +1,14 @@ +{ + "entity": { + "select": { + "zone_sleep": { + "state": { + "120_min": "120 \u0425\u0432\u0438\u043b\u0438\u043d", + "30_min": "30 \u0425\u0432\u0438\u043b\u0438\u043d", + "60_min": "60 \u0425\u0432\u0438\u043b\u0438\u043d", + "90_min": "90 \u0425\u0432\u0438\u043b\u0438\u043d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yamaha_musiccast/translations/zh-Hant.json b/homeassistant/components/yamaha_musiccast/translations/zh-Hant.json index 4931da48893..e8467548241 100644 --- a/homeassistant/components/yamaha_musiccast/translations/zh-Hant.json +++ b/homeassistant/components/yamaha_musiccast/translations/zh-Hant.json @@ -58,10 +58,10 @@ }, "zone_sleep": { "state": { - "120 min": "120 \u5206\u9418", - "30 min": "30 \u5206\u9418", - "60 min": "60 \u5206\u9418", - "90 min": "90 \u5206\u9418", + "120_min": "120 \u5206\u9418", + "30_min": "30 \u5206\u9418", + "60_min": "60 \u5206\u9418", + "90_min": "90 \u5206\u9418", "off": "\u95dc\u9589" } }, diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index c72a9401c52..62106f99d0d 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,7 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.7.10", "async-upnp-client==0.33.0"], + "requirements": ["yeelight==0.7.10", "async-upnp-client==0.33.1"], "codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"], "config_flow": true, "dependencies": ["network"], diff --git a/homeassistant/components/yeelight/scanner.py b/homeassistant/components/yeelight/scanner.py index 612edc29791..d988dfbcc41 100644 --- a/homeassistant/components/yeelight/scanner.py +++ b/homeassistant/components/yeelight/scanner.py @@ -35,7 +35,7 @@ _LOGGER = logging.getLogger(__name__) class YeelightScanner: """Scan for Yeelight devices.""" - _scanner = None + _scanner: YeelightScanner | None = None @classmethod @callback diff --git a/homeassistant/components/yeelight/translations/tr.json b/homeassistant/components/yeelight/translations/tr.json index 21ec60cde68..c631054e91c 100644 --- a/homeassistant/components/yeelight/translations/tr.json +++ b/homeassistant/components/yeelight/translations/tr.json @@ -10,7 +10,7 @@ "flow_title": "{model} {id} ({host})", "step": { "discovery_confirm": { - "description": "{model} ( {host} ) kurulumu yapmak istiyor musunuz?" + "description": "{model} ( {host} ) kurmak istiyor musunuz?" }, "pick_device": { "data": { diff --git a/homeassistant/components/youless/__init__.py b/homeassistant/components/youless/__init__.py index 0026d2ec484..5724f417a7f 100644 --- a/homeassistant/components/youless/__init__.py +++ b/homeassistant/components/youless/__init__.py @@ -27,7 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except URLError as exception: raise ConfigEntryNotReady from exception - async def async_update_data(): + async def async_update_data() -> YoulessAPI: """Fetch data from the API.""" await hass.async_add_executor_job(api.update) return api diff --git a/homeassistant/components/youless/sensor.py b/homeassistant/components/youless/sensor.py index ae8b0e1691b..b9120f433de 100644 --- a/homeassistant/components/youless/sensor.py +++ b/homeassistant/components/youless/sensor.py @@ -1,6 +1,7 @@ """The sensor entity for the Youless integration.""" from __future__ import annotations +from youless_api import YoulessAPI from youless_api.youless_sensor import YoulessSensor from homeassistant.components.sensor import ( @@ -26,7 +27,7 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Initialize the integration.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: DataUpdateCoordinator[YoulessAPI] = hass.data[DOMAIN][entry.entry_id] device = entry.data[CONF_DEVICE] if (device := entry.data[CONF_DEVICE]) is None: device = entry.entry_id @@ -50,12 +51,14 @@ async def async_setup_entry( ) -class YoulessBaseSensor(CoordinatorEntity, SensorEntity): +class YoulessBaseSensor( + CoordinatorEntity[DataUpdateCoordinator[YoulessAPI]], SensorEntity +): """The base sensor for Youless.""" def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[YoulessAPI], device: str, device_group: str, friendly_name: str, @@ -97,7 +100,9 @@ class GasSensor(YoulessBaseSensor): _attr_device_class = SensorDeviceClass.GAS _attr_state_class = SensorStateClass.TOTAL_INCREASING - def __init__(self, coordinator: DataUpdateCoordinator, device: str) -> None: + def __init__( + self, coordinator: DataUpdateCoordinator[YoulessAPI], device: str + ) -> None: """Instantiate a gas sensor.""" super().__init__(coordinator, device, "gas", "Gas meter", "gas") self._attr_name = "Gas usage" @@ -116,7 +121,9 @@ class CurrentPowerSensor(YoulessBaseSensor): _attr_device_class = SensorDeviceClass.POWER _attr_state_class = SensorStateClass.MEASUREMENT - def __init__(self, coordinator: DataUpdateCoordinator, device: str) -> None: + def __init__( + self, coordinator: DataUpdateCoordinator[YoulessAPI], device: str + ) -> None: """Instantiate the usage meter.""" super().__init__(coordinator, device, "power", "Power usage", "usage") self._device = device @@ -136,7 +143,7 @@ class DeliveryMeterSensor(YoulessBaseSensor): _attr_state_class = SensorStateClass.TOTAL_INCREASING def __init__( - self, coordinator: DataUpdateCoordinator, device: str, dev_type: str + self, coordinator: DataUpdateCoordinator[YoulessAPI], device: str, dev_type: str ) -> None: """Instantiate a delivery meter sensor.""" super().__init__( @@ -163,7 +170,7 @@ class EnergyMeterSensor(YoulessBaseSensor): def __init__( self, - coordinator: DataUpdateCoordinator, + coordinator: DataUpdateCoordinator[YoulessAPI], device: str, dev_type: str, state_class: SensorStateClass, @@ -194,7 +201,7 @@ class ExtraMeterSensor(YoulessBaseSensor): _attr_state_class = SensorStateClass.TOTAL_INCREASING def __init__( - self, coordinator: DataUpdateCoordinator, device: str, dev_type: str + self, coordinator: DataUpdateCoordinator[YoulessAPI], device: str, dev_type: str ) -> None: """Instantiate an extra meter sensor.""" super().__init__( @@ -220,7 +227,7 @@ class ExtraMeterPowerSensor(YoulessBaseSensor): _attr_state_class = SensorStateClass.MEASUREMENT def __init__( - self, coordinator: DataUpdateCoordinator, device: str, dev_type: str + self, coordinator: DataUpdateCoordinator[YoulessAPI], device: str, dev_type: str ) -> None: """Instantiate an extra meter power sensor.""" super().__init__( diff --git a/homeassistant/components/zamg/config_flow.py b/homeassistant/components/zamg/config_flow.py index 86584fd7f1c..badb59f9b8f 100644 --- a/homeassistant/components/zamg/config_flow.py +++ b/homeassistant/components/zamg/config_flow.py @@ -5,13 +5,11 @@ from typing import Any import voluptuous as vol from zamg import ZamgData -from zamg.exceptions import ZamgApiError, ZamgNoDataError, ZamgStationNotFoundError +from zamg.exceptions import ZamgApiError, ZamgNoDataError from homeassistant import config_entries -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import CONF_STATION_ID, DOMAIN, LOGGER @@ -73,56 +71,3 @@ class ZamgConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): title=self._client.get_station_name, data={CONF_STATION_ID: station_id}, ) - - async def async_step_import(self, config: dict[str, Any]) -> FlowResult: - """Handle ZAMG configuration import.""" - station_id = config.get(CONF_STATION_ID) - # create issue every time after restart - # parameter is_persistent seems not working - async_create_issue( - self.hass, - DOMAIN, - "deprecated_yaml", - breaks_in_ha_version="2023.1.0", - is_fixable=False, - severity=IssueSeverity.WARNING, - translation_key="deprecated_yaml", - ) - - if self._client is None: - self._client = ZamgData() - self._client.session = async_get_clientsession(self.hass) - - try: - if station_id not in await self._client.zamg_stations(): - LOGGER.warning( - ( - "Configured station_id %s could not be found at zamg, trying to" - " add nearest weather station instead" - ), - station_id, - ) - latitude = config.get(CONF_LATITUDE) or self.hass.config.latitude - longitude = config.get(CONF_LONGITUDE) or self.hass.config.longitude - station_id = await self._client.closest_station(latitude, longitude) - - # Check if already configured - await self.async_set_unique_id(station_id) - self._abort_if_unique_id_configured() - - LOGGER.debug( - "importing zamg station from configuration.yaml: station_id = %s", - station_id, - ) - except (ZamgApiError) as err: - LOGGER.error("Config_flow import: Received error from ZAMG: %s", err) - return self.async_abort(reason="cannot_connect") - except (ZamgStationNotFoundError) as err: - LOGGER.error("Config_flow import: Received error from ZAMG: %s", err) - return self.async_abort(reason="station_not_found") - - return await self.async_step_user( - user_input={ - CONF_STATION_ID: station_id, - } - ) diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 6b3f8c10700..348052bc5f4 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -3,9 +3,6 @@ from __future__ import annotations from collections.abc import Mapping from dataclasses import dataclass -from typing import Union - -import voluptuous as vol from homeassistant.components.sensor import ( SensorDeviceClass, @@ -13,12 +10,8 @@ from homeassistant.components.sensor import ( SensorEntityDescription, SensorStateClass, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_MONITORED_CONDITIONS, - CONF_NAME, DEGREE, PERCENTAGE, UnitOfPrecipitationDepth, @@ -28,11 +21,10 @@ from homeassistant.const import ( UnitOfTime, ) 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.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -40,21 +32,17 @@ from .const import ( ATTR_UPDATED, ATTRIBUTION, CONF_STATION_ID, - DEFAULT_NAME, DOMAIN, MANUFACTURER_URL, ) from .coordinator import ZamgDataUpdateCoordinator -_DType = Union[type[int], type[float], type[str]] - @dataclass class ZamgRequiredKeysMixin: """Mixin for required keys.""" para_name: str - dtype: _DType @dataclass @@ -70,7 +58,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, para_name="P", - dtype=float, ), ZamgSensorEntityDescription( key="pressure_sealevel", @@ -79,7 +66,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.PRESSURE, state_class=SensorStateClass.MEASUREMENT, para_name="PRED", - dtype=float, ), ZamgSensorEntityDescription( key="humidity", @@ -88,7 +74,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.HUMIDITY, state_class=SensorStateClass.MEASUREMENT, para_name="RFAM", - dtype=int, ), ZamgSensorEntityDescription( key="wind_speed", @@ -97,7 +82,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.WIND_SPEED, state_class=SensorStateClass.MEASUREMENT, para_name="FFAM", - dtype=float, ), ZamgSensorEntityDescription( key="wind_bearing", @@ -105,7 +89,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( native_unit_of_measurement=DEGREE, state_class=SensorStateClass.MEASUREMENT, para_name="DD", - dtype=int, ), ZamgSensorEntityDescription( key="wind_max_speed", @@ -114,7 +97,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.WIND_SPEED, state_class=SensorStateClass.MEASUREMENT, para_name="FFX", - dtype=float, ), ZamgSensorEntityDescription( key="wind_max_bearing", @@ -122,7 +104,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( native_unit_of_measurement=DEGREE, state_class=SensorStateClass.MEASUREMENT, para_name="DDX", - dtype=int, ), ZamgSensorEntityDescription( key="sun_last_10min", @@ -130,7 +111,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfTime.SECONDS, state_class=SensorStateClass.MEASUREMENT, para_name="SO", - dtype=int, ), ZamgSensorEntityDescription( key="temperature", @@ -139,7 +119,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, para_name="TL", - dtype=float, ), ZamgSensorEntityDescription( key="temperature_average", @@ -148,7 +127,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, para_name="TLAM", - dtype=float, ), ZamgSensorEntityDescription( key="precipitation", @@ -157,7 +135,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.PRECIPITATION, state_class=SensorStateClass.MEASUREMENT, para_name="RR", - dtype=float, ), ZamgSensorEntityDescription( key="snow", @@ -166,7 +143,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.PRECIPITATION, state_class=SensorStateClass.MEASUREMENT, para_name="SCHNEE", - dtype=float, ), ZamgSensorEntityDescription( key="dewpoint", @@ -175,7 +151,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, para_name="TP", - dtype=float, ), ZamgSensorEntityDescription( key="dewpoint_average", @@ -184,7 +159,6 @@ SENSOR_TYPES: tuple[ZamgSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, para_name="TPAM", - dtype=float, ), ) @@ -192,39 +166,6 @@ SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES] API_FIELDS: list[str] = [desc.para_name for desc in SENSOR_TYPES] -PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_MONITORED_CONDITIONS, default=["temperature"]): vol.All( - cv.ensure_list, [vol.In(SENSOR_KEYS)] - ), - vol.Optional(CONF_STATION_ID): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Inclusive( - CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together" - ): cv.latitude, - vol.Inclusive( - CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together" - ): cv.longitude, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the ZAMG sensor platform.""" - # trigger import flow - 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 diff --git a/homeassistant/components/zamg/translations/de.json b/homeassistant/components/zamg/translations/de.json index 8ec2acfe302..c9ac43e96c5 100644 --- a/homeassistant/components/zamg/translations/de.json +++ b/homeassistant/components/zamg/translations/de.json @@ -3,17 +3,17 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", - "station_not_found": "Stations-ID bei zamg nicht gefunden" + "station_not_found": "Stations-ID bei ZAMG nicht gefunden" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "station_not_found": "Stations-ID bei zamg nicht gefunden" + "station_not_found": "Stations-ID bei ZAMG nicht gefunden" }, "flow_title": "{name}", "step": { "user": { "data": { - "station_id": "Station-ID (standardm\u00e4\u00dfig n\u00e4chste Station)" + "station_id": "Station-ID (standardm\u00e4\u00dfig die n\u00e4chstgelegene Station)" }, "description": "Richte ZAMG f\u00fcr die Integration mit Home Assistant ein." } diff --git a/homeassistant/components/zamg/translations/lv.json b/homeassistant/components/zamg/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/zamg/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/nl.json b/homeassistant/components/zamg/translations/nl.json index 2bdc82453e8..5412159a5f2 100644 --- a/homeassistant/components/zamg/translations/nl.json +++ b/homeassistant/components/zamg/translations/nl.json @@ -7,6 +7,18 @@ "error": { "cannot_connect": "Kan geen verbinding maken" }, - "flow_title": "{name}" + "flow_title": "{name}", + "step": { + "user": { + "data": { + "station_id": "Station ID (standaard het dichtstbijzijnde station)" + } + } + } + }, + "issues": { + "deprecated_yaml": { + "title": "De ZAMG YAML-configuratie wordt verwijderd" + } } } \ No newline at end of file diff --git a/homeassistant/components/zamg/translations/tr.json b/homeassistant/components/zamg/translations/tr.json index 3ebc2a2788b..44b929de968 100644 --- a/homeassistant/components/zamg/translations/tr.json +++ b/homeassistant/components/zamg/translations/tr.json @@ -2,10 +2,12 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "station_not_found": "\u0130stasyon kimli\u011fi zamg'da bulunamad\u0131" }, "error": { - "cannot_connect": "Ba\u011flanma hatas\u0131" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "station_not_found": "\u0130stasyon kimli\u011fi zamg'da bulunamad\u0131" }, "flow_title": "{name}", "step": { diff --git a/homeassistant/components/zamg/weather.py b/homeassistant/components/zamg/weather.py index c57574d97ec..fc297399b97 100644 --- a/homeassistant/components/zamg/weather.py +++ b/homeassistant/components/zamg/weather.py @@ -1,60 +1,23 @@ """Sensor for zamg the Austrian "Zentralanstalt für Meteorologie und Geodynamik" integration.""" from __future__ import annotations -import voluptuous as vol - -from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.components.weather import WeatherEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, UnitOfPrecipitationDepth, UnitOfPressure, UnitOfSpeed, UnitOfTemperature, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import 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 from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ATTRIBUTION, CONF_STATION_ID, DOMAIN, MANUFACTURER_URL from .coordinator import ZamgDataUpdateCoordinator -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_STATION_ID): cv.string, - vol.Inclusive( - CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together" - ): cv.latitude, - vol.Inclusive( - CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together" - ): cv.longitude, - } -) - - -async def async_setup_platform( - hass: HomeAssistant, - config: ConfigType, - async_add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, -) -> None: - """Set up the ZAMG weather platform.""" - # trigger import flow - 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 diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index a3865f5e168..6b54aa18961 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -141,7 +141,7 @@ async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZero @callback def _async_zc_has_functional_dual_stack() -> bool: - """Return true for platforms that not support IP_ADD_MEMBERSHIP on an AF_INET6 socket. + """Return true for platforms not supporting IP_ADD_MEMBERSHIP on an AF_INET6 socket. Zeroconf only supports a single listen socket at this time. """ @@ -275,7 +275,8 @@ async def _async_register_hass_zc_service( adapters = await network.async_get_adapters(hass) # Puts the default IPv4 address first in the list to preserve compatibility, - # because some mDNS implementations ignores anything but the first announced address. + # because some mDNS implementations ignores anything but the first announced + # address. host_ip = await async_get_source_ip(hass, target_ip=MDNS_TARGET_IP) host_ip_pton = None if host_ip: @@ -429,15 +430,17 @@ class ZeroconfDiscovery: integration: Integration = await async_get_integration( self.hass, domain ) - # Since we prefer local control, if the integration that is being discovered - # is cloud AND the homekit device is UNPAIRED we still want to discovery it. + # Since we prefer local control, if the integration that is being + # discovered is cloud AND the homekit device is UNPAIRED we still + # want to discovery it. # - # Additionally if the integration is polling, HKC offers a local push - # experience for the user to control the device so we want to offer that - # as well. + # Additionally if the integration is polling, HKC offers a local + # push experience for the user to control the device so we want + # to offer that as well. # - # As soon as the device becomes paired, the config flow will be dismissed - # in the event the user does not want to pair with Home Assistant. + # As soon as the device becomes paired, the config flow will be + # dismissed in the event the user does not want to pair + # with Home Assistant. # if not integration.iot_class or ( not integration.iot_class.startswith("cloud") @@ -468,7 +471,8 @@ class ZeroconfDiscovery: "source": config_entries.SOURCE_ZEROCONF, } if domain: - # Domain of integration that offers alternative API to handle this device. + # Domain of integration that offers alternative API to handle + # this device. context["alternative_domain"] = domain discovery_flow.async_create_flow( diff --git a/homeassistant/components/zeroconf/usage.py b/homeassistant/components/zeroconf/usage.py index 7cedb11a418..0c452149bfd 100644 --- a/homeassistant/components/zeroconf/usage.py +++ b/homeassistant/components/zeroconf/usage.py @@ -10,7 +10,10 @@ from .models import HaZeroconf def install_multiple_zeroconf_catcher(hass_zc: HaZeroconf) -> None: - """Wrap the Zeroconf class to return the shared instance if multiple instances are detected.""" + """Wrap the Zeroconf class to return the shared instance. + + Only if if multiple instances are detected. + """ def new_zeroconf_new(self: zeroconf.Zeroconf, *k: Any, **kw: Any) -> HaZeroconf: report( diff --git a/homeassistant/components/zeversolar/__init__.py b/homeassistant/components/zeversolar/__init__.py new file mode 100644 index 00000000000..cff5cf413e5 --- /dev/null +++ b/homeassistant/components/zeversolar/__init__.py @@ -0,0 +1,25 @@ +"""The Zeversolar integration.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN, PLATFORMS +from .coordinator import ZeversolarCoordinator + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Zeversolar from a config entry.""" + coordinator = ZeversolarCoordinator(hass=hass, entry=entry) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/zeversolar/config_flow.py b/homeassistant/components/zeversolar/config_flow.py new file mode 100644 index 00000000000..f749b9d471c --- /dev/null +++ b/homeassistant/components/zeversolar/config_flow.py @@ -0,0 +1,61 @@ +"""Config flow for zeversolar integration.""" +from __future__ import annotations + +import logging +from typing import Any + +import voluptuous as vol +import zeversolar + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers import config_validation as cv + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + }, +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for zeversolar.""" + + 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 = {} + + client = zeversolar.ZeverSolarClient(host=user_input[CONF_HOST]) + try: + data = await self.hass.async_add_executor_job(client.get_data) + except zeversolar.ZeverSolarHTTPNotFound: + errors["base"] = "invalid_host" + except zeversolar.ZeverSolarHTTPError: + errors["base"] = "cannot_connect" + except zeversolar.ZeverSolarTimeout: + errors["base"] = "timeout_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(data.serial_number) + self._abort_if_unique_id_configured() + return self.async_create_entry(title="Zeversolar", data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/zeversolar/const.py b/homeassistant/components/zeversolar/const.py new file mode 100644 index 00000000000..e3622fefe33 --- /dev/null +++ b/homeassistant/components/zeversolar/const.py @@ -0,0 +1,9 @@ +"""Constants for the zeversolar integration.""" + +from homeassistant.const import Platform + +DOMAIN = "zeversolar" + +PLATFORMS = [ + Platform.SENSOR, +] diff --git a/homeassistant/components/zeversolar/coordinator.py b/homeassistant/components/zeversolar/coordinator.py new file mode 100644 index 00000000000..554fe195eab --- /dev/null +++ b/homeassistant/components/zeversolar/coordinator.py @@ -0,0 +1,34 @@ +"""Zeversolar coordinator.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +import zeversolar + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ZeversolarCoordinator(DataUpdateCoordinator[zeversolar.ZeverSolarData]): + """Data update coordinator.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=timedelta(minutes=1), + ) + self._client = zeversolar.ZeverSolarClient(host=entry.data[CONF_HOST]) + + async def _async_update_data(self) -> zeversolar.ZeverSolarData: + """Fetch the latest data from the source.""" + return await self.hass.async_add_executor_job(self._client.get_data) diff --git a/homeassistant/components/zeversolar/entity.py b/homeassistant/components/zeversolar/entity.py new file mode 100644 index 00000000000..ccda0add910 --- /dev/null +++ b/homeassistant/components/zeversolar/entity.py @@ -0,0 +1,29 @@ +"""Base Entity for Zeversolar sensors.""" +from __future__ import annotations + +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import ZeversolarCoordinator + + +class ZeversolarEntity( + CoordinatorEntity[ZeversolarCoordinator], +): + """Defines a base Zeversolar entity.""" + + _attr_has_entity_name = True + + def __init__( + self, + *, + coordinator: ZeversolarCoordinator, + ) -> None: + """Initialize the Zeversolar entity.""" + super().__init__(coordinator=coordinator) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.data.serial_number)}, + name="Zeversolar Sensor", + manufacturer="Zeversolar", + ) diff --git a/homeassistant/components/zeversolar/manifest.json b/homeassistant/components/zeversolar/manifest.json new file mode 100644 index 00000000000..0d67022920d --- /dev/null +++ b/homeassistant/components/zeversolar/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "zeversolar", + "name": "Zeversolar", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/zeversolar", + "requirements": ["zeversolar==0.2.0"], + "codeowners": ["@kvanzuijlen"], + "iot_class": "local_polling", + "integration_type": "device" +} diff --git a/homeassistant/components/zeversolar/sensor.py b/homeassistant/components/zeversolar/sensor.py new file mode 100644 index 00000000000..746434faeeb --- /dev/null +++ b/homeassistant/components/zeversolar/sensor.py @@ -0,0 +1,96 @@ +"""Support for the Zeversolar platform.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +import zeversolar + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfEnergy, UnitOfPower +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import ZeversolarCoordinator +from .entity import ZeversolarEntity + + +@dataclass +class ZeversolarEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[zeversolar.ZeverSolarData], zeversolar.kWh | zeversolar.Watt] + + +@dataclass +class ZeversolarEntityDescription( + SensorEntityDescription, ZeversolarEntityDescriptionMixin +): + """Describes Zeversolar sensor entity.""" + + +SENSOR_TYPES = ( + ZeversolarEntityDescription( + key="pac", + name="Current power", + icon="mdi:solar-power-variant", + native_unit_of_measurement=UnitOfPower.WATT, + state_class=SensorStateClass.MEASUREMENT, + entity_category=EntityCategory.DIAGNOSTIC, + device_class=SensorDeviceClass.POWER, + value_fn=lambda data: data.pac, + ), + ZeversolarEntityDescription( + key="energy_today", + name="Energy today", + icon="mdi:home-battery", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + state_class=SensorStateClass.TOTAL_INCREASING, + device_class=SensorDeviceClass.ENERGY, + value_fn=lambda data: data.energy_today, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the Zeversolar sensor.""" + coordinator: ZeversolarCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + ZeversolarSensor( + description=description, + coordinator=coordinator, + ) + for description in SENSOR_TYPES + ) + + +class ZeversolarSensor(ZeversolarEntity, SensorEntity): + """Implementation of the Zeversolar sensor.""" + + entity_description: ZeversolarEntityDescription + + def __init__( + self, + *, + description: ZeversolarEntityDescription, + coordinator: ZeversolarCoordinator, + ) -> None: + """Initialize the sensor.""" + self.entity_description = description + super().__init__(coordinator=coordinator) + self._attr_unique_id = f"{coordinator.data.serial_number}_{description.key}" + + @property + def native_value(self) -> int | float: + """Return sensor state.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/zeversolar/strings.json b/homeassistant/components/zeversolar/strings.json new file mode 100644 index 00000000000..a4f52dc6aa3 --- /dev/null +++ b/homeassistant/components/zeversolar/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "timeout_connect": "[%key:common::config_flow::error::timeout_connect%]", + "invalid_host": "[%key:common::config_flow::error::invalid_host%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/zeversolar/translations/bg.json b/homeassistant/components/zeversolar/translations/bg.json new file mode 100644 index 00000000000..ae714967b0b --- /dev/null +++ b/homeassistant/components/zeversolar/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", + "invalid_host": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u0438\u043c\u0435 \u043d\u0430 \u0445\u043e\u0441\u0442 \u0438\u043b\u0438 IP \u0430\u0434\u0440\u0435\u0441", + "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" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/ca.json b/homeassistant/components/zeversolar/translations/ca.json new file mode 100644 index 00000000000..d2fd04f5f5f --- /dev/null +++ b/homeassistant/components/zeversolar/translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids", + "timeout_connect": "S'ha esgotat el temps m\u00e0xim d'espera per establir connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/cs.json b/homeassistant/components/zeversolar/translations/cs.json new file mode 100644 index 00000000000..e1bf8e7f45f --- /dev/null +++ b/homeassistant/components/zeversolar/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/zeversolar/translations/de.json b/homeassistant/components/zeversolar/translations/de.json new file mode 100644 index 00000000000..66618c92f91 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse", + "timeout_connect": "Zeit\u00fcberschreitung beim Verbindungsaufbau", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/el.json b/homeassistant/components/zeversolar/translations/el.json new file mode 100644 index 00000000000..ef988355e8a --- /dev/null +++ b/homeassistant/components/zeversolar/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_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", + "timeout_connect": "\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/zeversolar/translations/en.json b/homeassistant/components/zeversolar/translations/en.json new file mode 100644 index 00000000000..bab043811b5 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_host": "Invalid hostname or IP address", + "timeout_connect": "Timeout establishing connection", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/es.json b/homeassistant/components/zeversolar/translations/es.json new file mode 100644 index 00000000000..34903a2b6b1 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos", + "timeout_connect": "Tiempo de espera agotado para establecer la conexi\u00f3n", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/et.json b/homeassistant/components/zeversolar/translations/et.json new file mode 100644 index 00000000000..1af2598a4da --- /dev/null +++ b/homeassistant/components/zeversolar/translations/et.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_host": "Sobimatu hostinimi v\u00f5i IP-aadress", + "timeout_connect": "\u00dchenduse loomise ajal\u00f5pp", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/fr.json b/homeassistant/components/zeversolar/translations/fr.json new file mode 100644 index 00000000000..4e5ecd503dc --- /dev/null +++ b/homeassistant/components/zeversolar/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_host": "Nom d'h\u00f4te ou adresse IP non valide", + "timeout_connect": "D\u00e9lai d'attente pour \u00e9tablir la connexion expir\u00e9", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/he.json b/homeassistant/components/zeversolar/translations/he.json new file mode 100644 index 00000000000..099af2231dc --- /dev/null +++ b/homeassistant/components/zeversolar/translations/he.json @@ -0,0 +1,20 @@ +{ + "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_host": "\u05e9\u05dd \u05de\u05d0\u05e8\u05d7 \u05d0\u05d5 \u05db\u05ea\u05d5\u05d1\u05ea IP \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9\u05d9\u05dd", + "timeout_connect": "\u05e4\u05e1\u05e7 \u05d6\u05de\u05df \u05dc\u05d9\u05e6\u05d9\u05e8\u05ea \u05d7\u05d9\u05d1\u05d5\u05e8", + "unknown": "\u05e9\u05d2\u05d9\u05d0\u05d4 \u05d1\u05dc\u05ea\u05d9 \u05e6\u05e4\u05d5\u05d9\u05d4" + }, + "step": { + "user": { + "data": { + "host": "\u05de\u05d0\u05e8\u05d7" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/hu.json b/homeassistant/components/zeversolar/translations/hu.json new file mode 100644 index 00000000000..bbf0dfb3557 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/hu.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_host": "\u00c9rv\u00e9nytelen hosztn\u00e9v vagy IP-c\u00edm", + "timeout_connect": "Id\u0151t\u00fall\u00e9p\u00e9s a kapcsolat l\u00e9trehoz\u00e1sa sor\u00e1n", + "unknown": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt" + }, + "step": { + "user": { + "data": { + "host": "C\u00edm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/id.json b/homeassistant/components/zeversolar/translations/id.json new file mode 100644 index 00000000000..f2fea9b49bf --- /dev/null +++ b/homeassistant/components/zeversolar/translations/id.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Perangkat sudah dikonfigurasi" + }, + "error": { + "cannot_connect": "Gagal terhubung", + "invalid_host": "Nama host atau alamat IP tidak valid", + "timeout_connect": "Tenggang waktu pembuatan koneksi habis", + "unknown": "Kesalahan yang tidak diharapkan" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/it.json b/homeassistant/components/zeversolar/translations/it.json new file mode 100644 index 00000000000..1ce109e85d7 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_host": "Nome host o indirizzo IP non valido", + "timeout_connect": "Tempo scaduto per stabile la connessione.", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/ja.json b/homeassistant/components/zeversolar/translations/ja.json new file mode 100644 index 00000000000..a42202307f2 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/ja.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "\u30db\u30b9\u30c8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/lv.json b/homeassistant/components/zeversolar/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/nl.json b/homeassistant/components/zeversolar/translations/nl.json new file mode 100644 index 00000000000..12ec47e61bc --- /dev/null +++ b/homeassistant/components/zeversolar/translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_host": "Ongeldige hostnaam of IP-adres", + "timeout_connect": "Time-out bij het maken van verbinding", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/no.json b/homeassistant/components/zeversolar/translations/no.json new file mode 100644 index 00000000000..616a85b9078 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_host": "Ugyldig vertsnavn eller IP-adresse", + "timeout_connect": "Tidsavbrudd oppretter forbindelse", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/pl.json b/homeassistant/components/zeversolar/translations/pl.json new file mode 100644 index 00000000000..49cc755998e --- /dev/null +++ b/homeassistant/components/zeversolar/translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_host": "Nieprawid\u0142owa nazwa hosta lub adres IP", + "timeout_connect": "Limit czasu na nawi\u0105zanie 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/zeversolar/translations/pt-BR.json b/homeassistant/components/zeversolar/translations/pt-BR.json new file mode 100644 index 00000000000..ee43d8a8851 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/pt-BR.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falhou ao conectar", + "invalid_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido", + "timeout_connect": "Tempo limite estabelecendo conex\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/ru.json b/homeassistant/components/zeversolar/translations/ru.json new file mode 100644 index 00000000000..7fda9d8c313 --- /dev/null +++ b/homeassistant/components/zeversolar/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." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441.", + "timeout_connect": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/sk.json b/homeassistant/components/zeversolar/translations/sk.json new file mode 100644 index 00000000000..39ddba280e5 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/sk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Zariadenie u\u017e je nakonfigurovan\u00e9" + }, + "error": { + "cannot_connect": "Nepodarilo sa pripoji\u0165", + "invalid_host": "Neplatn\u00fd n\u00e1zov hostite\u013ea alebo IP adresa", + "timeout_connect": "\u010casov\u00fd limit na nadviazanie spojenia", + "unknown": "Neo\u010dak\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostite\u013e" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/tr.json b/homeassistant/components/zeversolar/translations/tr.json new file mode 100644 index 00000000000..5ea7522df66 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_host": "Ge\u00e7ersiz ana bilgisayar ad\u0131 veya IP adresi", + "timeout_connect": "Ba\u011flant\u0131 kurulurken zaman a\u015f\u0131m\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Sunucu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/uk.json b/homeassistant/components/zeversolar/translations/uk.json new file mode 100644 index 00000000000..9933faa189a --- /dev/null +++ b/homeassistant/components/zeversolar/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0443\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f", + "invalid_host": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0435 \u0456\u043c'\u044f \u0445\u043e\u0441\u0442\u0430 \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "timeout_connect": "\u0427\u0430\u0441 \u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0437\u2019\u0454\u0434\u043d\u0430\u043d\u043d\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeversolar/translations/zh-Hant.json b/homeassistant/components/zeversolar/translations/zh-Hant.json new file mode 100644 index 00000000000..5255f425730 --- /dev/null +++ b/homeassistant/components/zeversolar/translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", + "timeout_connect": "\u5efa\u7acb\u9023\u7dda\u903e\u6642", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 70b9dfd9b46..48c10304ad5 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -7,8 +7,8 @@ import voluptuous as vol from zhaquirks import setup as setup_quirks from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH -from homeassistant import const as ha_const from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_TYPE, EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv @@ -39,7 +39,7 @@ from .core.const import ( ) from .core.discovery import GROUP_PROBE -DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(ha_const.CONF_TYPE): cv.string}) +DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(CONF_TYPE): cv.string}) ZHA_CONFIG_SCHEMA = { vol.Optional(CONF_BAUDRATE): cv.positive_int, vol.Optional(CONF_DATABASE): cv.string, @@ -99,7 +99,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b if config.get(CONF_ENABLE_QUIRKS, True): setup_quirks(config) - # temporary code to remove the zha storage file from disk. this will be removed in 2022.10.0 + # temporary code to remove the ZHA storage file from disk. this will be removed in 2022.10.0 storage_path = hass.config.path(STORAGE_DIR, "zha.storage") if os.path.isfile(storage_path): _LOGGER.debug("removing ZHA storage file") @@ -128,7 +128,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await zha_gateway.shutdown() zha_data[DATA_ZHA_SHUTDOWN_TASK] = hass.bus.async_listen_once( - ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown + EVENT_HOMEASSISTANT_STOP, async_zha_shutdown ) await zha_gateway.async_initialize_devices_and_entities() diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 59f2bfa331b..d0e04e0c162 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -800,7 +800,7 @@ async def websocket_device_cluster_commands( async def websocket_read_zigbee_cluster_attributes( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: - """Read zigbee attribute for cluster on zha entity.""" + """Read zigbee attribute for cluster on ZHA entity.""" zha_gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee: EUI64 = msg[ATTR_IEEE] endpoint_id: int = msg[ATTR_ENDPOINT_ID] @@ -1318,7 +1318,7 @@ def async_load_api(hass: HomeAssistant) -> None: ) async def issue_zigbee_cluster_command(service: ServiceCall) -> None: - """Issue command on zigbee cluster on zha entity.""" + """Issue command on zigbee cluster on ZHA entity.""" ieee: EUI64 = service.data[ATTR_IEEE] endpoint_id: int = service.data[ATTR_ENDPOINT_ID] cluster_id: int = service.data[ATTR_CLUSTER_ID] diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index df5fa047c99..a92a2c13a76 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -37,6 +37,7 @@ DECONZ_DOMAIN = "deconz" FORMATION_STRATEGY = "formation_strategy" FORMATION_FORM_NEW_NETWORK = "form_new_network" +FORMATION_FORM_INITIAL_NETWORK = "form_initial_network" FORMATION_REUSE_SETTINGS = "reuse_settings" FORMATION_CHOOSE_AUTOMATIC_BACKUP = "choose_automatic_backup" FORMATION_UPLOAD_MANUAL_BACKUP = "upload_manual_backup" @@ -270,8 +271,21 @@ class BaseZhaFlow(FlowHandler): strategies.append(FORMATION_REUSE_SETTINGS) strategies.append(FORMATION_UPLOAD_MANUAL_BACKUP) - strategies.append(FORMATION_FORM_NEW_NETWORK) + # Do not show "erase network settings" if there are none to erase + if self._radio_mgr.current_settings is None: + strategies.append(FORMATION_FORM_INITIAL_NETWORK) + else: + strategies.append(FORMATION_FORM_NEW_NETWORK) + + # Automatically form a new network if we're onboarding with a brand new radio + if not onboarding.async_is_onboarded(self.hass) and set(strategies) == { + FORMATION_UPLOAD_MANUAL_BACKUP, + FORMATION_FORM_INITIAL_NETWORK, + }: + return await self.async_step_form_initial_network() + + # Otherwise, let the user choose return self.async_show_menu( step_id="choose_formation_strategy", menu_options=strategies, @@ -283,10 +297,17 @@ class BaseZhaFlow(FlowHandler): """Reuse the existing network settings on the stick.""" return await self._async_create_radio_entry() + async def async_step_form_initial_network( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Form an initial network.""" + # This step exists only for translations, it does nothing new + return await self.async_step_form_new_network(user_input) + async def async_step_form_new_network( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Form a brand new network.""" + """Form a brand-new network.""" await self._radio_mgr.async_form_network() return await self._async_create_radio_entry() @@ -422,7 +443,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: - """Handle a zha config flow start.""" + """Handle a ZHA config flow start.""" if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") @@ -439,7 +460,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN return self.async_abort(reason="single_instance_allowed") # Without confirmation, discovery can automatically progress into parts of the - # config flow logic that interacts with hardware! + # config flow logic that interacts with hardware. if user_input is not None or not onboarding.async_is_onboarded(self.hass): # Probe the radio type if we don't have one yet if ( @@ -518,7 +539,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN else: self._radio_mgr.radio_type = RadioType.znp - node_name = local_name[: -len(".local")] + node_name = local_name.removesuffix(".local") device_path = f"socket://{discovery_info.host}:{port}" await self._set_unique_id_or_update_path( diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 849cdc29e47..7484b46256c 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -87,7 +87,7 @@ class Channels: @property def zha_device(self) -> ZHADevice: - """Return parent zha device.""" + """Return parent ZHA device.""" return self._zha_device @property diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index a1f7c6df7e1..55d77d507fd 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -45,6 +45,7 @@ class ColorChannel(ZigbeeChannel): "color_capabilities": True, "color_loop_active": False, "start_up_color_temperature": True, + "options": True, } @cached_property @@ -167,3 +168,13 @@ class ColorChannel(ZigbeeChannel): self.color_capabilities is not None and lighting.Color.ColorCapabilities.Color_loop in self.color_capabilities ) + + @property + def options(self) -> lighting.Color.Options: + """Return ZCL options of the channel.""" + return lighting.Color.Options(self.cluster.get("options", 0)) + + @property + def execute_if_off_supported(self) -> bool: + """Return True if the channel can execute commands when off.""" + return lighting.Color.Options.Execute_if_off in self.options diff --git a/homeassistant/components/zha/core/channels/lightlink.py b/homeassistant/components/zha/core/channels/lightlink.py index a29d9020a75..2884769d10f 100644 --- a/homeassistant/components/zha/core/channels/lightlink.py +++ b/homeassistant/components/zha/core/channels/lightlink.py @@ -17,7 +17,7 @@ class LightLink(ZigbeeChannel): BIND: bool = False async def async_configure(self) -> None: - """Add Coordinator to LightLink group .""" + """Add Coordinator to LightLink group.""" if self._ch_pool.skip_configuration: self._status = ChannelStatus.CONFIGURED diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 427579cfb59..e6b88a6c9ad 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -224,7 +224,8 @@ class InovelliConfigEntityChannel(ZigbeeChannel): "switch_type": False, "button_delay": False, "smart_bulb_mode": False, - "double_tap_up_for_full_brightness": True, + "double_tap_up_for_max_brightness": True, + "double_tap_down_for_min_brightness": True, "led_color_when_on": True, "led_color_when_off": True, "led_intensity_when_on": True, diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 9463a0351c1..a2e9d19818b 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -76,7 +76,7 @@ class IasAce(ZigbeeChannel): self.armed_state: AceCluster.PanelStatus = AceCluster.PanelStatus.Panel_Disarmed self.invalid_tries: int = 0 - # These will all be setup by the entity from zha configuration + # These will all be setup by the entity from ZHA configuration self.panel_code: str = "1234" self.code_required_arm_actions = False self.max_invalid_tries: int = 3 diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 66d3e3d6810..03d11356f0a 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -68,6 +68,24 @@ class Metering(ZigbeeChannel): REPORT_CONFIG = ( AttrReportConfig(attr="instantaneous_demand", config=REPORT_CONFIG_OP), AttrReportConfig(attr="current_summ_delivered", config=REPORT_CONFIG_DEFAULT), + AttrReportConfig( + attr="current_tier1_summ_delivered", config=REPORT_CONFIG_DEFAULT + ), + AttrReportConfig( + attr="current_tier2_summ_delivered", config=REPORT_CONFIG_DEFAULT + ), + AttrReportConfig( + attr="current_tier3_summ_delivered", config=REPORT_CONFIG_DEFAULT + ), + AttrReportConfig( + attr="current_tier4_summ_delivered", config=REPORT_CONFIG_DEFAULT + ), + AttrReportConfig( + attr="current_tier5_summ_delivered", config=REPORT_CONFIG_DEFAULT + ), + AttrReportConfig( + attr="current_tier6_summ_delivered", config=REPORT_CONFIG_DEFAULT + ), AttrReportConfig(attr="status", config=REPORT_CONFIG_ASAP), ) ZCL_INIT_ATTRS = { diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index c6958abc742..8a773213a58 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -133,6 +133,7 @@ CONF_DEVICE_CONFIG = "device_config" CONF_ENABLE_ENHANCED_LIGHT_TRANSITION = "enhanced_light_transition" CONF_ENABLE_LIGHT_TRANSITIONING_FLAG = "light_transitioning_flag" CONF_ALWAYS_PREFER_XY_COLOR_MODE = "always_prefer_xy_color_mode" +CONF_GROUP_MEMBERS_ASSUME_STATE = "group_members_assume_state" CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join" CONF_ENABLE_QUIRKS = "enable_quirks" CONF_FLOWCONTROL = "flow_control" @@ -151,6 +152,7 @@ CONF_ZHA_OPTIONS_SCHEMA = vol.Schema( vol.Required(CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, default=False): cv.boolean, vol.Required(CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, default=True): cv.boolean, vol.Required(CONF_ALWAYS_PREFER_XY_COLOR_MODE, default=True): cv.boolean, + vol.Required(CONF_GROUP_MEMBERS_ASSUME_STATE, default=True): cv.boolean, vol.Required(CONF_ENABLE_IDENTIFY_ON_JOIN, default=True): cv.boolean, vol.Optional( CONF_CONSIDER_UNAVAILABLE_MAINS, diff --git a/homeassistant/components/zha/core/decorators.py b/homeassistant/components/zha/core/decorators.py index c57cad7d65e..5cf9322170f 100644 --- a/homeassistant/components/zha/core/decorators.py +++ b/homeassistant/components/zha/core/decorators.py @@ -2,12 +2,12 @@ from __future__ import annotations from collections.abc import Callable -from typing import Any, TypeVar, Union +from typing import Any, TypeVar _TypeT = TypeVar("_TypeT", bound=type[Any]) -class DictRegistry(dict[Union[int, str], _TypeT]): +class DictRegistry(dict[int | str, _TypeT]): """Dict Registry of items.""" def register(self, name: int | str) -> Callable[[_TypeT], _TypeT]: @@ -21,7 +21,7 @@ class DictRegistry(dict[Union[int, str], _TypeT]): return decorator -class SetRegistry(set[Union[int, str]]): +class SetRegistry(set[int | str]): """Set Registry of items.""" def register(self, name: int | str) -> Callable[[_TypeT], _TypeT]: diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 6b690f4da08..ec83f489d39 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -129,7 +129,7 @@ class ProbeEndpoint: self.probe_single_cluster(component, channel, channel_pool) - # until we can get rid off registries + # until we can get rid of registries self.handle_on_off_output_cluster_exception(channel_pool) @staticmethod @@ -254,7 +254,7 @@ class GroupProbe: ) def cleanup(self) -> None: - """Clean up on when zha shuts down.""" + """Clean up on when ZHA shuts down.""" for unsub in self._unsubs[:]: unsub() self._unsubs.remove(unsub) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index ffd005e8edc..5b04c623306 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -11,7 +11,7 @@ import logging import re import time import traceback -from typing import TYPE_CHECKING, Any, NamedTuple, Union +from typing import TYPE_CHECKING, Any, NamedTuple from zigpy.application import ControllerApplication from zigpy.config import CONF_DEVICE @@ -91,7 +91,7 @@ if TYPE_CHECKING: from ..entity import ZhaEntity from .channels.base import ZigbeeChannel - _LogFilterType = Union[Filter, Callable[[LogRecord], int]] + _LogFilterType = Filter | Callable[[LogRecord], int] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index 0c86a83c8b3..b0e6a181f25 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -65,7 +65,7 @@ class ZHAGroupMember(LogMixin): @property def device(self) -> ZHADevice: - """Return the zha device for this group member.""" + """Return the ZHA device for this group member.""" return self._zha_device @property diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 38a4c8e3149..431ab8620fc 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -213,7 +213,7 @@ def async_is_bindable_target(source_zha_device, target_zha_device): def async_get_zha_config_value( config_entry: ConfigEntry, section: str, config_key: str, default: _T ) -> _T: - """Get the value for the specified configuration from the zha config entry.""" + """Get the value for the specified configuration from the ZHA config entry.""" return ( config_entry.options.get(CUSTOM_CONFIGURATION, {}) .get(section, {}) diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 42f6bb55f51..5610f7bee1f 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -149,7 +149,7 @@ class MatchRule: def weight(self) -> int: """Return the weight of the matching rule. - Most specific matches should be preferred over less specific. Model matching + More specific matches should be preferred over less specific. Model matching rules have a priority over manufacturer matching rules and rules matching a single model/manufacturer get a better priority over rules matching multiple models/manufacturers. And any model or manufacturers matching rules get better @@ -343,7 +343,7 @@ class ZHAEntityRegistry: def decorator(zha_ent: _ZhaEntityT) -> _ZhaEntityT: """Register a strict match rule. - All non empty fields of a match rule must match. + All non-empty fields of a match rule must match. """ self._strict_registry[component][rule] = zha_ent return zha_ent @@ -406,7 +406,7 @@ class ZHAEntityRegistry: def decorator(zha_entity: _ZhaEntityT) -> _ZhaEntityT: """Register a loose match rule. - All non empty fields of a match rule must match. + All non-empty fields of a match rule must match. """ # group the rules by channels self._config_diagnostic_entity_registry[component][stop_on_match_group][ diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 6f78aa6f858..03a13f317f3 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -41,7 +41,7 @@ async def async_validate_trigger_config( zha_device.device_automation_triggers is None or trigger not in zha_device.device_automation_triggers ): - raise InvalidDeviceAutomationConfig + raise InvalidDeviceAutomationConfig(f"device does not have trigger {trigger}") return config diff --git a/homeassistant/components/zha/diagnostics.py b/homeassistant/components/zha/diagnostics.py index 697e7be336c..8b025f6eec8 100644 --- a/homeassistant/components/zha/diagnostics.py +++ b/homeassistant/components/zha/diagnostics.py @@ -69,7 +69,7 @@ def shallow_asdict(obj: Any) -> dict: async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" config: dict = hass.data[DATA_ZHA].get(DATA_ZHA_CONFIG, {}) gateway: ZHAGateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] @@ -94,7 +94,7 @@ async def async_get_config_entry_diagnostics( async def async_get_device_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a device.""" zha_device: ZHADevice = async_get_zha_device(hass, device.id) device_info: dict[str, Any] = zha_device.zha_device_info diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 95a15778fe7..065f2ce1572 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -41,7 +41,7 @@ _ZhaGroupEntitySelfT = TypeVar("_ZhaGroupEntitySelfT", bound="ZhaGroupEntity") _LOGGER = logging.getLogger(__name__) ENTITY_SUFFIX = "entity_suffix" -UPDATE_GROUP_FROM_CHILD_DELAY = 0.5 +DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY = 0.5 class BaseZhaEntity(LogMixin, entity.Entity): @@ -77,7 +77,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): @property def zha_device(self) -> ZHADevice: - """Return the zha device this entity is attached to.""" + """Return the ZHA device this entity is attached to.""" return self._zha_device @property @@ -258,7 +258,7 @@ class ZhaGroupEntity(BaseZhaEntity): zha_device: ZHADevice, **kwargs: Any, ) -> None: - """Initialize a light group.""" + """Initialize a ZHA group.""" super().__init__(unique_id, zha_device, **kwargs) self._available = False self._group = zha_device.gateway.groups.get(group_id) @@ -270,6 +270,7 @@ class ZhaGroupEntity(BaseZhaEntity): self._async_unsub_state_changed: CALLBACK_TYPE | None = None self._handled_group_membership = False self._change_listener_debouncer: Debouncer | None = None + self._update_group_from_child_delay = DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY @property def available(self) -> bool: @@ -316,7 +317,7 @@ class ZhaGroupEntity(BaseZhaEntity): self._change_listener_debouncer = Debouncer( self.hass, _LOGGER, - cooldown=UPDATE_GROUP_FROM_CHILD_DELAY, + cooldown=self._update_group_from_child_delay, immediate=False, function=functools.partial(self.async_update_ha_state, True), ) diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index d4b31a69890..13d63808b61 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -105,7 +105,7 @@ class BaseFan(FanEntity): await self.async_set_percentage(0) async def async_set_percentage(self, percentage: int) -> None: - """Set the speed percenage of the fan.""" + """Set the speed percentage of the fan.""" fan_mode = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) await self._async_set_fan_mode(fan_mode) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index b4c760b27ec..be21d704062 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -29,7 +29,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, Platform, ) -from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, State, callback +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -47,6 +47,7 @@ from .core.const import ( CONF_DEFAULT_LIGHT_TRANSITION, CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, + CONF_GROUP_MEMBERS_ASSUME_STATE, DATA_ZHA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, @@ -67,6 +68,7 @@ DEFAULT_EXTRA_TRANSITION_DELAY_SHORT = 0.25 DEFAULT_EXTRA_TRANSITION_DELAY_LONG = 2.0 DEFAULT_LONG_TRANSITION_TIME = 10 DEFAULT_MIN_BRIGHTNESS = 2 +ASSUME_UPDATE_GROUP_FROM_CHILD_DELAY = 0.05 FLASH_EFFECTS = { light.FLASH_SHORT: Identify.EffectIdentifier.Blink, @@ -79,6 +81,7 @@ PARALLEL_UPDATES = 0 SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed" SIGNAL_LIGHT_GROUP_TRANSITION_START = "zha_light_group_transition_start" SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED = "zha_light_group_transition_finished" +SIGNAL_LIGHT_GROUP_ASSUME_GROUP_STATE = "zha_light_group_assume_group_state" DEFAULT_MIN_TRANSITION_MANUFACTURERS = {"sengled"} COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.XY} @@ -119,7 +122,7 @@ class BaseLight(LogMixin, light.LightEntity): super().__init__(*args, **kwargs) self._attr_min_mireds: int | None = 153 self._attr_max_mireds: int | None = 500 - self._attr_color_mode = ColorMode.UNKNOWN # Set by sub classes + self._attr_color_mode = ColorMode.UNKNOWN # Set by subclasses self._attr_supported_features: int = 0 self._attr_state: bool | None self._off_with_transition: bool = False @@ -132,7 +135,8 @@ class BaseLight(LogMixin, light.LightEntity): self._level_channel = None self._color_channel = None self._identify_channel = None - self._transitioning: bool = False + self._transitioning_individual: bool = False + self._transitioning_group: bool = False self._transition_listener: Callable[[], None] | None = None @property @@ -159,7 +163,7 @@ class BaseLight(LogMixin, light.LightEntity): on at `on_level` Zigbee attribute value, regardless of the last set level """ - if self._transitioning: + if self.is_transitioning: self.debug( "received level %s while transitioning - skipping update", value, @@ -184,6 +188,12 @@ class BaseLight(LogMixin, light.LightEntity): xy_color = kwargs.get(light.ATTR_XY_COLOR) hs_color = kwargs.get(light.ATTR_HS_COLOR) + execute_if_off_supported = ( + self._GROUP_SUPPORTS_EXECUTE_IF_OFF + if isinstance(self, LightGroup) + else self._color_channel and self._color_channel.execute_if_off_supported + ) + set_transition_flag = ( brightness_supported(self._attr_supported_color_modes) or temperature is not None @@ -250,6 +260,7 @@ class BaseLight(LogMixin, light.LightEntity): ) ) and brightness_supported(self._attr_supported_color_modes) + and not execute_if_off_supported ) if ( @@ -284,6 +295,23 @@ class BaseLight(LogMixin, light.LightEntity): # Currently only setting it to "on", as the correct level state will be set at the second move_to_level call self._attr_state = True + if execute_if_off_supported: + self.debug("handling color commands before turning on/level") + if not await self.async_handle_color_commands( + temperature, + duration, # duration is ignored by lights when off + hs_color, + xy_color, + new_color_provided_while_off, + t_log, + ): + # Color calls before on/level calls failed, + # so if the transitioning delay isn't running from a previous call, the flag can be unset immediately + if set_transition_flag and not self._transition_listener: + self.async_transition_complete() + self.debug("turned on: %s", t_log) + return + if ( (brightness is not None or transition) and not new_color_provided_while_off @@ -322,18 +350,20 @@ class BaseLight(LogMixin, light.LightEntity): return self._attr_state = True - if not await self.async_handle_color_commands( - temperature, - duration, - hs_color, - xy_color, - new_color_provided_while_off, - t_log, - ): - # Color calls failed, but as brightness may still transition, we start the timer to unset the flag - self.async_transition_start_timer(transition_time) - self.debug("turned on: %s", t_log) - return + if not execute_if_off_supported: + self.debug("handling color commands after turning on/level") + if not await self.async_handle_color_commands( + temperature, + duration, + hs_color, + xy_color, + new_color_provided_while_off, + t_log, + ): + # Color calls failed, but as brightness may still transition, we start the timer to unset the flag + self.async_transition_start_timer(transition_time) + self.debug("turned on: %s", t_log) + return if new_color_provided_while_off: # The light is has the correct color, so we can now transition it to the correct brightness level. @@ -407,7 +437,7 @@ class BaseLight(LogMixin, light.LightEntity): if self._zha_config_enable_light_transitioning_flag: self.async_transition_set_flag() - # is not none looks odd here but it will override built in bulb transition times if we pass 0 in here + # is not none looks odd here, but it will override built in bulb transition times if we pass 0 in here if transition is not None and supports_level: result = await self._level_channel.move_to_level_with_on_off( level=0, @@ -424,10 +454,15 @@ class BaseLight(LogMixin, light.LightEntity): return self._attr_state = False - if supports_level: - # store current brightness so that the next turn_on uses it. - self._off_with_transition = transition is not None + if supports_level and not self._off_with_transition: + # store current brightness so that the next turn_on uses it: + # when using "enhanced turn on" self._off_brightness = self._attr_brightness + if transition is not None: + # save for when calling turn_on without a brightness: + # current_level is set to 1 after transitioning to level 0, needed for correct state with light groups + self._attr_brightness = 1 + self._off_with_transition = transition is not None self.async_write_ha_state() @@ -503,11 +538,17 @@ class BaseLight(LogMixin, light.LightEntity): return True + @property + def is_transitioning(self) -> bool: + """Return if the light is transitioning.""" + return self._transitioning_individual or self._transitioning_group + @callback def async_transition_set_flag(self) -> None: """Set _transitioning to True.""" self.debug("setting transitioning flag to True") - self._transitioning = True + self._transitioning_individual = True + self._transitioning_group = False if isinstance(self, LightGroup): async_dispatcher_send( self.hass, @@ -519,7 +560,7 @@ class BaseLight(LogMixin, light.LightEntity): @callback def async_transition_start_timer(self, transition_time) -> None: - """Start a timer to unset _transitioning after transition_time if necessary.""" + """Start a timer to unset _transitioning_individual after transition_time if necessary.""" if not transition_time: return # For longer transitions, we want to extend the timer a bit more @@ -534,9 +575,9 @@ class BaseLight(LogMixin, light.LightEntity): @callback def async_transition_complete(self, _=None) -> None: - """Set _transitioning to False and write HA state.""" + """Set _transitioning_individual to False and write HA state.""" self.debug("transition complete - future attribute reports will write HA state") - self._transitioning = False + self._transitioning_individual = False if self._transition_listener: self._transition_listener() self._transition_listener = None @@ -671,7 +712,7 @@ class Light(BaseLight, ZhaEntity): @callback def async_set_state(self, attr_id, attr_name, value): """Set the state.""" - if self._transitioning: + if self.is_transitioning: self.debug( "received onoff %s while transitioning - skipping update", value, @@ -711,7 +752,7 @@ class Light(BaseLight, ZhaEntity): self.debug( "group transition started - setting member transitioning flag" ) - self._transitioning = True + self._transitioning_group = True self.async_accept_signal( None, @@ -727,7 +768,7 @@ class Light(BaseLight, ZhaEntity): self.debug( "group transition completed - unsetting member transitioning flag" ) - self._transitioning = False + self._transitioning_group = False self.async_accept_signal( None, @@ -736,6 +777,13 @@ class Light(BaseLight, ZhaEntity): signal_override=True, ) + self.async_accept_signal( + None, + SIGNAL_LIGHT_GROUP_ASSUME_GROUP_STATE, + self._assume_group_state, + signal_override=True, + ) + async def async_will_remove_from_hass(self) -> None: """Disconnect entity object when removed.""" assert self._cancel_refresh_handle @@ -768,18 +816,31 @@ class Light(BaseLight, ZhaEntity): if not self._attr_available: return self.debug("polling current state") + if self._on_off_channel: state = await self._on_off_channel.get_attribute_value( "on_off", from_cache=False ) + # check if transition started whilst waiting for polled state + if self.is_transitioning: + return + if state is not None: self._attr_state = state + if state: # reset "off with transition" flag if the light is on + self._off_with_transition = False + self._off_brightness = None + if self._level_channel: level = await self._level_channel.get_attribute_value( "current_level", from_cache=False ) + # check if transition started whilst waiting for polled state + if self.is_transitioning: + return if level is not None: self._attr_brightness = level + if self._color_channel: attributes = [ "color_mode", @@ -808,6 +869,11 @@ class Light(BaseLight, ZhaEntity): attributes, from_cache=False, only_cache=False ) + # although rare, a transition might have been started while we were waiting for the polled attributes, + # so abort if we are transitioning, as that state will not be accurate + if self.is_transitioning: + return + if (color_mode := results.get("color_mode")) is not None: if color_mode == Color.ColorMode.Color_temperature: self._attr_color_mode = ColorMode.COLOR_TEMP @@ -853,14 +919,14 @@ class Light(BaseLight, ZhaEntity): async def async_update(self) -> None: """Update to the latest state.""" - if self._transitioning: + if self.is_transitioning: self.debug("skipping async_update while transitioning") return await self.async_get_state() async def _refresh(self, time): """Call async_get_state at an interval.""" - if self._transitioning: + if self.is_transitioning: self.debug("skipping _refresh while transitioning") return await self.async_get_state() @@ -869,12 +935,71 @@ class Light(BaseLight, ZhaEntity): async def _maybe_force_refresh(self, signal): """Force update the state if the signal contains the entity id for this entity.""" if self.entity_id in signal["entity_ids"]: - if self._transitioning: + if self.is_transitioning: self.debug("skipping _maybe_force_refresh while transitioning") return await self.async_get_state() self.async_write_ha_state() + @callback + def _assume_group_state(self, signal, update_params) -> None: + """Handle an assume group state event from a group.""" + if self.entity_id in signal["entity_ids"] and self._attr_available: + self.debug("member assuming group state with: %s", update_params) + + state = update_params["state"] + brightness = update_params.get(light.ATTR_BRIGHTNESS) + color_mode = update_params.get(light.ATTR_COLOR_MODE) + color_temp = update_params.get(light.ATTR_COLOR_TEMP) + xy_color = update_params.get(light.ATTR_XY_COLOR) + hs_color = update_params.get(light.ATTR_HS_COLOR) + effect = update_params.get(light.ATTR_EFFECT) + + supported_modes = self._attr_supported_color_modes + + # unset "off brightness" and "off with transition" if group turned on this light + if state and not self._attr_state: + self._off_with_transition = False + self._off_brightness = None + + # set "off brightness" and "off with transition" if group turned off this light + elif ( + not state # group is turning the light off + and self._attr_state # check the light was not already off (to not override _off_with_transition) + and brightness_supported(supported_modes) + ): + # use individual brightness, instead of possibly averaged brightness from group + self._off_brightness = self._attr_brightness + self._off_with_transition = update_params["off_with_transition"] + + # Note: If individual lights have off_with_transition set, but not the group, + # and the group is then turned on without a level, individual lights might fall back to brightness level 1. + # Since all lights might need different brightness levels to be turned on, we can't use one group call. + # And making individual calls when turning on a ZHA group would cause a lot of traffic. + # In this case, turn_on should either just be called with a level or individual turn_on calls can be used. + + # state is always set (light.turn_on/light.turn_off) + self._attr_state = state + + # before assuming a group state attribute, check if the attribute was actually set in that call + if brightness is not None and brightness_supported(supported_modes): + self._attr_brightness = brightness + if color_mode is not None and color_mode in supported_modes: + self._attr_color_mode = color_mode + if color_temp is not None and ColorMode.COLOR_TEMP in supported_modes: + self._attr_color_temp = color_temp + if xy_color is not None and ColorMode.XY in supported_modes: + self._attr_xy_color = xy_color + if hs_color is not None and ColorMode.HS in supported_modes: + self._attr_hs_color = hs_color + # the effect is always deactivated in async_turn_on if not provided + if effect is None: + self._attr_effect = None + elif self._attr_effect_list and effect in self._attr_effect_list: + self._attr_effect = effect + + self.async_write_ha_state() + @STRICT_MATCH( channel_names=CHANNEL_ON_OFF, @@ -924,6 +1049,20 @@ class LightGroup(BaseLight, ZhaGroupEntity): """Initialize a light group.""" super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) group = self.zha_device.gateway.get_group(self._group_id) + + self._GROUP_SUPPORTS_EXECUTE_IF_OFF = True # pylint: disable=invalid-name + # Check all group members to see if they support execute_if_off. + # If at least one member has a color cluster and doesn't support it, it's not used. + for member in group.members: + for pool in member.device.channels.pools: + for channel in pool.all_channels.values(): + if ( + channel.name == CHANNEL_COLOR + and not channel.execute_if_off_supported + ): + self._GROUP_SUPPORTS_EXECUTE_IF_OFF = False + break + self._DEFAULT_MIN_TRANSITION_TIME = any( # pylint: disable=invalid-name member.device.manufacturer in DEFAULT_MIN_TRANSITION_MANUFACTURERS for member in group.members @@ -951,6 +1090,14 @@ class LightGroup(BaseLight, ZhaGroupEntity): CONF_ALWAYS_PREFER_XY_COLOR_MODE, True, ) + self._zha_config_group_members_assume_state = async_get_zha_config_value( + zha_device.gateway.config_entry, + ZHA_OPTIONS, + CONF_GROUP_MEMBERS_ASSUME_STATE, + True, + ) + if self._zha_config_group_members_assume_state: + self._update_group_from_child_delay = ASSUME_UPDATE_GROUP_FROM_CHILD_DELAY self._zha_config_enhanced_light_transition = False self._attr_color_mode = None @@ -975,8 +1122,13 @@ class LightGroup(BaseLight, ZhaGroupEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" + # "off with transition" and "off brightness" will get overridden when turning on the group, + # but they are needed for setting the assumed member state correctly, so save them here + off_brightness = self._off_brightness if self._off_with_transition else None await super().async_turn_on(**kwargs) - if self._transitioning: + if self._zha_config_group_members_assume_state: + self._send_member_assume_state_event(True, kwargs, off_brightness) + if self.is_transitioning: # when transitioning, state is refreshed at the end return if self._debounced_member_refresh: await self._debounced_member_refresh.async_call() @@ -984,33 +1136,27 @@ class LightGroup(BaseLight, ZhaGroupEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" await super().async_turn_off(**kwargs) - if self._transitioning: + if self._zha_config_group_members_assume_state: + self._send_member_assume_state_event(False, kwargs) + if self.is_transitioning: return if self._debounced_member_refresh: await self._debounced_member_refresh.async_call() - @callback - def async_state_changed_listener(self, event: Event) -> None: - """Handle child updates.""" - if self._transitioning: - self.debug("skipping group entity state update during transition") - return - super().async_state_changed_listener(event) - - async def async_update_ha_state(self, force_refresh: bool = False) -> None: - """Update Home Assistant with current state of entity.""" - if self._transitioning: - self.debug("skipping group entity state update during transition") - return - await super().async_update_ha_state(force_refresh) - async def async_update(self) -> None: """Query all members and determine the light group state.""" + self.debug("updating group state") all_states = [self.hass.states.get(x) for x in self._entity_ids] states: list[State] = list(filter(None, all_states)) on_states = [state for state in states if state.state == STATE_ON] self._attr_state = len(on_states) > 0 + + # reset "off with transition" flag if any member is on + if self._attr_state: + self._off_with_transition = False + self._off_brightness = None + self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states) self._attr_brightness = helpers.reduce_attribute( @@ -1095,3 +1241,41 @@ class LightGroup(BaseLight, ZhaGroupEntity): SIGNAL_LIGHT_GROUP_STATE_CHANGED, {"entity_ids": self._entity_ids}, ) + + def _send_member_assume_state_event( + self, state, service_kwargs, off_brightness=None + ) -> None: + """Send an assume event to all members of the group.""" + update_params = { + "state": state, + "off_with_transition": self._off_with_transition, + } + + # check if the parameters were actually updated in the service call before updating members + if light.ATTR_BRIGHTNESS in service_kwargs: # or off brightness + update_params[light.ATTR_BRIGHTNESS] = self._attr_brightness + elif off_brightness is not None: + # if we turn on the group light with "off brightness", pass that to the members + update_params[light.ATTR_BRIGHTNESS] = off_brightness + + if light.ATTR_COLOR_TEMP in service_kwargs: + update_params[light.ATTR_COLOR_MODE] = self._attr_color_mode + update_params[light.ATTR_COLOR_TEMP] = self._attr_color_temp + + if light.ATTR_XY_COLOR in service_kwargs: + update_params[light.ATTR_COLOR_MODE] = self._attr_color_mode + update_params[light.ATTR_XY_COLOR] = self._attr_xy_color + + if light.ATTR_HS_COLOR in service_kwargs: + update_params[light.ATTR_COLOR_MODE] = self._attr_color_mode + update_params[light.ATTR_HS_COLOR] = self._attr_hs_color + + if light.ATTR_EFFECT in service_kwargs: + update_params[light.ATTR_EFFECT] = self._attr_effect + + async_dispatcher_send( + self.hass, + SIGNAL_LIGHT_GROUP_ASSUME_GROUP_STATE, + {"entity_ids": self._entity_ids}, + update_params, + ) diff --git a/homeassistant/components/zha/logbook.py b/homeassistant/components/zha/logbook.py index 0c8fd6523a8..a82b1f87103 100644 --- a/homeassistant/components/zha/logbook.py +++ b/homeassistant/components/zha/logbook.py @@ -26,7 +26,7 @@ def async_describe_events( @callback def async_describe_zha_event(event: Event) -> dict[str, str]: - """Describe zha logbook event.""" + """Describe ZHA logbook event.""" device: dr.DeviceEntry | None = None device_name: str = "Unknown device" zha_device: ZHADevice | None = None diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 9adddc97720..065f4aaf8a4 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,10 +4,10 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows==0.34.6", + "bellows==0.34.7", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.90", + "zha-quirks==0.0.92", "zigpy-deconz==0.19.2", "zigpy==0.53.0", "zigpy-xbee==0.16.2", diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 41e90899894..63eb5892242 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -454,7 +454,7 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity): class AqaraMotionDetectionInterval( ZHANumberConfigurationEntity, id_suffix="detection_interval" ): - """Representation of a ZHA on off transition time configuration entity.""" + """Representation of a ZHA motion detection interval configuration entity.""" _attr_native_min_value: float = 2 _attr_native_max_value: float = 65535 @@ -577,7 +577,7 @@ class TimerDurationMinutes(ZHANumberConfigurationEntity, id_suffix="timer_durati @CONFIG_DIAGNOSTIC_MATCH(channel_names="ikea_airpurifier") class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time"): - """Representation of a ZHA timer duration configuration entity.""" + """Representation of a ZHA filter lifetime configuration entity.""" _attr_entity_category = EntityCategory.CONFIG _attr_icon: str = ICONS[14] diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index 2a914dee1d7..c68e65fd48f 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -11,7 +11,7 @@ from typing import Any import voluptuous as vol from zigpy.application import ControllerApplication import zigpy.backups -from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH +from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH, CONF_NWK_BACKUP_ENABLED from zigpy.exceptions import NetworkNotFormed from homeassistant import config_entries @@ -126,6 +126,7 @@ class ZhaRadioManager: app_config[CONF_DATABASE] = database_path app_config[CONF_DEVICE] = self.device_settings + app_config[CONF_NWK_BACKUP_ENABLED] = False app_config = self.radio_type.controller.SCHEMA(app_config) app = await self.radio_type.controller.new( @@ -206,11 +207,12 @@ class ZhaRadioManager: # The list of backups will always exist self.backups = app.backups.backups.copy() + self.backups.sort(reverse=True, key=lambda b: b.backup_time) return backup async def async_form_network(self) -> None: - """Form a brand new network.""" + """Form a brand-new network.""" async with self._connect_zigpy_app() as app: await app.form_network() @@ -273,7 +275,7 @@ class ZhaMultiPANMigrationHelper: """Helper class for automatic migration when upgrading the firmware of a radio. This class is currently only intended to be used when changing the firmware on the - radio used in the Home Assistant Sky Connect USB stick and the Home Asssistant Yellow + radio used in the Home Assistant SkyConnect USB stick and the Home Assistant Yellow from Zigbee only firmware to firmware supporting both Zigbee and Thread. """ diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index ff95b66d101..eb952d44610 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -1,6 +1,7 @@ """Sensors on Zigbee Home Automation networks.""" from __future__ import annotations +import enum import functools import numbers from typing import TYPE_CHECKING, Any, TypeVar @@ -20,6 +21,7 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_MILLION, LIGHT_LUX, PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, Platform, UnitOfApparentPower, UnitOfElectricCurrent, @@ -174,7 +176,7 @@ class Sensor(ZhaEntity, SensorEntity): """Handle state update from channel.""" self.async_write_ha_state() - def formatter(self, value: int) -> int | float | None: + def formatter(self, value: int | enum.IntEnum) -> int | float | str | None: """Numeric pass-through formatter.""" if self._decimals > 0: return round( @@ -183,12 +185,6 @@ class Sensor(ZhaEntity, SensorEntity): return round(float(value * self._multiplier) / self._divisor) -@MULTI_MATCH( - channel_names=CHANNEL_ANALOG_INPUT, - manufacturers="LUMI", - models={"lumi.plug", "lumi.plug.maus01", "lumi.plug.mmeu01"}, - stop_on_match_group=CHANNEL_ANALOG_INPUT, -) @MULTI_MATCH( channel_names=CHANNEL_ANALOG_INPUT, manufacturers="Digi", @@ -495,7 +491,7 @@ class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered") @MULTI_MATCH( channel_names=CHANNEL_SMARTENERGY_METERING, - models={"TS011F"}, + models={"TS011F", "ZLinky_TIC"}, stop_on_match_group=CHANNEL_SMARTENERGY_METERING, ) class PolledSmartEnergySummation(SmartEnergySummation): @@ -510,6 +506,84 @@ class PolledSmartEnergySummation(SmartEnergySummation): await self._channel.async_force_update() +@MULTI_MATCH( + channel_names=CHANNEL_SMARTENERGY_METERING, + models={"ZLinky_TIC"}, +) +class Tier1SmartEnergySummation( + PolledSmartEnergySummation, id_suffix="tier1_summation_delivered" +): + """Tier 1 Smart Energy Metering summation sensor.""" + + SENSOR_ATTR: int | str = "current_tier1_summ_delivered" + _attr_name: str = "Tier 1 summation delivered" + + +@MULTI_MATCH( + channel_names=CHANNEL_SMARTENERGY_METERING, + models={"ZLinky_TIC"}, +) +class Tier2SmartEnergySummation( + PolledSmartEnergySummation, id_suffix="tier2_summation_delivered" +): + """Tier 2 Smart Energy Metering summation sensor.""" + + SENSOR_ATTR: int | str = "current_tier2_summ_delivered" + _attr_name: str = "Tier 2 summation delivered" + + +@MULTI_MATCH( + channel_names=CHANNEL_SMARTENERGY_METERING, + models={"ZLinky_TIC"}, +) +class Tier3SmartEnergySummation( + PolledSmartEnergySummation, id_suffix="tier3_summation_delivered" +): + """Tier 3 Smart Energy Metering summation sensor.""" + + SENSOR_ATTR: int | str = "current_tier3_summ_delivered" + _attr_name: str = "Tier 3 summation delivered" + + +@MULTI_MATCH( + channel_names=CHANNEL_SMARTENERGY_METERING, + models={"ZLinky_TIC"}, +) +class Tier4SmartEnergySummation( + PolledSmartEnergySummation, id_suffix="tier4_summation_delivered" +): + """Tier 4 Smart Energy Metering summation sensor.""" + + SENSOR_ATTR: int | str = "current_tier4_summ_delivered" + _attr_name: str = "Tier 4 summation delivered" + + +@MULTI_MATCH( + channel_names=CHANNEL_SMARTENERGY_METERING, + models={"ZLinky_TIC"}, +) +class Tier5SmartEnergySummation( + PolledSmartEnergySummation, id_suffix="tier5_summation_delivered" +): + """Tier 5 Smart Energy Metering summation sensor.""" + + SENSOR_ATTR: int | str = "current_tier5_summ_delivered" + _attr_name: str = "Tier 5 summation delivered" + + +@MULTI_MATCH( + channel_names=CHANNEL_SMARTENERGY_METERING, + models={"ZLinky_TIC"}, +) +class Tier6SmartEnergySummation( + PolledSmartEnergySummation, id_suffix="tier6_summation_delivered" +): + """Tier 6 Smart Energy Metering summation sensor.""" + + SENSOR_ATTR: int | str = "current_tier6_summ_delivered" + _attr_name: str = "Tier 6 summation delivered" + + @MULTI_MATCH(channel_names=CHANNEL_PRESSURE) class Pressure(Sensor): """Pressure sensor.""" @@ -755,6 +829,8 @@ class RSSISensor(Sensor, id_suffix="rssi"): """RSSI sensor for a device.""" _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT + _attr_device_class: SensorDeviceClass | None = SensorDeviceClass.SIGNAL_STRENGTH + _attr_native_unit_of_measurement: str | None = SIGNAL_STRENGTH_DECIBELS_MILLIWATT _attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_registry_enabled_default = False _attr_should_poll = True # BaseZhaEntity defaults to False @@ -789,6 +865,8 @@ class LQISensor(RSSISensor, id_suffix="lqi"): """LQI sensor for a device.""" _attr_name: str = "LQI" + _attr_device_class = None + _attr_native_unit_of_measurement = None @MULTI_MATCH( @@ -870,7 +948,7 @@ class AqaraPetFeederPortionsDispensed(Sensor, id_suffix="portions_dispensed"): @MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"}) class AqaraPetFeederWeightDispensed(Sensor, id_suffix="weight_dispensed"): - """Sensor that displays the weight weight dispensed by the pet feeder.""" + """Sensor that displays the weight dispensed by the pet feeder.""" SENSOR_ATTR = "weight_dispensed" _attr_name: str = "Weight dispensed today" diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 057c0adb007..132f6ed9d95 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -31,7 +31,8 @@ "title": "Network Formation", "description": "Choose the network settings for your radio.", "menu_options": { - "form_new_network": "Erase network settings and form a new network", + "form_new_network": "Erase network settings and create a new network", + "form_initial_network": "Create a network", "reuse_settings": "Keep radio network settings", "choose_automatic_backup": "Restore an automatic backup", "upload_manual_backup": "Upload a manual backup" @@ -86,11 +87,11 @@ }, "intent_migrate": { "title": "Migrate to a new radio", - "description": "Your old radio will be factory reset. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\nDo you wish to continue?" + "description": "Before plugging in your new radio, your old radio needs to be reset. An automatic backup will be performed. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\n*Note: if you are migrating from a **ConBee/RaspBee**, make sure it is running firmware `0x26720700` or newer! Otherwise, some devices may not be controllable after migrating until they are power cycled.*\n\nDo you wish to continue?" }, "instruct_unplug": { "title": "Unplug your old radio", - "description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it." + "description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it.\n\nYou can now plug in your new radio." }, "choose_serial_port": { "title": "[%key:component::zha::config::step::choose_serial_port::title%]", @@ -120,6 +121,7 @@ "description": "[%key:component::zha::config::step::choose_formation_strategy::description%]", "menu_options": { "form_new_network": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::form_new_network%]", + "form_initial_network": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::form_initial_network%]", "reuse_settings": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::reuse_settings%]", "choose_automatic_backup": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::choose_automatic_backup%]", "upload_manual_backup": "[%key:component::zha::config::step::choose_formation_strategy::menu_options::upload_manual_backup%]" @@ -163,6 +165,7 @@ "enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state", "light_transitioning_flag": "Enable enhanced brightness slider during light transition", "always_prefer_xy_color_mode": "Always prefer XY color mode", + "group_members_assume_state": "Group members assume state of group", "enable_identify_on_join": "Enable identify effect when devices join the network", "default_light_transition": "Default light transition time (seconds)", "consider_unavailable_mains": "Consider mains powered devices unavailable after (seconds)", @@ -183,22 +186,22 @@ "issue_individual_led_effect": "Issue effect for individual LED" }, "trigger_type": { - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", - "remote_button_long_press": "\"{subtype}\" button continuously pressed", - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_double_press": "\"{subtype}\" button double clicked", - "remote_button_triple_press": "\"{subtype}\" button triple clicked", - "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", - "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", - "remote_button_alt_short_press": "\"{subtype}\" button pressed (Alternate mode)", - "remote_button_alt_short_release": "\"{subtype}\" button released (Alternate mode)", - "remote_button_alt_long_press": "\"{subtype}\" button continuously pressed (Alternate mode)", - "remote_button_alt_long_release": "\"{subtype}\" button released after long press (Alternate mode)", - "remote_button_alt_double_press": "\"{subtype}\" button double clicked (Alternate mode)", - "remote_button_alt_triple_press": "\"{subtype}\" button triple clicked (Alternate mode)", - "remote_button_alt_quadruple_press": "\"{subtype}\" button quadruple clicked (Alternate mode)", - "remote_button_alt_quintuple_press": "\"{subtype}\" button quintuple clicked (Alternate mode)", + "remote_button_short_press": "\"{subtype}\" pressed", + "remote_button_short_release": "\"{subtype}\" released", + "remote_button_long_press": "\"{subtype}\" continuously pressed", + "remote_button_long_release": "\"{subtype}\" released after long press", + "remote_button_double_press": "\"{subtype}\" double clicked", + "remote_button_triple_press": "\"{subtype}\" triple clicked", + "remote_button_quadruple_press": "\"{subtype}\" quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" quintuple clicked", + "remote_button_alt_short_press": "\"{subtype}\" pressed (Alternate mode)", + "remote_button_alt_short_release": "\"{subtype}\" released (Alternate mode)", + "remote_button_alt_long_press": "\"{subtype}\" continuously pressed (Alternate mode)", + "remote_button_alt_long_release": "\"{subtype}\" released after long press (Alternate mode)", + "remote_button_alt_double_press": "\"{subtype}\" double clicked (Alternate mode)", + "remote_button_alt_triple_press": "\"{subtype}\" triple clicked (Alternate mode)", + "remote_button_alt_quadruple_press": "\"{subtype}\" quadruple clicked (Alternate mode)", + "remote_button_alt_quintuple_press": "\"{subtype}\" quintuple clicked (Alternate mode)", "device_rotated": "Device rotated \"{subtype}\"", "device_shaken": "Device shaken", "device_slid": "Device slid \"{subtype}\"", @@ -218,6 +221,7 @@ "open": "Open", "close": "Close", "both_buttons": "Both buttons", + "button": "Button", "button_1": "First button", "button_2": "Second button", "button_3": "Third button", diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index c9469747a3f..f7a16c83b60 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -160,7 +160,7 @@ class SwitchGroup(ZhaGroupEntity, SwitchEntity): self.async_write_ha_state() async def async_update(self) -> None: - """Query all members and determine the light group state.""" + """Query all members and determine the switch group state.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] states: list[State] = list(filter(None, all_states)) on_states = [state for state in states if state.state == STATE_ON] @@ -372,14 +372,26 @@ class InovelliSmartBulbMode(ZHASwitchConfigurationEntity, id_suffix="smart_bulb_ channel_names=CHANNEL_INOVELLI, ) class InovelliDoubleTapForFullBrightness( - ZHASwitchConfigurationEntity, id_suffix="double_tap_up_for_full_brightness" + ZHASwitchConfigurationEntity, id_suffix="double_tap_up_for_max_brightness" ): """Inovelli double tap for full brightness control.""" - _zcl_attribute: str = "double_tap_up_for_full_brightness" + _zcl_attribute: str = "double_tap_up_for_max_brightness" _attr_name: str = "Double tap full brightness" +@CONFIG_DIAGNOSTIC_MATCH( + channel_names=CHANNEL_INOVELLI, +) +class InovelliDoubleTapForMinBrightness( + ZHASwitchConfigurationEntity, id_suffix="double_tap_down_for_min_brightness" +): + """Inovelli double tap down for minimum brightness control.""" + + _zcl_attribute: str = "double_tap_down_for_min_brightness" + _attr_name: str = "Double tap minimum brightness" + + @CONFIG_DIAGNOSTIC_MATCH( channel_names=CHANNEL_INOVELLI, ) diff --git a/homeassistant/components/zha/translations/bg.json b/homeassistant/components/zha/translations/bg.json index 3fa4c4fbdf5..409bd01e893 100644 --- a/homeassistant/components/zha/translations/bg.json +++ b/homeassistant/components/zha/translations/bg.json @@ -48,6 +48,9 @@ }, "config_panel": { "zha_options": { + "default_light_transition": "\u0412\u0440\u0435\u043c\u0435 \u0437\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0435\u043d \u043f\u0440\u0435\u0445\u043e\u0434 \u043f\u043e \u043f\u043e\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043d\u0435 (\u0441\u0435\u043a\u0443\u043d\u0434\u0438)", + "enable_identify_on_join": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0435\u0444\u0435\u043a\u0442\u0430 \u0437\u0430 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u043d\u0435, \u043a\u043e\u0433\u0430\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441\u0435 \u043f\u0440\u0438\u0441\u044a\u0435\u0434\u0438\u043d\u044f\u0432\u0430\u0442 \u043a\u044a\u043c \u043c\u0440\u0435\u0436\u0430\u0442\u0430", + "enhanced_light_transition": "\u0410\u043a\u0442\u0438\u0432\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043f\u043e\u0434\u043e\u0431\u0440\u0435\u043d\u0438\u044f \u043f\u0440\u0435\u0445\u043e\u0434 \u043d\u0430 \u0446\u0432\u044f\u0442/\u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043d\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430\u0442\u0430 \u043e\u0442 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0441\u044a\u0441\u0442\u043e\u044f\u043d\u0438\u0435", "title": "\u0413\u043b\u043e\u0431\u0430\u043b\u043d\u0438 \u043e\u043f\u0446\u0438\u0438" } }, @@ -58,6 +61,7 @@ }, "trigger_subtype": { "both_buttons": "\u0418 \u0434\u0432\u0430\u0442\u0430 \u0431\u0443\u0442\u043e\u043d\u0430", + "button": "\u0411\u0443\u0442\u043e\u043d", "button_1": "\u041f\u044a\u0440\u0432\u0438 \u0431\u0443\u0442\u043e\u043d", "button_2": "\u0412\u0442\u043e\u0440\u0438 \u0431\u0443\u0442\u043e\u043d", "button_3": "\u0422\u0440\u0435\u0442\u0438 \u0431\u0443\u0442\u043e\u043d", diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index 22ae3910518..2e3837bdcf6 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -87,6 +87,7 @@ "default_light_transition": "Temps de transici\u00f3 predeterminat (segons)", "enable_identify_on_join": "Activa l'efecte d'identificaci\u00f3 quan els dispositius s'uneixin a la xarxa", "enhanced_light_transition": "Activa la transici\u00f3 millorada de color/temperatura de llum des de l'estat apagat", + "group_members_assume_state": "Els membres del grup assumeixen l'estat del grup", "light_transitioning_flag": "Activa el control lliscant de brillantor millorat durant la transici\u00f3", "title": "Opcions globals" } @@ -100,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "Ambd\u00f3s botons", + "button": "Bot\u00f3", "button_1": "Primer bot\u00f3", "button_2": "Segon bot\u00f3", "button_3": "Tercer bot\u00f3", @@ -131,22 +133,22 @@ "device_shaken": "Dispositiu sacsejat", "device_slid": "Dispositiu lliscat a \"{subtype}\"", "device_tilted": "Dispositiu inclinat", - "remote_button_alt_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades (mode alternatiu)", - "remote_button_alt_long_press": "Bot\u00f3 \"{subtype}\" premut cont\u00ednuament (mode alternatiu)", - "remote_button_alt_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut (mode alternatiu", - "remote_button_alt_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades (mode alternatiu)", - "remote_button_alt_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades (mode alternatiu)", - "remote_button_alt_short_press": "Bot\u00f3 \"{subtype}\" premut (mode alternatiu)", - "remote_button_alt_short_release": "Bot\u00f3 \"{subtype}\" alliberat (mode alternatiu)", - "remote_button_alt_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades (mode alternatiu)", - "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades", - "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut cont\u00ednuament", - "remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", - "remote_button_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades", - "remote_button_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades", - "remote_button_short_press": "Bot\u00f3 \"{subtype}\" premut", - "remote_button_short_release": "Bot\u00f3 \"{subtype}\" alliberat", - "remote_button_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades" + "remote_button_alt_double_press": "\"{subtype}\" clicat dues vegades (mode alternatiu)", + "remote_button_alt_long_press": "\"{subtype}\" premut cont\u00ednuament (mode alternatiu)", + "remote_button_alt_long_release": "\"{subtype}\" alliberat despr\u00e9s d'una estona premut (mode alternatiu)", + "remote_button_alt_quadruple_press": "\"{subtype}\" clicat quatre vegades (mode alternatiu)", + "remote_button_alt_quintuple_press": "\"{subtype}\" clicat cinc vegades (mode alternatiu)", + "remote_button_alt_short_press": "\"{subtype}\" premut (mode alternatiu)", + "remote_button_alt_short_release": "\"{subtype}\" alliberat (mode alternatiu)", + "remote_button_alt_triple_press": "\"{subtype}\" clicat tres vegades (mode alternatiu)", + "remote_button_double_press": "\"{subtype}\" clicat dues vegades", + "remote_button_long_press": "\"{subtype}\" premut cont\u00ednuament", + "remote_button_long_release": "\"{subtype}\" alliberat despr\u00e9s d'una estona premut", + "remote_button_quadruple_press": "\"{subtype}\" clicat quatre vegades", + "remote_button_quintuple_press": "\"{subtype}\" clicat cinc vegades", + "remote_button_short_press": "\"{subtype}\" premut", + "remote_button_short_release": "\"{subtype}\" alliberat", + "remote_button_triple_press": "\"{subtype}\" clicat tres vegades" } }, "options": { diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 40112b0c36c..58dd8e4343e 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -87,6 +87,7 @@ "default_light_transition": "Standardlicht\u00fcbergangszeit (Sekunden)", "enable_identify_on_join": "Aktiviere den Identifikationseffekt, wenn Ger\u00e4te dem Netzwerk beitreten", "enhanced_light_transition": "Aktiviere einen verbesserten Lichtfarben-/Temperatur\u00fcbergang aus einem ausgeschalteten Zustand", + "group_members_assume_state": "Gruppenmitglieder nehmen den Status der Gruppe optimistisch an", "light_transitioning_flag": "Verbesserten Helligkeitsregler w\u00e4hrend des Licht\u00fcbergangs aktivieren", "title": "Globale Optionen" } @@ -100,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "Beide Tasten", + "button": "Taste", "button_1": "Erste Taste", "button_2": "Zweite Taste", "button_3": "Dritte Taste", @@ -131,22 +133,22 @@ "device_shaken": "Ger\u00e4t ersch\u00fcttert", "device_slid": "Ger\u00e4t gerutscht \"{subtype}\"", "device_tilted": "Ger\u00e4t gekippt", - "remote_button_alt_double_press": "\"{subtype}\" Taste doppelt gedr\u00fcckt (Alternativer Modus)", - "remote_button_alt_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt (Alternativer Modus)", - "remote_button_alt_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen (Alternativer Modus)", - "remote_button_alt_quadruple_press": "\"{subtype}\" Taste vierfach gedr\u00fcckt (Alternativer Modus)", - "remote_button_alt_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach gedr\u00fcckt (Alternativer Modus)", - "remote_button_alt_short_press": "\"{subtype}\" Taste gedr\u00fcckt (Alternativer Modus)", - "remote_button_alt_short_release": "\"{subtype}\" Taste losgelassen (Alternativer Modus)", - "remote_button_alt_triple_press": "\"{subtype}\" Taste dreimal gedr\u00fcckt (Alternativer Modus)", - "remote_button_double_press": "\"{subtype}\" Taste doppelt angedr\u00fcckt", - "remote_button_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt", - "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen", - "remote_button_quadruple_press": "\"{subtype}\" Taste vierfach gedr\u00fcckt", - "remote_button_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach gedr\u00fcckt", - "remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt", - "remote_button_short_release": "\"{subtype}\" Taste losgelassen", - "remote_button_triple_press": "\"{subtype}\" Taste dreimal gedr\u00fcckt" + "remote_button_alt_double_press": "\"{subtype}\" doppelt gedr\u00fcckt (Alternativer Modus)", + "remote_button_alt_long_press": "\"{subtype}\" kontinuierlich gedr\u00fcckt (Alternativer Modus)", + "remote_button_alt_long_release": "\"{subtype}\" nach langem Dr\u00fccken losgelassen (Alternativer Modus)", + "remote_button_alt_quadruple_press": "\"{subtype}\" vierfach gedr\u00fcckt (Alternativer Modus)", + "remote_button_alt_quintuple_press": "\"{subtype}\" f\u00fcnffach gedr\u00fcckt (Alternativer Modus)", + "remote_button_alt_short_press": "\"{subtype}\" gedr\u00fcckt (Alternativer Modus)", + "remote_button_alt_short_release": "\"{subtype}\" losgelassen (Alternativer Modus)", + "remote_button_alt_triple_press": "\"{subtype}\" dreifach angeklickt (Alternativer Modus)", + "remote_button_double_press": "\"{subtype}\" doppelt angedr\u00fcckt", + "remote_button_long_press": "\"{subtype}\" kontinuierlich gedr\u00fcckt", + "remote_button_long_release": "\"{subtype}\" nach langem Dr\u00fccken losgelassen", + "remote_button_quadruple_press": "\"{subtype}\" vierfach gedr\u00fcckt", + "remote_button_quintuple_press": "\"{subtype}\" f\u00fcnffach gedr\u00fcckt", + "remote_button_short_press": "\"{subtype}\" gedr\u00fcckt", + "remote_button_short_release": "\"{subtype}\" losgelassen", + "remote_button_triple_press": "\"{subtype}\" dreimal gedr\u00fcckt" } }, "options": { diff --git a/homeassistant/components/zha/translations/el.json b/homeassistant/components/zha/translations/el.json index 3a85b35b27b..2b683e62366 100644 --- a/homeassistant/components/zha/translations/el.json +++ b/homeassistant/components/zha/translations/el.json @@ -36,7 +36,7 @@ "title": "\u0395\u03c0\u03b9\u03bb\u03ad\u03be\u03c4\u03b5 \u03bc\u03b9\u03b1 \u03c3\u03b5\u03b9\u03c1\u03b9\u03b1\u03ba\u03ae \u03b8\u03cd\u03c1\u03b1" }, "confirm": { - "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name} ;" + "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" }, "confirm_hardware": { "description": "\u0398\u03ad\u03bb\u03b5\u03c4\u03b5 \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c4\u03b5 \u03c4\u03bf {name};" @@ -87,6 +87,7 @@ "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", "enhanced_light_transition": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03b7 \u03b2\u03b5\u03bb\u03c4\u03b9\u03c9\u03bc\u03ad\u03bd\u03b7 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7 \u03c7\u03c1\u03ce\u03bc\u03b1\u03c4\u03bf\u03c2 \u03c6\u03c9\u03c4\u03cc\u03c2/\u03b8\u03b5\u03c1\u03bc\u03bf\u03ba\u03c1\u03b1\u03c3\u03af\u03b1\u03c2 \u03b1\u03c0\u03cc \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03b5\u03ba\u03c4\u03cc\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2", + "group_members_assume_state": "\u03a4\u03b1 \u03bc\u03ad\u03bb\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2 \u03b1\u03bd\u03b1\u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03bf\u03c5\u03bd \u03c4\u03b7\u03bd \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bf\u03bc\u03ac\u03b4\u03b1\u03c2", "light_transitioning_flag": "\u0395\u03bd\u03b5\u03c1\u03b3\u03bf\u03c0\u03bf\u03af\u03b7\u03c3\u03b7 \u03b2\u03b5\u03bb\u03c4\u03b9\u03c9\u03bc\u03ad\u03bd\u03bf\u03c5 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd \u03c6\u03c9\u03c4\u03b5\u03b9\u03bd\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03bc\u03b5\u03c4\u03ac\u03b2\u03b1\u03c3\u03b7 \u03c6\u03c9\u03c4\u03cc\u03c2", "title": "\u039a\u03b1\u03b8\u03bf\u03bb\u03b9\u03ba\u03ad\u03c2 \u03b5\u03c0\u03b9\u03bb\u03bf\u03b3\u03ad\u03c2" } @@ -100,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "\u039a\u03b1\u03b9 \u03c4\u03b1 \u03b4\u03cd\u03bf \u03ba\u03bf\u03c5\u03bc\u03c0\u03b9\u03ac", + "button": "\u039a\u03bf\u03c5\u03bc\u03c0\u03af", "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", diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index 049cdce4c4c..59e8004ad39 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -22,7 +22,8 @@ "description": "Choose the network settings for your radio.", "menu_options": { "choose_automatic_backup": "Restore an automatic backup", - "form_new_network": "Erase network settings and form a new network", + "form_initial_network": "Create a network", + "form_new_network": "Erase network settings and create a new network", "reuse_settings": "Keep radio network settings", "upload_manual_backup": "Upload a manual backup" }, @@ -87,6 +88,7 @@ "default_light_transition": "Default light transition time (seconds)", "enable_identify_on_join": "Enable identify effect when devices join the network", "enhanced_light_transition": "Enable enhanced light color/temperature transition from an off-state", + "group_members_assume_state": "Group members assume state of group", "light_transitioning_flag": "Enable enhanced brightness slider during light transition", "title": "Global Options" } @@ -100,6 +102,7 @@ }, "trigger_subtype": { "both_buttons": "Both buttons", + "button": "Button", "button_1": "First button", "button_2": "Second button", "button_3": "Third button", @@ -131,22 +134,22 @@ "device_shaken": "Device shaken", "device_slid": "Device slid \"{subtype}\"", "device_tilted": "Device tilted", - "remote_button_alt_double_press": "\"{subtype}\" button double clicked (Alternate mode)", - "remote_button_alt_long_press": "\"{subtype}\" button continuously pressed (Alternate mode)", - "remote_button_alt_long_release": "\"{subtype}\" button released after long press (Alternate mode)", - "remote_button_alt_quadruple_press": "\"{subtype}\" button quadruple clicked (Alternate mode)", - "remote_button_alt_quintuple_press": "\"{subtype}\" button quintuple clicked (Alternate mode)", - "remote_button_alt_short_press": "\"{subtype}\" button pressed (Alternate mode)", - "remote_button_alt_short_release": "\"{subtype}\" button released (Alternate mode)", - "remote_button_alt_triple_press": "\"{subtype}\" button triple clicked (Alternate mode)", - "remote_button_double_press": "\"{subtype}\" button double clicked", - "remote_button_long_press": "\"{subtype}\" button continuously pressed", - "remote_button_long_release": "\"{subtype}\" button released after long press", - "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", - "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", - "remote_button_short_press": "\"{subtype}\" button pressed", - "remote_button_short_release": "\"{subtype}\" button released", - "remote_button_triple_press": "\"{subtype}\" button triple clicked" + "remote_button_alt_double_press": "\"{subtype}\" double clicked (Alternate mode)", + "remote_button_alt_long_press": "\"{subtype}\" continuously pressed (Alternate mode)", + "remote_button_alt_long_release": "\"{subtype}\" released after long press (Alternate mode)", + "remote_button_alt_quadruple_press": "\"{subtype}\" quadruple clicked (Alternate mode)", + "remote_button_alt_quintuple_press": "\"{subtype}\" quintuple clicked (Alternate mode)", + "remote_button_alt_short_press": "\"{subtype}\" pressed (Alternate mode)", + "remote_button_alt_short_release": "\"{subtype}\" released (Alternate mode)", + "remote_button_alt_triple_press": "\"{subtype}\" triple clicked (Alternate mode)", + "remote_button_double_press": "\"{subtype}\" double clicked", + "remote_button_long_press": "\"{subtype}\" continuously pressed", + "remote_button_long_release": "\"{subtype}\" released after long press", + "remote_button_quadruple_press": "\"{subtype}\" quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" quintuple clicked", + "remote_button_short_press": "\"{subtype}\" pressed", + "remote_button_short_release": "\"{subtype}\" released", + "remote_button_triple_press": "\"{subtype}\" triple clicked" } }, "options": { @@ -172,7 +175,8 @@ "description": "Choose the network settings for your radio.", "menu_options": { "choose_automatic_backup": "Restore an automatic backup", - "form_new_network": "Erase network settings and form a new network", + "form_initial_network": "Create a network", + "form_new_network": "Erase network settings and create a new network", "reuse_settings": "Keep radio network settings", "upload_manual_backup": "Upload a manual backup" }, @@ -190,11 +194,11 @@ "title": "Reconfigure ZHA" }, "instruct_unplug": { - "description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it.", + "description": "Your old radio has been reset. If the hardware is no longer needed, you can now unplug it.\n\nYou can now plug in your new radio.", "title": "Unplug your old radio" }, "intent_migrate": { - "description": "Your old radio will be factory reset. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\nDo you wish to continue?", + "description": "Before plugging in your new radio, your old radio needs to be reset. An automatic backup will be performed. If you are using a combined Z-Wave and Zigbee adapter like the HUSBZB-1, this will only reset the Zigbee portion.\n\n*Note: if you are migrating from a **ConBee/RaspBee**, make sure it is running firmware `0x26720700` or newer! Otherwise, some devices may not be controllable after migrating until they are power cycled.*\n\nDo you wish to continue?", "title": "Migrate to a new radio" }, "manual_pick_radio_type": { diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 3b5315f2fbd..fbfada98033 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -87,6 +87,7 @@ "default_light_transition": "Tiempo de transici\u00f3n de luz predeterminado (segundos)", "enable_identify_on_join": "Habilitar el efecto de identificaci\u00f3n cuando los dispositivos se unan a la red", "enhanced_light_transition": "Habilitar la transici\u00f3n mejorada de color de luz/temperatura desde un estado apagado", + "group_members_assume_state": "Los miembros del grupo asumen el estado del grupo", "light_transitioning_flag": "Habilitar el control deslizante de brillo mejorado durante la transici\u00f3n de luz", "title": "Opciones globales" } @@ -100,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "Ambos botones", + "button": "Bot\u00f3n", "button_1": "Primer bot\u00f3n", "button_2": "Segundo bot\u00f3n", "button_3": "Tercer bot\u00f3n", @@ -131,22 +133,22 @@ "device_shaken": "Dispositivo agitado", "device_slid": "Dispositivo deslizado \"{subtype}\"", "device_tilted": "Dispositivo inclinado", - "remote_button_alt_double_press": "Bot\u00f3n \"{subtype}\" doble pulsaci\u00f3n (modo Alternativo)", - "remote_button_alt_long_press": "Bot\u00f3n \"{subtype}\" pulsado continuamente (modo Alternativo)", - "remote_button_alt_long_release": "Bot\u00f3n \"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga (modo Alternativo)", - "remote_button_alt_quadruple_press": "Bot\u00f3n \"{subtype}\" cu\u00e1druple pulsaci\u00f3n (modo Alternativo)", - "remote_button_alt_quintuple_press": "Bot\u00f3n \"{subtype}\" qu\u00edntuple pulsaci\u00f3n (modo Alternativo)", - "remote_button_alt_short_press": "Bot\u00f3n \"{subtype}\" pulsado (modo Alternativo)", - "remote_button_alt_short_release": "Bot\u00f3n \"{subtype}\" soltado (modo Alternativo)", - "remote_button_alt_triple_press": "Bot\u00f3n \"{subtype}\" triple pulsaci\u00f3n (modo Alternativo)", - "remote_button_double_press": "Bot\u00f3n \"{subtype}\" pulsado dos veces", - "remote_button_long_press": "Bot\u00f3n \"{subtype}\" pulsado continuamente", - "remote_button_long_release": "Bot\u00f3n \"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", - "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces", - "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces", - "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", - "remote_button_short_release": "Bot\u00f3n \"{subtype}\" soltado", - "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado tres veces" + "remote_button_alt_double_press": "\"{subtype}\" pulsado dos veces (modo alternativo)", + "remote_button_alt_long_press": "\"{subtype}\" pulsado continuamente (modo alternativo)", + "remote_button_alt_long_release": "\"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga (modo alternativo)", + "remote_button_alt_quadruple_press": "\"{subtype}\" cu\u00e1druple pulsaci\u00f3n (modo alternativo)", + "remote_button_alt_quintuple_press": "\"{subtype}\" qu\u00edntuple pulsaci\u00f3n (modo alternativo)", + "remote_button_alt_short_press": "\"{subtype}\" pulsado (modo alternativo)", + "remote_button_alt_short_release": "\"{subtype}\" soltado (modo alternativo)", + "remote_button_alt_triple_press": "\"{subtype}\" pulsado tres veces (modo alternativo)", + "remote_button_double_press": "\"{subtype}\" pulsado dos veces", + "remote_button_long_press": "\"{subtype}\" pulsado continuamente", + "remote_button_long_release": "\"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", + "remote_button_quadruple_press": "\"{subtype}\" pulsado cuatro veces", + "remote_button_quintuple_press": "\"{subtype}\" pulsado cinco veces", + "remote_button_short_press": "\"{subtype}\" pulsado", + "remote_button_short_release": "\"{subtype}\" soltado", + "remote_button_triple_press": "\"{subtype}\" pulsado tres veces" } }, "options": { diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index d0e8a062120..1759cf5f1e6 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -87,6 +87,7 @@ "default_light_transition": "Heleduse vaike\u00fclemineku aeg (sekundites)", "enable_identify_on_join": "Luba tuvastamine kui seadmed liituvad v\u00f5rguga", "enhanced_light_transition": "Luba t\u00e4iustatud valguse v\u00e4rvi/temperatuuri \u00fcleminek v\u00e4ljal\u00fclitatud olekust", + "group_members_assume_state": "Grupi liikmed v\u00f5tavad grupi oleku", "light_transitioning_flag": "Luba t\u00e4iustatud heleduse liugur valguse \u00fclemineku ajal", "title": "\u00dcldised valikud" } @@ -100,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "M\u00f5lemad nupud", + "button": "Nupp", "button_1": "Esimene nupp", "button_2": "Teine nupp", "button_3": "Kolmas nupp", @@ -135,12 +137,12 @@ "remote_button_alt_long_press": "\"{subtype}\" nuppu vajutati pikalt (alternatiivre\u017eiim)", "remote_button_alt_long_release": "\"{subtype}\" nupp vabastati peale pikka vajutust (alternatiivre\u017eiim)", "remote_button_alt_quadruple_press": "\"{subtype}\" on neljakordselt kl\u00f5psatud (alternatiivre\u017eiim)", - "remote_button_alt_quintuple_press": "{subtype} on neljakordselt kl\u00f5psatud (alternatiivre\u017eiim)", - "remote_button_alt_short_press": "{subtype} nuppu vajutati (alternatiivre\u017eiim)", - "remote_button_alt_short_release": "{subtype} nupp vabastati (alternatiivre\u017eiim)", - "remote_button_alt_triple_press": "{subtype} on kolmekordselt kl\u00f5psatud (alternatiivre\u017eiim)", - "remote_button_double_press": "{subtype} on topeltkl\u00f5psatud", - "remote_button_long_press": "{subtype} on pikalt alla vajutatud", + "remote_button_alt_quintuple_press": "\"{subtype}\" on neljakordselt kl\u00f5psatud (alternatiivre\u017eiim)", + "remote_button_alt_short_press": "\"{subtype}\" nuppu vajutati (alternatiivre\u017eiim)", + "remote_button_alt_short_release": "\"{subtype}\" nupp vabastati (alternatiivre\u017eiim)", + "remote_button_alt_triple_press": "\"{subtype}\" on kolmekordselt kl\u00f5psatud (alternatiivre\u017eiim)", + "remote_button_double_press": "\"{subtype}\" on topeltkl\u00f5psatud", + "remote_button_long_press": "\"{subtype}\" on pikalt alla vajutatud", "remote_button_long_release": "\"{subtype}\" nupp vabastatati p\u00e4rast pikka vajutust", "remote_button_quadruple_press": "\"{subtype}\" nuppu on neljakordselt kl\u00f5psatud", "remote_button_quintuple_press": "\"{subtype}\" nuppu on viiekordselt kl\u00f5psatud", diff --git a/homeassistant/components/zha/translations/hu.json b/homeassistant/components/zha/translations/hu.json index 8b596df47d7..60ca6a50db2 100644 --- a/homeassistant/components/zha/translations/hu.json +++ b/homeassistant/components/zha/translations/hu.json @@ -87,6 +87,7 @@ "default_light_transition": "Alap\u00e9rtelmezett f\u00e9ny-\u00e1tmeneti id\u0151 (m\u00e1sodpercben)", "enable_identify_on_join": "Azonos\u00edt\u00f3 hat\u00e1s, amikor az eszk\u00f6z\u00f6k csatlakoznak a h\u00e1l\u00f3zathoz", "enhanced_light_transition": "F\u00e9ny sz\u00edn/sz\u00ednh\u0151m\u00e9rs\u00e9klet \u00e1tmenete kikapcsolt \u00e1llapotb\u00f3l", + "group_members_assume_state": "A csoport tagjai \u00e1tveszik a csoport \u00e1llapot\u00e1t", "light_transitioning_flag": "Fokozott f\u00e9nyer\u0151-szab\u00e1lyoz\u00f3 enged\u00e9lyez\u00e9se f\u00e9nyv\u00e1lt\u00e1skor", "title": "Glob\u00e1lis be\u00e1ll\u00edt\u00e1sok" } @@ -100,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "Mindk\u00e9t gomb", + "button": "Nyom\u00f3gomb", "button_1": "Els\u0151 gomb", "button_2": "M\u00e1sodik gomb", "button_3": "Harmadik gomb", @@ -131,17 +133,17 @@ "device_shaken": "A k\u00e9sz\u00fcl\u00e9k megr\u00e1zk\u00f3dott", "device_slid": "Eszk\u00f6z cs\u00fasztatott \"{subtype}\"", "device_tilted": "K\u00e9sz\u00fcl\u00e9k megd\u00f6ntve", - "remote_button_alt_double_press": "A \u201e{subtype}\u201d gombra dupl\u00e1n kattintva (Alternat\u00edv m\u00f3d)", + "remote_button_alt_double_press": "\"{subtype}\" gombra dupl\u00e1n kattintva (Alternat\u00edv m\u00f3d)", "remote_button_alt_long_press": "\"{subtype}\" gomb folyamatosan nyomva (alternat\u00edv m\u00f3d)", - "remote_button_alt_long_release": "A \u201e{subtype}\u201d gomb elenged\u00e9se hossz\u00fa megnyom\u00e1st k\u00f6vet\u0151en (alternat\u00edv m\u00f3d)", - "remote_button_alt_quadruple_press": "A \u201e{subtype}\u201d gombra n\u00e9gyszer kattintottak (alternat\u00edv m\u00f3d)", + "remote_button_alt_long_release": "\"{subtype}\" gomb elenged\u00e9se nyomvatart\u00e1st k\u00f6vet\u0151en (alternat\u00edv m\u00f3d)", + "remote_button_alt_quadruple_press": "\"{subtype}\" gombra n\u00e9gyszer kattintottak (alternat\u00edv m\u00f3d)", "remote_button_alt_quintuple_press": "\"{subtype}\" gombra \u00f6tsz\u00f6r kattintottak (alternat\u00edv m\u00f3d)", - "remote_button_alt_short_press": "\u201e{subtype}\u201d gomb lenyomva (alternat\u00edv m\u00f3d)", - "remote_button_alt_short_release": "A \"{subtype}\" gomb elengedett (alternat\u00edv m\u00f3d)", - "remote_button_alt_triple_press": "A \u201e{subtype}\u201d gombra h\u00e1romszor kattintottak (alternat\u00edv m\u00f3d)", + "remote_button_alt_short_press": "\"{subtype}\" gomb lenyomva (alternat\u00edv m\u00f3d)", + "remote_button_alt_short_release": "\"{subtype}\" gomb elengedett (alternat\u00edv m\u00f3d)", + "remote_button_alt_triple_press": "\"{subtype}\" gombra h\u00e1romszor kattintottak (alternat\u00edv m\u00f3d)", "remote_button_double_press": "\"{subtype}\" gombra k\u00e9tszer kattintottak", - "remote_button_long_press": "A \"{subtype}\" gomb folyamatosan lenyomva", - "remote_button_long_release": "A \"{subtype}\" gomb hossz\u00fa megnyom\u00e1s ut\u00e1n elengedve", + "remote_button_long_press": "\"{subtype}\" gomb folyamatosan lenyomva", + "remote_button_long_release": "\"{subtype}\" gomb nyomvatart\u00e1s ut\u00e1n elengedve", "remote_button_quadruple_press": "\"{subtype}\" gombra n\u00e9gyszer kattintottak", "remote_button_quintuple_press": "\"{subtype}\" gombra \u00f6tsz\u00f6r kattintottak", "remote_button_short_press": "\"{subtype}\" gomb lenyomva", diff --git a/homeassistant/components/zha/translations/id.json b/homeassistant/components/zha/translations/id.json index 9ede5ff9d98..83b3782eaf7 100644 --- a/homeassistant/components/zha/translations/id.json +++ b/homeassistant/components/zha/translations/id.json @@ -87,6 +87,7 @@ "default_light_transition": "Waktu transisi lampu default (detik)", "enable_identify_on_join": "Aktifkan efek identifikasi saat perangkat bergabung dengan jaringan", "enhanced_light_transition": "Aktifkan versi canggih untuk transisi warna/suhu cahaya dari keadaan tidak aktif", + "group_members_assume_state": "Anggota kelompok mengasumsikan status grup", "light_transitioning_flag": "Aktifkan penggeser kecerahan yang lebih canggih pada waktu transisi lampu", "title": "Opsi Global" } @@ -98,6 +99,7 @@ }, "trigger_subtype": { "both_buttons": "Kedua tombol", + "button": "Tombol", "button_1": "Tombol pertama", "button_2": "Tombol kedua", "button_3": "Tombol ketiga", @@ -129,22 +131,22 @@ "device_shaken": "Perangkat diguncangkan", "device_slid": "Perangkat diluncurkan \"{subtype}\"", "device_tilted": "Perangkat dimiringkan", - "remote_button_alt_double_press": "Tombol \"{subtype}\" diklik dua kali (Mode alternatif)", - "remote_button_alt_long_press": "Tombol \"{subtype}\" terus ditekan (Mode alternatif)", - "remote_button_alt_long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama (Mode alternatif)", - "remote_button_alt_quadruple_press": "Tombol \"{subtype}\" diklik empat kali (Mode alternatif)", - "remote_button_alt_quintuple_press": "Tombol \"{subtype}\" diklik lima kali (Mode alternatif)", - "remote_button_alt_short_press": "Tombol \"{subtype}\" ditekan (Mode alternatif)", - "remote_button_alt_short_release": "Tombol \"{subtype}\" dilepaskan (Mode alternatif)", - "remote_button_alt_triple_press": "Tombol \"{subtype}\" diklik tiga kali (Mode alternatif)", - "remote_button_double_press": "Tombol \"{subtype}\" diklik dua kali", - "remote_button_long_press": "Tombol \"{subtype}\" terus ditekan", - "remote_button_long_release": "Tombol \"{subtype}\" dilepaskan setelah ditekan lama", - "remote_button_quadruple_press": "Tombol \"{subtype}\" diklik empat kali", - "remote_button_quintuple_press": "Tombol \"{subtype}\" diklik lima kali", - "remote_button_short_press": "Tombol \"{subtype}\" ditekan", - "remote_button_short_release": "Tombol \"{subtype}\" dilepaskan", - "remote_button_triple_press": "Tombol \"{subtype}\" diklik tiga kali" + "remote_button_alt_double_press": "\"{subtype}\" diklik dua kali (Mode alternatif)", + "remote_button_alt_long_press": "\"{subtype}\" terus ditekan (Mode alternatif)", + "remote_button_alt_long_release": "\"{subtype}\" dilepaskan setelah ditekan lama (Mode alternatif)", + "remote_button_alt_quadruple_press": "\"{subtype}\" diklik empat kali (Mode alternatif)", + "remote_button_alt_quintuple_press": "\"{subtype}\" diklik lima kali (Mode alternatif)", + "remote_button_alt_short_press": "\"{subtype}\" ditekan (Mode alternatif)", + "remote_button_alt_short_release": "\"{subtype}\" dilepaskan (Mode alternatif)", + "remote_button_alt_triple_press": "\"{subtype}\" diklik tiga kali (Mode alternatif)", + "remote_button_double_press": "\"{subtype}\" diklik dua kali", + "remote_button_long_press": "\"{subtype}\" terus ditekan", + "remote_button_long_release": "\"{subtype}\" dilepaskan setelah ditekan lama", + "remote_button_quadruple_press": "\"{subtype}\" diklik empat kali", + "remote_button_quintuple_press": "\"{subtype}\" diklik lima kali", + "remote_button_short_press": "\"{subtype}\" ditekan", + "remote_button_short_release": "\"{subtype}\" dilepaskan", + "remote_button_triple_press": "\"{subtype}\" diklik tiga kali" } }, "options": { diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index 56f0f28e7f6..e7890372162 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -87,6 +87,7 @@ "default_light_transition": "Tempo di transizione della luce predefinito (secondi)", "enable_identify_on_join": "Abilita l'effetto di identificazione quando i dispositivi si uniscono alla rete", "enhanced_light_transition": "Abilita una transizione migliorata del colore/temperatura della luce da uno stato spento", + "group_members_assume_state": "I membri del gruppo assumono lo stato del gruppo", "light_transitioning_flag": "Abilita il cursore della luminosit\u00e0 avanzata durante la transizione della luce", "title": "Opzioni globali" } @@ -100,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "Entrambi i pulsanti", + "button": "Pulsante", "button_1": "Primo pulsante", "button_2": "Secondo pulsante", "button_3": "Terzo pulsante", @@ -107,7 +109,7 @@ "button_5": "Quinto pulsante", "button_6": "Sesto pulsante", "close": "Chiudere", - "dim_down": "Diminuire luminosit\u00e0", + "dim_down": "Diminuisce luminosit\u00e0", "dim_up": "Aumenta luminosit\u00e0", "face_1": "con faccia 1 attivata", "face_2": "con faccia 2 attivata", @@ -131,22 +133,22 @@ "device_shaken": "Dispositivo in vibrazione", "device_slid": "Dispositivo scivolato \"{subtype}\"", "device_tilted": "Dispositivo inclinato", - "remote_button_alt_double_press": "Pulsante \"{subtype}\" cliccato due volte (modalit\u00e0 Alternata)", - "remote_button_alt_long_press": "Pulsante \"{subtype}\" premuto continuamente (modalit\u00e0 Alternata)", - "remote_button_alt_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione (modalit\u00e0 Alternata)", - "remote_button_alt_quadruple_press": "Pulsante \"{subtype}\" cliccato quattro volte (modalit\u00e0 Alternata)", - "remote_button_alt_quintuple_press": "Pulsante \"{subtype}\" cliccato cinque volte (modalit\u00e0 Alternata)", - "remote_button_alt_short_press": "Pulsante \"{subtype}\" premuto (modalit\u00e0 Alternata)", - "remote_button_alt_short_release": "Pulsante \"{subtype}\" rilasciato (modalit\u00e0 Alternata)", - "remote_button_alt_triple_press": "Pulsante \"{subtype}\" cliccato tre volte (modalit\u00e0 Alternata)", - "remote_button_double_press": "Pulsante \"{subtype}\" cliccato due volte", - "remote_button_long_press": "Pulsante \"{subtype}\" premuto continuamente", - "remote_button_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione", - "remote_button_quadruple_press": "Pulsante \"{subtype}\" cliccato quattro volte", - "remote_button_quintuple_press": "Pulsante \"{subtype}\" cliccato cinque volte", - "remote_button_short_press": "Pulsante \"{subtype}\" premuto", - "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", - "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte" + "remote_button_alt_double_press": "\"{subtype}\" cliccato due volte (modalit\u00e0 Alternata)", + "remote_button_alt_long_press": "\"{subtype}\" premuto continuamente (modalit\u00e0 Alternata)", + "remote_button_alt_long_release": "\"{subtype}\" rilasciato dopo una lunga pressione (modalit\u00e0 Alternata)", + "remote_button_alt_quadruple_press": "\"{subtype}\" cliccato quattro volte (modalit\u00e0 Alternata)", + "remote_button_alt_quintuple_press": "\"{subtype}\" cliccato cinque volte (modalit\u00e0 Alternata)", + "remote_button_alt_short_press": "\"{subtype}\" premuto (modalit\u00e0 Alternata)", + "remote_button_alt_short_release": "\"{subtype}\" rilasciato (modalit\u00e0 Alternata)", + "remote_button_alt_triple_press": "\"{subtype}\" cliccato tre volte (modalit\u00e0 Alternata)", + "remote_button_double_press": "\"{subtype}\" cliccato due volte", + "remote_button_long_press": "\"{subtype}\" premuto continuamente", + "remote_button_long_release": "\"{subtype}\" rilasciato dopo una lunga pressione", + "remote_button_quadruple_press": "\"{subtype}\" cliccato quattro volte", + "remote_button_quintuple_press": "\"{subtype}\" cliccato cinque volte", + "remote_button_short_press": "\"{subtype}\" premuto", + "remote_button_short_release": "\"{subtype}\" rilasciato", + "remote_button_triple_press": "\"{subtype}\" cliccato tre volte" } }, "options": { diff --git a/homeassistant/components/zha/translations/ja.json b/homeassistant/components/zha/translations/ja.json index ba8da34fd73..e9b9c290ec6 100644 --- a/homeassistant/components/zha/translations/ja.json +++ b/homeassistant/components/zha/translations/ja.json @@ -81,12 +81,12 @@ "title": "\u30a2\u30e9\u30fc\u30e0 \u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30d1\u30cd\u30eb\u306e\u30aa\u30d7\u30b7\u30e7\u30f3" }, "zha_options": { - "always_prefer_xy_color_mode": "\u5e38\u306bXY\u30ab\u30e9\u30fc\u30e2\u30fc\u30c9\u3092\u512a\u5148", - "consider_unavailable_battery": "\u30d0\u30c3\u30c6\u30ea\u99c6\u52d5\u306e\u30c7\u30d0\u30a4\u30b9\u306f (\u79d2) \u5f8c\u306b\u5229\u7528\u3067\u304d\u306a\u3044\u3068\u898b\u306a\u3059", - "consider_unavailable_mains": "(\u79d2) \u5f8c\u306b\u4e3b\u96fb\u6e90\u304c\u4f9b\u7d66\u3055\u308c\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u4f7f\u7528\u3067\u304d\u306a\u3044\u3068\u898b\u306a\u3059", + "always_prefer_xy_color_mode": "\u5e38\u306bXY\u30ab\u30e9\u30fc\u30e2\u30fc\u30c9\u3092\u512a\u5148\u3059\u308b", + "consider_unavailable_battery": "\u30d0\u30c3\u30c6\u30ea\u30fc\u99c6\u52d5\u306e\u30c7\u30d0\u30a4\u30b9\u306f\u3001(\u79d2)\u5f8c\u306b\u5229\u7528\u3067\u304d\u306a\u3044\u3068\u898b\u306a\u3059", + "consider_unavailable_mains": "(\u79d2)\u5f8c\u306b\u4e3b\u96fb\u6e90\u304c\u4f9b\u7d66\u3055\u308c\u3066\u3044\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u4f7f\u7528\u3067\u304d\u306a\u3044\u3068\u898b\u306a\u3059", "default_light_transition": "\u30c7\u30d5\u30a9\u30eb\u30c8\u306e\u30e9\u30a4\u30c8\u9077\u79fb\u6642\u9593(\u79d2)", "enable_identify_on_join": "\u30c7\u30d0\u30a4\u30b9\u304c\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u306b\u53c2\u52a0\u3059\u308b\u969b\u306b\u3001\u8b58\u5225\u52b9\u679c\u3092\u6709\u52b9\u306b\u3059\u308b", - "enhanced_light_transition": "\u30aa\u30d5\u72b6\u614b\u304b\u3089\u3001\u30a8\u30f3\u30cf\u30f3\u30b9\u30c9\u30e9\u30a4\u30c8\u30ab\u30e9\u30fc/\u8272\u6e29\u5ea6\u3078\u306e\u9077\u79fb\u3092\u6709\u52b9\u306b\u3057\u307e\u3059", + "enhanced_light_transition": "\u30aa\u30d5\u72b6\u614b\u304b\u3089\u3001\u30a8\u30f3\u30cf\u30f3\u30b9\u30c9\u30e9\u30a4\u30c8\u30ab\u30e9\u30fc/\u8272\u6e29\u5ea6\u3078\u306e\u9077\u79fb\u3092\u6709\u52b9\u306b\u3059\u308b", "light_transitioning_flag": "\u5149\u6e90\u79fb\u884c\u6642\u306e\u8f1d\u5ea6\u30b9\u30e9\u30a4\u30c0\u30fc\u306e\u62e1\u5f35\u3092\u6709\u52b9\u306b\u3059\u308b", "title": "\u30b0\u30ed\u30fc\u30d0\u30eb\u30aa\u30d7\u30b7\u30e7\u30f3" } @@ -100,6 +100,7 @@ }, "trigger_subtype": { "both_buttons": "\u4e21\u65b9\u306e\u30dc\u30bf\u30f3", + "button": "\u30dc\u30bf\u30f3", "button_1": "1\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_2": "2\u756a\u76ee\u306e\u30dc\u30bf\u30f3", "button_3": "3\u756a\u76ee\u306e\u30dc\u30bf\u30f3", diff --git a/homeassistant/components/zha/translations/lt.json b/homeassistant/components/zha/translations/lt.json new file mode 100644 index 00000000000..78c5e36fcbe --- /dev/null +++ b/homeassistant/components/zha/translations/lt.json @@ -0,0 +1,9 @@ +{ + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "U\u017erakinimui reikalingas kodas", + "alarm_master_code": "Signalizacijos valdymo pulto (-\u0173) pagrindinis kodas", + "title": "Signalizacijos valdymo pulto parinktys" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/lv.json b/homeassistant/components/zha/translations/lv.json new file mode 100644 index 00000000000..a2ee2bb8e31 --- /dev/null +++ b/homeassistant/components/zha/translations/lv.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "trigger_subtype": { + "button": "Poga" + }, + "trigger_type": { + "remote_button_short_press": "\"{subtype}\" piespiests", + "remote_button_short_release": "\"{subtype}\" atlaists" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json index a01d56c7b8f..d30d2ecefcb 100644 --- a/homeassistant/components/zha/translations/nl.json +++ b/homeassistant/components/zha/translations/nl.json @@ -87,6 +87,7 @@ "default_light_transition": "Standaard licht transitietijd (seconden)", "enable_identify_on_join": "Schakel het identificatie-effect in wanneer apparaten in het netwerk komen", "enhanced_light_transition": "Inschakelen geavanceerde light kleur/temperatuur transitie vanaf uitgeschakelde toestand", + "group_members_assume_state": "Groepsleden nemen de status van de groep aan", "light_transitioning_flag": "Inschakelen geavanceerde helderheid schuifregelaar gedurende de lichtovergang", "title": "Globale opties" } @@ -100,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "Beide knoppen", + "button": "Drukknop", "button_1": "Eerste knop", "button_2": "Tweede knop", "button_3": "Derde knop", diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json index 0d55fc2be2a..9787800ca95 100644 --- a/homeassistant/components/zha/translations/no.json +++ b/homeassistant/components/zha/translations/no.json @@ -87,6 +87,7 @@ "default_light_transition": "Standard lysovergangstid (sekunder)", "enable_identify_on_join": "Aktiver identifiseringseffekt n\u00e5r enheter blir med i nettverket", "enhanced_light_transition": "Aktiver forbedret lysfarge/temperaturovergang fra en off-tilstand", + "group_members_assume_state": "Gruppemedlemmer antar gruppestatus", "light_transitioning_flag": "Aktiver skyveknappen for forbedret lysstyrke under lysovergang", "title": "Globale alternativer" } @@ -100,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "Begge knapper", + "button": "Knapp", "button_1": "F\u00f8rste knapp", "button_2": "Andre knapp", "button_3": "Tredje knapp", @@ -131,22 +133,22 @@ "device_shaken": "Enhet ristet", "device_slid": "Enheten skled \"{subtype}\"", "device_tilted": "Enheten skr\u00e5stilt", - "remote_button_alt_double_press": "\"{subtype}\"-knapp trykket p\u00e5 to ganger (vekslende modus)", - "remote_button_alt_long_press": "\"{subtype}\"-knapp holdt inne (vekslende modus)", - "remote_button_alt_long_release": "\"{subtype}\"-knapp sluppet etter langt trykk (vekslende modus)", - "remote_button_alt_quadruple_press": "\"{subtype}\"-knapp trykket p\u00e5 fire ganger (vekslende modus)", - "remote_button_alt_quintuple_press": "\"{subtype}\"-knapp trykket p\u00e5 fem ganger (vekslende modus)", - "remote_button_alt_short_press": "\"{subtype}\"-knapp trykket p\u00e5 (vekslende modus)", - "remote_button_alt_short_release": "\"{subtype}\"-knapp sluppet (vekslende modus)", - "remote_button_alt_triple_press": "\"{subtype}\"-knapp trykket p\u00e5 tre ganger (vekslende modus)", - "remote_button_double_press": "\"{subtype}\" knapp trykket p\u00e5 to ganger", - "remote_button_long_press": "\"{subtype}\"-knapp holdt inne", - "remote_button_long_release": "\"{subtype}\"-knapp sluppet etter langt trykk", - "remote_button_quadruple_press": "\"{subtype}\"-knapp trykket p\u00e5 fire ganger", - "remote_button_quintuple_press": "\"{subtype}\"-knapp trykket p\u00e5 fem ganger", - "remote_button_short_press": "\"{subtype}\"-knapp trykket p\u00e5", - "remote_button_short_release": "\"{subtype}\"-knapp sluppet", - "remote_button_triple_press": "\"{subtype}\"-knapp trykket p\u00e5 tre ganger" + "remote_button_alt_double_press": "\" {subtype} \" dobbeltklikket (alternativ modus)", + "remote_button_alt_long_press": "\" {subtype} \" kontinuerlig trykket (alternativ modus)", + "remote_button_alt_long_release": "\" {subtype} \" slippes etter lang trykk (alternativ modus)", + "remote_button_alt_quadruple_press": "\" {subtype} \" firedoblet klikk (alternativ modus)", + "remote_button_alt_quintuple_press": "\" {subtype} \" femdobbelt klikket (alternativ modus)", + "remote_button_alt_short_press": "\" {subtype} \" trykket (alternativ modus)", + "remote_button_alt_short_release": "\" {subtype} \" utgitt (alternativ modus)", + "remote_button_alt_triple_press": "\" {subtype} \" trippelklikket (alternativ modus)", + "remote_button_double_press": "\" {subtype} \" dobbeltklikket", + "remote_button_long_press": "\" {subtype} \" kontinuerlig trykket", + "remote_button_long_release": "\" {subtype} \" utgitt etter lang trykk", + "remote_button_quadruple_press": "\" {subtype} \" firedoblet klikk", + "remote_button_quintuple_press": "\" {subtype} \" femdobbelt klikket", + "remote_button_short_press": "\" {subtype} \" trykket", + "remote_button_short_release": "\" {subtype} \" utgitt", + "remote_button_triple_press": "\" {subtype} \" trippelklikket" } }, "options": { diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index c3593b862ce..bd0b2eb81b1 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -87,6 +87,7 @@ "default_light_transition": "Domy\u015blny czas efektu przej\u015bcia dla \u015bwiat\u0142a (w sekundach)", "enable_identify_on_join": "W\u0142\u0105cz efekt identyfikacji, gdy urz\u0105dzenia do\u0142\u0105czaj\u0105 do sieci", "enhanced_light_transition": "W\u0142\u0105cz ulepszone przej\u015bcie koloru \u015bwiat\u0142a/temperatury ze stanu wy\u0142\u0105czenia", + "group_members_assume_state": "Encje grupy przyjmuj\u0105 stan grupy", "light_transitioning_flag": "W\u0142\u0105cz suwak zwi\u0119kszonej jasno\u015bci podczas przej\u015bcia \u015bwiat\u0142a", "title": "Opcje og\u00f3lne" } @@ -100,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "oba przyciski", + "button": "Przycisk", "button_1": "pierwszy", "button_2": "drugi", "button_3": "trzeci", @@ -131,22 +133,22 @@ "device_shaken": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", "device_slid": "nast\u0105pi przesuni\u0119cie urz\u0105dzenia \"{subtype}\"", "device_tilted": "nast\u0105pi przechylenie urz\u0105dzenia", - "remote_button_alt_double_press": "przycisk \"{subtype}\" zostanie dwukrotnie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_alt_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y (tryb alternatywny)", - "remote_button_alt_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu (tryb alternatywny)", - "remote_button_alt_quadruple_press": "przycisk \"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_alt_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_alt_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_alt_short_release": "przycisk \"{subtype}\" zostanie zwolniony (tryb alternatywny)", - "remote_button_alt_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", - "remote_button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", - "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", - "remote_button_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", - "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", - "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", - "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" + "remote_button_alt_double_press": "\"{subtype}\" zostanie dwukrotnie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y (tryb alternatywny)", + "remote_button_alt_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu (tryb alternatywny)", + "remote_button_alt_quadruple_press": "\"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_short_release": "\"{subtype}\" zostanie zwolniony (tryb alternatywny)", + "remote_button_alt_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_double_press": "\"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "\"{subtype}\" czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty", + "remote_button_short_release": "\"{subtype}\" zostanie zwolniony", + "remote_button_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" } }, "options": { diff --git a/homeassistant/components/zha/translations/pt-BR.json b/homeassistant/components/zha/translations/pt-BR.json index 0eac3642f58..f3cfde000a6 100644 --- a/homeassistant/components/zha/translations/pt-BR.json +++ b/homeassistant/components/zha/translations/pt-BR.json @@ -87,6 +87,7 @@ "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", "enhanced_light_transition": "Ative a transi\u00e7\u00e3o de cor/temperatura da luz aprimorada de um estado desligado", + "group_members_assume_state": "Os membros do grupo assumem o estado do grupo", "light_transitioning_flag": "Ative o controle deslizante de brilho aprimorado durante a transi\u00e7\u00e3o de luz", "title": "Op\u00e7\u00f5es globais" } @@ -100,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "Ambos os bot\u00f5es", + "button": "Bot\u00e3o", "button_1": "Primeiro bot\u00e3o", "button_2": "Segundo bot\u00e3o", "button_3": "Terceiro bot\u00e3o", @@ -131,22 +133,22 @@ "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" + "remote_button_alt_double_press": "\"{subtype}\" duplo clique (modo alternativo)", + "remote_button_alt_long_press": "\"{subtype}\" pressionado continuamente (modo alternativo)", + "remote_button_alt_long_release": "\"{subtype}\" liberado ap\u00f3s um toque longo (modo alternativo)", + "remote_button_alt_quadruple_press": "\"{subtype}\" qu\u00e1druplo clicado (modo alternativo)", + "remote_button_alt_quintuple_press": "\"{subtype}\" qu\u00edntuplo clicado (modo alternativo)", + "remote_button_alt_short_press": "\"{subtype}\" pressionado (modo alternativo)", + "remote_button_alt_short_release": "\"{subtype}\" liberado (modo alternativo)", + "remote_button_alt_triple_press": "\"{subtype}\" triplo clique (modo alternativo)", + "remote_button_double_press": "\"{subtype}\" clicado duas vezes", + "remote_button_long_press": "\"{subtype}\" pressionado continuamente", + "remote_button_long_release": "\"{subtype}\" liberado ap\u00f3s um toque longo", + "remote_button_quadruple_press": "\"{subtype}\" qu\u00e1druplo clicado", + "remote_button_quintuple_press": "\"{subtype}\" qu\u00edntuplo clicado", + "remote_button_short_press": "\"{subtype}\" pressionado", + "remote_button_short_release": "\"{subtype}\" liberado", + "remote_button_triple_press": "\"{subtype}\" triplo clique" } }, "options": { diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index bc935c9aa75..24e97367a8d 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -87,6 +87,7 @@ "default_light_transition": "\u0412\u0440\u0435\u043c\u044f \u043f\u043b\u0430\u0432\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u0441\u0432\u0435\u0442\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "enable_identify_on_join": "\u042d\u0444\u0444\u0435\u043a\u0442 \u0434\u043b\u044f \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043a \u0441\u0435\u0442\u0438", "enhanced_light_transition": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0443\u043b\u0443\u0447\u0448\u0435\u043d\u043d\u044b\u0439 \u043f\u0435\u0440\u0435\u0445\u043e\u0434 \u0446\u0432\u0435\u0442\u0430/\u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b \u0441\u0432\u0435\u0442\u0430 \u0438\u0437 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u0433\u043e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", + "group_members_assume_state": "\u0427\u043b\u0435\u043d\u044b \u0433\u0440\u0443\u043f\u043f\u044b \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u044e\u0442 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0433\u0440\u0443\u043f\u043f\u044b", "light_transitioning_flag": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0443\u043b\u0443\u0447\u0448\u0435\u043d\u043d\u044b\u0439 \u043f\u043e\u043b\u0437\u0443\u043d\u043e\u043a \u044f\u0440\u043a\u043e\u0441\u0442\u0438 \u043f\u0440\u0438 \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0435 \u0441\u0432\u0435\u0442\u0430", "title": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" } @@ -98,6 +99,7 @@ }, "trigger_subtype": { "both_buttons": "\u041e\u0431\u0435 \u043a\u043d\u043e\u043f\u043a\u0438", + "button": "\u041a\u043d\u043e\u043f\u043a\u0430", "button_1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", @@ -129,22 +131,22 @@ "device_shaken": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0442\u0440\u044f\u0441\u043b\u0438", "device_slid": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0434\u0432\u0438\u043d\u0443\u043b\u0438 {subtype}", "device_tilted": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0430\u043a\u043b\u043e\u043d\u0438\u043b\u0438", - "remote_button_alt_double_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", - "remote_button_alt_long_press": "{subtype} \u0434\u043e\u043b\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", - "remote_button_alt_long_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", - "remote_button_alt_quadruple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", - "remote_button_alt_quintuple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", - "remote_button_alt_short_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", - "remote_button_alt_short_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", - "remote_button_alt_triple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", - "remote_button_double_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", - "remote_button_long_press": "{subtype} \u0434\u043e\u043b\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0430", - "remote_button_long_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", - "remote_button_quadruple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", - "remote_button_quintuple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", - "remote_button_short_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430", - "remote_button_short_release": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", - "remote_button_triple_press": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430" + "remote_button_alt_double_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_long_press": "\"{subtype}\" \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0436\u0430\u0442\u043e\u0439 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_long_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_quadruple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_quintuple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_short_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_short_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_triple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_double_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", + "remote_button_long_press": "\"{subtype}\" \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043d\u0430\u0436\u0430\u0442\u043e\u0439", + "remote_button_long_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043e\u043b\u0433\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_quadruple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", + "remote_button_quintuple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", + "remote_button_short_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_short_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_triple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430" } }, "options": { diff --git a/homeassistant/components/zha/translations/sk.json b/homeassistant/components/zha/translations/sk.json index 5d29db03b34..fc10a3f8e4b 100644 --- a/homeassistant/components/zha/translations/sk.json +++ b/homeassistant/components/zha/translations/sk.json @@ -87,6 +87,7 @@ "default_light_transition": "Predvolen\u00fd \u010das prechodu svetla (sekundy)", "enable_identify_on_join": "Povoli\u0165 efekt identifik\u00e1cie, ke\u010f sa zariadenia prip\u00e1jaj\u00fa k sieti", "enhanced_light_transition": "Povoli\u0165 vylep\u0161en\u00fd prechod farby svetla/teploty z vypnut\u00e9ho stavu", + "group_members_assume_state": "\u010clenovia skupiny prevezm\u00fa stav skupiny", "light_transitioning_flag": "Povolenie vylep\u0161en\u00e9ho posuvn\u00edka jasu po\u010das prechodu svetla", "title": "Glob\u00e1lne mo\u017enosti" } @@ -100,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "Obe tla\u010didl\u00e1", + "button": "Tla\u010didlo", "button_1": "Prv\u00e9 tla\u010didlo", "button_2": "Druh\u00e9 tla\u010didlo", "button_3": "Tretie tla\u010didlo", @@ -124,29 +126,29 @@ }, "trigger_type": { "device_dropped": "Zariadenie spadlo", - "device_flipped": "Zariadenie sa obr\u00e1tilo \u201e{subtype}\u201c", - "device_knocked": "Zariadenie zaklopalo \u201e{subtype}\u201c", + "device_flipped": "Zariadenie sa obr\u00e1tilo \"{subtype}\"", + "device_knocked": "Zariadenie zaklopalo \"{subtype}\"", "device_offline": "Zariadenie je offline", - "device_rotated": "Zariadenie oto\u010den\u00e9 \u201e{subtype}\u201c", + "device_rotated": "Zariadenie oto\u010den\u00e9 \"{subtype}\"", "device_shaken": "Zariadenie sa zatriaslo", - "device_slid": "Zariadenie posunut\u00e9 \u201e{subtype}\u201c", + "device_slid": "Zariadenie posunut\u00e9 \"{subtype}\"", "device_tilted": "Zariadenie je naklonen\u00e9", - "remote_button_alt_double_press": "dvojit\u00e9 kliknutie na tla\u010didlo \u201e{subtype}\u201c (Alternat\u00edvny re\u017eim)", - "remote_button_alt_long_press": "Trvalo stla\u010den\u00e9 tla\u010didlo \u201e{subtype}\u201c (Alternat\u00edvny re\u017eim)", - "remote_button_alt_long_release": "Tla\u010didlo \"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed (alternat\u00edvny re\u017eim)", - "remote_button_alt_quadruple_press": "\u0161tvorit\u00e9 kliknutie na tla\u010didlo \u201e{subtype}\u201c (Alternat\u00edvny re\u017eim)", - "remote_button_alt_quintuple_press": "p\u00e4\u0165n\u00e1sobn\u00e9 kliknutie na tla\u010didlo \u201e{subtype}\u201c (Alternat\u00edvny re\u017eim)", - "remote_button_alt_short_press": "Tla\u010didlo \"{subtype}\" stla\u010den\u00e9 (alternat\u00edvny re\u017eim)", - "remote_button_alt_short_release": "Uvolnen\u00e9 tla\u010didlo \"{subtype}\" (alternat\u00edvny re\u017eim)", - "remote_button_alt_triple_press": "trojit\u00e9 kliknutie na tla\u010didlo \u201e{subtype}\u201c (Alternat\u00edvny re\u017eim)", - "remote_button_double_press": "dvojklik na tla\u010didlo \u201e{subtype}\u201c", - "remote_button_long_press": "Trvalo stla\u010den\u00e9 tla\u010didlo \"{subtype}\"", - "remote_button_long_release": "Tla\u010didlo \"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed", - "remote_button_quadruple_press": "Tla\u010didlo \"{subtype}\" kliknut\u00e9 \u0161tyrikr\u00e1t", - "remote_button_quintuple_press": "Tla\u010didlo \"{subtype}\" kliknut\u00e9 p\u00e4\u0165kr\u00e1t", - "remote_button_short_press": "Stla\u010den\u00e9 tla\u010didlo \"{subtype}\"", - "remote_button_short_release": "Tla\u010didlo \"{subtype}\" bolo uvo\u013enen\u00e9", - "remote_button_triple_press": "Trojklik na tla\u010didlo \"{subtype}\"" + "remote_button_alt_double_press": "dvojit\u00e9 kliknutie na \"{subtype}\" (Alternat\u00edvny re\u017eim)", + "remote_button_alt_long_press": "\"{subtype}\" trvalo stla\u010den\u00e9 (Alternat\u00edvny re\u017eim)", + "remote_button_alt_long_release": "\"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed (alternat\u00edvny re\u017eim)", + "remote_button_alt_quadruple_press": "\"{subtype}\" \u0161tvorit\u00e9 kliknutie (Alternat\u00edvny re\u017eim)", + "remote_button_alt_quintuple_press": "\"{subtype}\" p\u00e4\u0165n\u00e1sobn\u00e9 kliknutie (Alternat\u00edvny re\u017eim)", + "remote_button_alt_short_press": "\"{subtype}\" stla\u010den\u00e9 (alternat\u00edvny re\u017eim)", + "remote_button_alt_short_release": "\"{subtype}\" Uvolnen\u00e9 (alternat\u00edvny re\u017eim)", + "remote_button_alt_triple_press": "\"{subtype}\" trojit\u00e9 kliknutie (Alternat\u00edvny re\u017eim)", + "remote_button_double_press": "\"{subtype}\" dvojklik", + "remote_button_long_press": "\"{subtype}\" trvalo stla\u010den\u00e9", + "remote_button_long_release": "\"{subtype}\" uvo\u013enen\u00e9 po dlhom stla\u010den\u00ed", + "remote_button_quadruple_press": "\"{subtype}\" kliknut\u00e9 \u0161tyrikr\u00e1t", + "remote_button_quintuple_press": "\"{subtype}\" kliknut\u00e9 p\u00e4\u0165kr\u00e1t", + "remote_button_short_press": "\"{subtype}\" stla\u010den\u00e9", + "remote_button_short_release": "\"{subtype}\" bolo uvo\u013enen\u00e9", + "remote_button_triple_press": "\"{subtype}\" trojklik" } }, "options": { diff --git a/homeassistant/components/zha/translations/sv.json b/homeassistant/components/zha/translations/sv.json index 5fb36e9644b..eefc4c8e6e2 100644 --- a/homeassistant/components/zha/translations/sv.json +++ b/homeassistant/components/zha/translations/sv.json @@ -87,6 +87,7 @@ "default_light_transition": "Standard ljus\u00f6verg\u00e5ngstid (sekunder)", "enable_identify_on_join": "Aktivera identifieringseffekt n\u00e4r enheter ansluter till n\u00e4tverket", "enhanced_light_transition": "Aktivera f\u00f6rb\u00e4ttrad ljusf\u00e4rg/temperatur\u00f6verg\u00e5ng fr\u00e5n ett avst\u00e4ngt l\u00e4ge", + "group_members_assume_state": "Gruppmedlemmar antar gruppens tillst\u00e5nd", "light_transitioning_flag": "Aktivera f\u00f6rb\u00e4ttrad ljusstyrka vid ljus\u00f6verg\u00e5ng", "title": "Globala alternativ" } @@ -100,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "B\u00e5da knapparna", + "button": "Knapp", "button_1": "F\u00f6rsta knappen", "button_2": "Andra knappen", "button_3": "Tredje knappen", @@ -139,14 +141,14 @@ "remote_button_alt_short_press": "\"{subtype}\" trycktes (Alternativt l\u00e4ge)", "remote_button_alt_short_release": "\"{subtype}\" sl\u00e4pptes upp (Alternativt l\u00e4ge)", "remote_button_alt_triple_press": "\"{subtype}\" trippelklickades (Alternativt l\u00e4ge)", - "remote_button_double_press": "\"{subtype}\"-knappen dubbelklickades", - "remote_button_long_press": "\"{subtype}\"-knappen kontinuerligt nedtryckt", - "remote_button_long_release": "\"{subtype}\"-knappen sl\u00e4pptes efter ett l\u00e5ngttryck", - "remote_button_quadruple_press": "\"{subtype}\"-knappen klickades \nfyrfaldigt", - "remote_button_quintuple_press": "\"{subtype}\"-knappen klickades \nfemfaldigt", - "remote_button_short_press": "\"{subtype}\"-knappen trycktes in", - "remote_button_short_release": "\"{subtype}\"-knappen sl\u00e4ppt", - "remote_button_triple_press": "\"{subtype}\"-knappen trippelklickades" + "remote_button_double_press": "\"{subtype}\" dubbelklickades", + "remote_button_long_press": "\"{subtype}\" kontinuerligt nedtryckt", + "remote_button_long_release": "\"{subtype}\" sl\u00e4pptes efter l\u00e5ngtryckning", + "remote_button_quadruple_press": "\"{subtype}\" klickades fyrfaldigt", + "remote_button_quintuple_press": "\"{subtype}\" klickades femfaldigt", + "remote_button_short_press": "\"{subtype}\" trycktes in", + "remote_button_short_release": "\"{subtype}\" sl\u00e4pptes", + "remote_button_triple_press": "\"{subtype}\" trippelklickades" } }, "options": { diff --git a/homeassistant/components/zha/translations/tr.json b/homeassistant/components/zha/translations/tr.json index e4409a7a1f0..3b316220ea0 100644 --- a/homeassistant/components/zha/translations/tr.json +++ b/homeassistant/components/zha/translations/tr.json @@ -36,10 +36,10 @@ "title": "Seri Ba\u011flant\u0131 Noktas\u0131 Se\u00e7in" }, "confirm": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "confirm_hardware": { - "description": "{name} kurulumunu yapmak istiyor musunuz?" + "description": "{name} 'i kurmak istiyor musunuz?" }, "manual_pick_radio_type": { "data": { diff --git a/homeassistant/components/zha/translations/uk.json b/homeassistant/components/zha/translations/uk.json index 7e9fdbbca54..e36e78e8044 100644 --- a/homeassistant/components/zha/translations/uk.json +++ b/homeassistant/components/zha/translations/uk.json @@ -14,6 +14,7 @@ }, "trigger_subtype": { "both_buttons": "\u041e\u0431\u0438\u0434\u0432\u0456 \u043a\u043d\u043e\u043f\u043a\u0438", + "button": "\u041a\u043d\u043e\u043f\u043a\u0430", "button_1": "\u041f\u0435\u0440\u0448\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", "button_2": "\u0414\u0440\u0443\u0433\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", "button_3": "\u0422\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0430", diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index 78cd1dfdb66..3300264987b 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -87,6 +87,7 @@ "default_light_transition": "\u9810\u8a2d\u71c8\u5149\u8f49\u63db\u6642\u9593\uff08\u79d2\uff09", "enable_identify_on_join": "\u7576\u88dd\u7f6e\u52a0\u5165\u7db2\u8def\u6642\u3001\u958b\u555f\u8b58\u5225\u6548\u679c", "enhanced_light_transition": "\u958b\u555f\u7531\u95dc\u9589\u72c0\u614b\u589e\u5f37\u5149\u8272/\u8272\u6eab\u8f49\u63db", + "group_members_assume_state": "\u7fa4\u7d44\u6210\u54e1\u5448\u73fe\u7fa4\u7d44\u72c0\u614b", "light_transitioning_flag": "\u958b\u555f\u71c8\u5149\u8f49\u63db\u589e\u5f37\u4eae\u5ea6\u8abf\u6574\u5217", "title": "Global \u9078\u9805" } @@ -100,6 +101,7 @@ }, "trigger_subtype": { "both_buttons": "\u5169\u500b\u6309\u9215", + "button": "\u6309\u9215", "button_1": "\u7b2c\u4e00\u500b\u6309\u9215", "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215", "button_3": "\u7b2c\u4e09\u500b\u6309\u9215", @@ -131,22 +133,22 @@ "device_shaken": "\u88dd\u7f6e\u6416\u6643", "device_slid": "\u63a8\u52d5 \"{subtype}\" \u88dd\u7f6e", "device_tilted": "\u88dd\u7f6e\u540d\u7a31", - "remote_button_alt_double_press": "\"{subtype}\" \u6309\u9215\u96d9\u64ca\u9375\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", - "remote_button_alt_long_press": "\"{subtype}\" \u6309\u9215\u6301\u7e8c\u6309\u4e0b\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", - "remote_button_alt_long_release": "\"{subtype}\" \u6309\u9215\u9577\u6309\u5f8c\u91cb\u653e\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", - "remote_button_alt_quadruple_press": "\"{subtype}\" \u6309\u9215\u56db\u9023\u64ca\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", - "remote_button_alt_quintuple_press": "\"{subtype}\" \u6309\u9215\u4e94\u9023\u64ca\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", - "remote_button_alt_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", - "remote_button_alt_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", - "remote_button_alt_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u64ca\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", - "remote_button_double_press": "\"{subtype}\" \u6309\u9215\u96d9\u64ca", - "remote_button_long_press": "\"{subtype}\" \u6309\u9215\u6301\u7e8c\u6309\u4e0b", - "remote_button_long_release": "\"{subtype}\" \u6309\u9215\u9577\u6309\u5f8c\u91cb\u653e", - "remote_button_quadruple_press": "\"{subtype}\" \u6309\u9215\u56db\u9023\u64ca", - "remote_button_quintuple_press": "\"{subtype}\" \u6309\u9215\u4e94\u9023\u64ca", - "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", - "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", - "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u64ca" + "remote_button_alt_double_press": "\"{subtype}\" \u96d9\u64ca\u9375\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", + "remote_button_alt_long_press": "\"{subtype}\" \u6301\u7e8c\u6309\u4e0b\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", + "remote_button_alt_long_release": "\"{subtype}\" \u9577\u6309\u5f8c\u91cb\u653e\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", + "remote_button_alt_quadruple_press": "\"{subtype}\" \u56db\u9023\u64ca\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", + "remote_button_alt_quintuple_press": "\"{subtype}\" \u4e94\u9023\u64ca\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", + "remote_button_alt_short_press": "\"{subtype}\" \u5df2\u6309\u4e0b\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", + "remote_button_alt_short_release": "\"{subtype}\" \u5df2\u91cb\u653e\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", + "remote_button_alt_triple_press": "\"{subtype}\" \u4e09\u9023\u64ca\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", + "remote_button_double_press": "\"{subtype}\" \u96d9\u64ca", + "remote_button_long_press": "\"{subtype}\" \u6301\u7e8c\u6309\u4e0b", + "remote_button_long_release": "\"{subtype}\" \u9577\u6309\u5f8c\u91cb\u653e", + "remote_button_quadruple_press": "\"{subtype}\" \u56db\u9023\u64ca", + "remote_button_quintuple_press": "\"{subtype}\" \u4e94\u9023\u64ca", + "remote_button_short_press": "\"{subtype}\" \u5df2\u6309\u4e0b", + "remote_button_short_release": "\"{subtype}\" \u5df2\u91cb\u653e", + "remote_button_triple_press": "\"{subtype}\" \u4e09\u9023\u64ca" } }, "options": { diff --git a/homeassistant/components/zodiac/translations/tr.json b/homeassistant/components/zodiac/translations/tr.json new file mode 100644 index 00000000000..a1024faf374 --- /dev/null +++ b/homeassistant/components/zodiac/translations/tr.json @@ -0,0 +1,22 @@ +{ + "entity": { + "sensor": { + "sign": { + "state": { + "aquarius": "Kova", + "aries": "Ko\u00e7", + "cancer": "Yenge\u00e7", + "capricorn": "O\u011flak", + "gemini": "Ikizler", + "leo": "Aslan", + "libra": "Terazi", + "pisces": "Bal\u0131k", + "sagittarius": "Yay", + "scorpio": "Akrep", + "taurus": "Bo\u011fa", + "virgo": "Ba\u015fak" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/uk.json b/homeassistant/components/zodiac/translations/uk.json new file mode 100644 index 00000000000..a3acf2e6995 --- /dev/null +++ b/homeassistant/components/zodiac/translations/uk.json @@ -0,0 +1,22 @@ +{ + "entity": { + "sensor": { + "sign": { + "state": { + "aquarius": "\u0412\u043e\u0434\u043e\u043b\u0456\u0439", + "aries": "\u041e\u0432\u0435\u043d", + "cancer": "\u0420\u0430\u043a", + "capricorn": "\u041a\u043e\u0437\u0435\u0440\u0456\u0433", + "gemini": "\u0411\u043b\u0438\u0437\u043d\u044e\u043a\u0438", + "leo": "\u041b\u0435\u0432", + "libra": "\u0412\u0430\u0433\u0438", + "pisces": "\u0420\u0438\u0431\u0438", + "sagittarius": "\u0421\u0442\u0440\u0456\u043b\u0435\u0446\u044c", + "scorpio": "\u0421\u043a\u043e\u0440\u043f\u0456\u043e\u043d", + "taurus": "\u0422\u0435\u043b\u0435\u0446\u044c", + "virgo": "\u0414\u0456\u0432\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/translations/lt.json b/homeassistant/components/zone/translations/lt.json index d7127048a63..8a3035dde0f 100644 --- a/homeassistant/components/zone/translations/lt.json +++ b/homeassistant/components/zone/translations/lt.json @@ -3,8 +3,13 @@ "step": { "init": { "data": { - "name": "Pavadinimas" - } + "latitude": "Platuma", + "longitude": "Ilguma", + "name": "Pavadinimas", + "passive": "Pasyvus", + "radius": "Spindulys" + }, + "title": "Apibr\u0117\u017eti zonos parametrus" } }, "title": "Zona" diff --git a/homeassistant/components/zone/translations/uk.json b/homeassistant/components/zone/translations/uk.json index ce082d34a1c..aedd14c6bbf 100644 --- a/homeassistant/components/zone/translations/uk.json +++ b/homeassistant/components/zone/translations/uk.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0406\u043c'\u044f \u0432\u0436\u0435 \u0456\u0441\u043d\u0443\u0454" + "name_exists": "\u041d\u0430\u0437\u0432\u0430 \u0432\u0436\u0435 \u0456\u0441\u043d\u0443\u0454" }, "step": { "init": { diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 25ca742a611..ee007a95e81 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -157,9 +157,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady(f"Invalid server version: {err}") from err except (asyncio.TimeoutError, BaseZwaveJSServerError) as err: raise ConfigEntryNotReady(f"Failed to connect: {err}") from err - else: - async_delete_issue(hass, DOMAIN, "invalid_server_version") - LOGGER.info("Connected to Zwave JS Server") + + async_delete_issue(hass, DOMAIN, "invalid_server_version") + LOGGER.info("Connected to Zwave JS Server") dev_reg = device_registry.async_get(hass) ent_reg = entity_registry.async_get(hass) diff --git a/homeassistant/components/zwave_js/diagnostics.py b/homeassistant/components/zwave_js/diagnostics.py index 068be7feb0b..50130fc2632 100644 --- a/homeassistant/components/zwave_js/diagnostics.py +++ b/homeassistant/components/zwave_js/diagnostics.py @@ -106,7 +106,7 @@ def get_device_entities( async def async_get_config_entry_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry -) -> list[dict]: +) -> dict[str, Any]: """Return diagnostics for a config entry.""" msgs: list[dict] = async_redact_data( await dump_msgs( @@ -119,12 +119,12 @@ async def async_get_config_entry_diagnostics( network_state["result"]["state"]["nodes"] = [ redact_node_state(node) for node in network_state["result"]["state"]["nodes"] ] - return [*handshake_msgs, network_state] + return {"messages": [*handshake_msgs, network_state]} async def async_get_device_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry -) -> dict: +) -> dict[str, Any]: """Return diagnostics for a device.""" client: Client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] identifiers = get_home_and_node_id_from_device_entry(device) diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py index 7d9f20b111e..82d2b9ffc44 100644 --- a/homeassistant/components/zwave_js/discovery_data_template.py +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Iterable, Mapping from dataclasses import dataclass, field import logging -from typing import Any, Union, cast +from typing import Any, cast from zwave_js_server.const import CommandClass from zwave_js_server.const.command_class.meter import ( @@ -497,7 +497,7 @@ class ConfigurableFanValueMappingDataTemplate( ) -> dict[str, ZwaveConfigurationValue | None]: """Resolve helper class data for a discovered value.""" zwave_value = cast( - Union[ZwaveConfigurationValue, None], + ZwaveConfigurationValue | None, self._get_value_from_id(value.node, self.configuration_option), ) return {"configuration_value": zwave_value} diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index 94d55dc1a2f..70434290d54 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -125,7 +125,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): CommandClass.SWITCH_COLOR, value_property_key=ColorComponent.COLD_WHITE, ) - self._supported_color_modes = set() + self._supported_color_modes: set[ColorMode] = set() # get additional (optional) values and set features self._target_brightness = self.get_zwave_value( @@ -218,7 +218,7 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): return self._max_mireds @property - def supported_color_modes(self) -> set | None: + def supported_color_modes(self) -> set[ColorMode] | None: """Flag supported features.""" return self._supported_color_modes diff --git a/homeassistant/components/zwave_js/translations/ja.json b/homeassistant/components/zwave_js/translations/ja.json index c42fff18139..642005e8871 100644 --- a/homeassistant/components/zwave_js/translations/ja.json +++ b/homeassistant/components/zwave_js/translations/ja.json @@ -92,6 +92,12 @@ "zwave_js.value_updated.value": "Z-Wave JS\u5024\u306e\u5024\u3092\u5909\u66f4" } }, + "issues": { + "invalid_server_version": { + "description": "\u73fe\u5728\u5b9f\u884c\u3057\u3066\u3044\u308b\u3001Z-Wave JS\u30b5\u30fc\u30d0\u30fc\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u306f\u3001\u3053\u306e\u30d0\u30fc\u30b8\u30e7\u30f3\u306eHome Assistant\u306b\u306f\u53e4\u3059\u304e\u307e\u3059\u3002\u3053\u306e\u554f\u984c\u3092\u4fee\u6b63\u3059\u308b\u306b\u306f\u3001Z-Wave JS\u30b5\u30fc\u30d0\u30fc\u3092\u6700\u65b0\u30d0\u30fc\u30b8\u30e7\u30f3\u306b\u66f4\u65b0\u3057\u3066\u304f\u3060\u3055\u3044\u3002", + "title": "\u65b0\u3057\u3044\u30d0\u30fc\u30b8\u30e7\u30f3\u306eZ-Wave JS\u30b5\u30fc\u30d0\u30fc\u304c\u5fc5\u8981\u3067\u3059" + } + }, "options": { "abort": { "addon_get_discovery_info_failed": "Z-Wave JS\u30a2\u30c9\u30aa\u30f3\u306e\u691c\u51fa\u60c5\u5831\u306e\u53d6\u5f97\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002", diff --git a/homeassistant/components/zwave_js/translations/lv.json b/homeassistant/components/zwave_js/translations/lv.json new file mode 100644 index 00000000000..a96da546080 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/lv.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + }, + "options": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/sk.json b/homeassistant/components/zwave_js/translations/sk.json index e8f1d0291e7..9ed9c7807b8 100644 --- a/homeassistant/components/zwave_js/translations/sk.json +++ b/homeassistant/components/zwave_js/translations/sk.json @@ -84,7 +84,7 @@ "trigger_type": { "event.notification.entry_control": "Odosla\u0165 ozn\u00e1menie o riaden\u00ed vstupu", "event.notification.notification": "Odosla\u0165 ozn\u00e1menie", - "event.value_notification.basic": "Z\u00e1kladn\u00e1 ud\u00e1los\u0165 CC na {subtype}", + "event.value_notification.basic": "Z\u00e1kladn\u00e1 udalos\u0165 CC na {subtype}", "event.value_notification.central_scene": "Akcia centr\u00e1lnej sc\u00e9ny na {subtype}", "event.value_notification.scene_activation": "Aktiv\u00e1cia sc\u00e9ny na {subtype}", "state.node_status": "Stav uzlov zmenen\u00fd", diff --git a/homeassistant/components/zwave_js/translations/tr.json b/homeassistant/components/zwave_js/translations/tr.json index ed45816c2dd..48a68fceab1 100644 --- a/homeassistant/components/zwave_js/translations/tr.json +++ b/homeassistant/components/zwave_js/translations/tr.json @@ -58,7 +58,7 @@ "title": "Z-Wave JS eklentisi ba\u015fl\u0131yor." }, "usb_confirm": { - "description": "{name} Z-Wave JS eklentisiyle kurmak istiyor musunuz?" + "description": "{name} i\u00e7in Z-Wave JS eklentisini kurmak istiyor musunuz?" }, "zeroconf_confirm": { "description": "{url} adresinde bulunan {home_id} ev kimli\u011fine sahip Z-Wave JS Sunucusunu Home Assistant'a eklemek istiyor musunuz?", diff --git a/homeassistant/components/zwave_me/__init__.py b/homeassistant/components/zwave_me/__init__.py index ed3d538d052..f47b77b29d1 100644 --- a/homeassistant/components/zwave_me/__init__.py +++ b/homeassistant/components/zwave_me/__init__.py @@ -1,5 +1,4 @@ """The Z-Wave-Me WS integration.""" -import asyncio import logging from zwave_me_ws import ZWaveMe, ZWaveMeData @@ -24,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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)) + await async_setup_platforms(hass, entry, controller) registry = device_registry.async_get(hass) controller.remove_stale_devices(registry) return True @@ -98,12 +97,7 @@ 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 - ] - ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) controller.platforms_inited = True await hass.async_add_executor_job(controller.zwave_api.get_devices) diff --git a/homeassistant/components/zwave_me/cover.py b/homeassistant/components/zwave_me/cover.py index 5e2fdba8608..790cfc6c574 100644 --- a/homeassistant/components/zwave_me/cover.py +++ b/homeassistant/components/zwave_me/cover.py @@ -73,8 +73,23 @@ class ZWaveMeCover(ZWaveMeEntity, CoverEntity): """Return current position of cover. None is unknown, 0 is closed, 100 is fully open. + + Allow small calibration errors (some devices after a long time become not well calibrated) """ - if self.device.level == 99: # Scale max value + if self.device.level > 95: return 100 return self.device.level + + @property + def is_closed(self) -> bool | None: + """Return true if cover is closed. + + None is unknown. + + Allow small calibration errors (some devices after a long time become not well calibrated) + """ + if self.device.level is None: + return None + + return self.device.level < 5 diff --git a/homeassistant/components/zwave_me/manifest.json b/homeassistant/components/zwave_me/manifest.json index 9aeeb7b2a40..9d60d14f274 100644 --- a/homeassistant/components/zwave_me/manifest.json +++ b/homeassistant/components/zwave_me/manifest.json @@ -7,5 +7,5 @@ "after_dependencies": ["zeroconf"], "zeroconf": [{ "type": "_hap._tcp.local.", "name": "*z.wave-me*" }], "config_flow": true, - "codeowners": ["@lawfulchaos", "@Z-Wave-Me"] + "codeowners": ["@lawfulchaos", "@Z-Wave-Me", "@PoltoS"] } diff --git a/homeassistant/components/zwave_me/translations/el.json b/homeassistant/components/zwave_me/translations/el.json index af8efe8ea86..95ec93f86d7 100644 --- a/homeassistant/components/zwave_me/translations/el.json +++ b/homeassistant/components/zwave_me/translations/el.json @@ -13,7 +13,7 @@ "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)." + "description": "\u0395\u03b9\u03c3\u03b1\u03b3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP \u03bc\u03b5 \u03b8\u03cd\u03c1\u03b1 \u03ba\u03b1\u03b9 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b4\u03b9\u03b1\u03ba\u03bf\u03bc\u03b9\u03c3\u03c4\u03ae Z-Way. \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 Smart Home UI > \u039c\u03b5\u03bd\u03bf\u03cd > \u03a1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 > \u03a7\u03c1\u03ae\u03c3\u03c4\u03b5\u03c2 > \u0394\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae\u03c2 > \u0394\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc API. \n\n \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf Z-Way \u03c3\u03c4\u03bf \u03c4\u03bf\u03c0\u03b9\u03ba\u03cc \u03b4\u03af\u03ba\u03c4\u03c5\u03bf:\n URL: {local_url}\n Token: {local_token} \n\n \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf Z-Way \u03bc\u03ad\u03c3\u03c9 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 find.z-wave.me:\n URL: {find_url}\n Token: {find_token} \n\n \u03a0\u03b1\u03c1\u03ac\u03b4\u03b5\u03b9\u03b3\u03bc\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf Z-Way \u03bc\u03b5 \u03c3\u03c4\u03b1\u03c4\u03b9\u03ba\u03ae \u03b4\u03b7\u03bc\u03cc\u03c3\u03b9\u03b1 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7 IP:\n URL: {remote_url}\n Token: {local_token} \n\n \u038c\u03c4\u03b1\u03bd \u03c3\u03c5\u03bd\u03b4\u03ad\u03b5\u03c3\u03c4\u03b5 \u03bc\u03ad\u03c3\u03c9 find.z-wave.me, \u03c0\u03c1\u03ad\u03c0\u03b5\u03b9 \u03bd\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03bc\u03b5 \u03ba\u03b1\u03b8\u03bf\u03bb\u03b9\u03ba\u03cc \u03b5\u03cd\u03c1\u03bf\u03c2 (\u03c3\u03c5\u03bd\u03b4\u03b5\u03b8\u03b5\u03af\u03c4\u03b5 \u03c3\u03c4\u03bf Z-Way \u03bc\u03ad\u03c3\u03c9 find.z-wave.me \u03b3\u03b9\u03b1 \u03b1\u03c5\u03c4\u03cc)." } } } diff --git a/homeassistant/components/zwave_me/translations/lv.json b/homeassistant/components/zwave_me/translations/lv.json new file mode 100644 index 00000000000..affb0efe4c3 --- /dev/null +++ b/homeassistant/components/zwave_me/translations/lv.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ier\u012bce jau pievienota Home Assistant." + } + } +} \ No newline at end of file diff --git a/homeassistant/config.py b/homeassistant/config.py index a4205461b30..283f8726e2b 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -330,7 +330,7 @@ async def async_ensure_config_exists(hass: HomeAssistant) -> bool: if os.path.isfile(config_path): return True - print( + print( # noqa: T201 "Unable to find configuration. Creating default one in", hass.config.config_dir ) return await async_create_default_config(hass) @@ -384,7 +384,7 @@ def _write_default_config(config_dir: str) -> bool: return True except OSError: - print("Unable to create default configuration file", config_path) + print("Unable to create default configuration file", config_path) # noqa: T201 return False @@ -864,8 +864,8 @@ async def async_process_component_config( # noqa: C901 config_validator, "async_validate_config" ): try: - return await config_validator.async_validate_config( # type: ignore[no-any-return] - hass, config + return ( # type: ignore[no-any-return] + await config_validator.async_validate_config(hass, config) ) except (vol.Invalid, HomeAssistantError) as ex: async_log_exception(ex, domain, config, hass, integration.documentation) @@ -976,7 +976,7 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> str | None: This method is a coroutine. """ - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .helpers import check_config res = await check_config.async_check_ha_config_file(hass) @@ -994,7 +994,7 @@ def async_notify_setup_error( This method must be run in the event loop. """ - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .components import persistent_notification if (errors := hass.data.get(DATA_PERSISTENT_ERRORS)) is None: diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index f2e2be97e42..e127476073d 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -11,7 +11,7 @@ import functools import logging from random import randint from types import MappingProxyType, MethodType -from typing import TYPE_CHECKING, Any, Optional, TypeVar, cast +from typing import TYPE_CHECKING, Any, TypeVar, cast import weakref from . import data_entry_flow, loader @@ -63,14 +63,14 @@ SOURCE_USB = "usb" SOURCE_USER = "user" SOURCE_ZEROCONF = "zeroconf" -# 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 -# with the same unique id are ignored. +# 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 with the same unique id are ignored. SOURCE_IGNORE = "ignore" # This is used when a user uses the "Stop Ignoring" button in the UI (the -# config_entries/ignore_flow websocket command). It's triggered after the "ignore" config entry has -# been removed and unloaded. +# config_entries/ignore_flow websocket command). It's triggered after the +# "ignore" config entry has been removed and unloaded. SOURCE_UNIGNORE = "unignore" # This is used to signal that re-authentication is required by the user. @@ -627,7 +627,7 @@ class ConfigEntry: ) return False if result: - # pylint: disable=protected-access + # pylint: disable-next=protected-access hass.config_entries._async_schedule_save() # https://github.com/python/mypy/issues/11839 return result # type: ignore[no-any-return] @@ -643,7 +643,8 @@ class ConfigEntry: Returns function to unlisten. """ weak_listener: Any - # weakref.ref is not applicable to a bound method, e.g. method of a class instance, as reference will die immediately + # weakref.ref is not applicable to a bound method, e.g., + # method of a class instance, as reference will die immediately. if hasattr(listener, "__self__"): weak_listener = weakref.WeakMethod(cast(MethodType, listener)) else: @@ -760,6 +761,15 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): super().__init__(hass) self.config_entries = config_entries self._hass_config = hass_config + self._pending_import_flows: dict[str, dict[str, asyncio.Future[None]]] = {} + self._initialize_tasks: dict[str, list[asyncio.Task]] = {} + + async def async_wait_import_flow_initialized(self, handler: str) -> None: + """Wait till all import flows in progress are initialized.""" + if not (current := self._pending_import_flows.get(handler)): + return + + await asyncio.wait(current.values()) @callback def _async_has_other_discovery_flows(self, flow_id: str) -> bool: @@ -769,12 +779,77 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): for flow in self._progress.values() ) + async def async_init( + self, handler: str, *, context: dict[str, Any] | None = None, data: Any = None + ) -> FlowResult: + """Start a configuration flow.""" + if not context or "source" not in context: + raise KeyError("Context not set or doesn't have a source set") + + flow_id = uuid_util.random_uuid_hex() + if context["source"] == SOURCE_IMPORT: + init_done: asyncio.Future[None] = asyncio.Future() + self._pending_import_flows.setdefault(handler, {})[flow_id] = init_done + + task = asyncio.create_task(self._async_init(flow_id, handler, context, data)) + self._initialize_tasks.setdefault(handler, []).append(task) + + try: + flow, result = await task + finally: + self._initialize_tasks[handler].remove(task) + self._pending_import_flows.get(handler, {}).pop(flow_id, None) + + if result["type"] != data_entry_flow.FlowResultType.ABORT: + await self.async_post_init(flow, result) + + return result + + async def _async_init( + self, + flow_id: str, + handler: str, + context: dict, + data: Any, + ) -> tuple[data_entry_flow.FlowHandler, FlowResult]: + """Run the init in a task to allow it to be canceled at shutdown.""" + flow = await self.async_create_flow(handler, context=context, data=data) + if not flow: + raise data_entry_flow.UnknownFlow("Flow was not created") + flow.hass = self.hass + flow.handler = handler + flow.flow_id = flow_id + flow.context = context + flow.init_data = data + self._async_add_flow_progress(flow) + try: + result = await self._async_handle_step(flow, flow.init_step, data) + finally: + init_done = self._pending_import_flows.get(handler, {}).get(flow_id) + if init_done and not init_done.done(): + init_done.set_result(None) + return flow, result + + async def async_shutdown(self) -> None: + """Cancel any initializing flows.""" + for task_list in self._initialize_tasks.values(): + for task in task_list: + task.cancel() + async def async_finish_flow( self, flow: data_entry_flow.FlowHandler, result: data_entry_flow.FlowResult ) -> data_entry_flow.FlowResult: """Finish a config flow and add an entry.""" flow = cast(ConfigFlow, flow) + # Mark the step as done. + # We do this to avoid a circular dependency where async_finish_flow sets up a + # new entry, which needs the integration to be set up, which is waiting for + # init to be done. + init_done = self._pending_import_flows.get(flow.handler, {}).get(flow.flow_id) + if init_done and not init_done.done(): + init_done.set_result(None) + # Remove notification if no other discovery config entries in progress if not self._async_has_other_discovery_flows(flow.flow_id): persistent_notification.async_dismiss(self.hass, DISCOVERY_NOTIFICATION_ID) @@ -994,10 +1069,10 @@ class ConfigEntries: ): self.hass.config_entries.flow.async_abort(progress_flow["flow_id"]) - # After we have fully removed an "ignore" config entry we can try and rediscover it so that a - # user is able to immediately start configuring it. We do this by starting a new flow with - # the 'unignore' step. If the integration doesn't implement async_step_unignore then - # this will be a no-op. + # After we have fully removed an "ignore" config entry we can try and rediscover + # it so that a user is able to immediately start configuring it. We do this by + # starting a new flow with the 'unignore' step. If the integration doesn't + # implement async_step_unignore then this will be a no-op. if entry.source == SOURCE_IGNORE: self.hass.async_create_task( self.hass.config_entries.flow.async_init( @@ -1040,7 +1115,8 @@ class ConfigEntries: for entry in config["entries"]: pref_disable_new_entities = entry.get("pref_disable_new_entities") - # Between 0.98 and 2021.6 we stored 'disable_new_entities' in a system options dictionary + # Between 0.98 and 2021.6 we stored 'disable_new_entities' in a + # system options dictionary. if pref_disable_new_entities is None and "system_options" in entry: pref_disable_new_entities = entry.get("system_options", {}).get( "disable_new_entities" @@ -1100,7 +1176,9 @@ class ConfigEntries: if not result: return result - return entry.state is ConfigEntryState.LOADED # type: ignore[comparison-overlap] # mypy bug? + return ( + entry.state is ConfigEntryState.LOADED # type: ignore[comparison-overlap] + ) async def async_unload(self, entry_id: str) -> bool: """Unload a config entry.""" @@ -1249,10 +1327,10 @@ class ConfigEntries: report( ( "called async_setup_platforms instead of awaiting" - " async_forward_entry_setups; this will fail in version 2022.12" + " async_forward_entry_setups; this will fail in version 2023.3" ), # Raise this to warning once all core integrations have been migrated - level=logging.DEBUG, + level=logging.WARNING, error_if_core=False, ) for platform in platforms: @@ -1359,7 +1437,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): if not self.context: return None - return cast(Optional[str], self.context.get("unique_id")) + return cast(str | None, self.context.get("unique_id")) @staticmethod @callback @@ -1377,12 +1455,19 @@ class ConfigFlow(data_entry_flow.FlowHandler): def _async_abort_entries_match( self, match_dict: dict[str, Any] | None = None ) -> None: - """Abort if current entries match all data.""" + """Abort if current entries match all data. + + Requires `already_configured` in strings.json in user visible flows. + """ if match_dict is None: match_dict = {} # Match any entry for entry in self._async_current_entries(include_ignore=False): if all( - item in ChainMap(entry.options, entry.data).items() # type: ignore[arg-type] + item + in ChainMap( + entry.options, # type: ignore[arg-type] + entry.data, # type: ignore[arg-type] + ).items() for item in match_dict.items() ): raise data_entry_flow.AbortFlow("already_configured") @@ -1395,7 +1480,11 @@ class ConfigFlow(data_entry_flow.FlowHandler): *, error: str = "already_configured", ) -> None: - """Abort if the unique ID is already configured.""" + """Abort if the unique ID is already configured. + + Requires strings.json entry corresponding to the `error` parameter + in user visible flows. + """ if self.unique_id is None: return @@ -1474,7 +1563,8 @@ class ConfigFlow(data_entry_flow.FlowHandler): ) -> list[ConfigEntry]: """Return current entries. - If the flow is user initiated, filter out ignored entries unless include_ignore is True. + If the flow is user initiated, filter out ignored entries, + unless include_ignore is True. """ config_entries = self.hass.config_entries.async_entries(self.handler) @@ -1536,6 +1626,9 @@ class ConfigFlow(data_entry_flow.FlowHandler): when the handler has no existing config entries. It ensures that the discovery can be ignored by the user. + + Requires `already_configured` and `already_in_progress` in strings.json + in user visible flows. """ if self.unique_id is not None: return @@ -1737,7 +1830,7 @@ class OptionsFlowWithConfigEntry(OptionsFlow): class EntityRegistryDisabledHandler: - """Handler to handle when entities related to config entries updating disabled_by.""" + """Handler when entities related to config entries updated disabled_by.""" def __init__(self, hass: HomeAssistant) -> None: """Initialize the handler.""" diff --git a/homeassistant/const.py b/homeassistant/const.py index c738d7fca6d..ee5d60a32be 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -7,14 +7,14 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 -MINOR_VERSION: Final = 1 -PATCH_VERSION: Final = "7" +MINOR_VERSION: Final = 2 +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" -REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) +REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) REQUIRED_NEXT_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) # Truthy date string triggers showing related deprecation warning messages. -REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "2023.2" +REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = "" # Format for platform files PLATFORM_FORMAT: Final = "{platform}.{domain}" @@ -517,6 +517,7 @@ class UnitOfEnergy(StrEnum): GIGA_JOULE = "GJ" KILO_WATT_HOUR = "kWh" + MEGA_JOULE = "MJ" MEGA_WATT_HOUR = "MWh" WATT_HOUR = "Wh" @@ -1031,6 +1032,13 @@ DATA_RATE_GIBIBYTES_PER_SECOND: Final = "GiB/s" """Deprecated: please use UnitOfDataRate.GIBIBYTES_PER_SECOND""" +# States +COMPRESSED_STATE_STATE = "s" +COMPRESSED_STATE_ATTRIBUTES = "a" +COMPRESSED_STATE_CONTEXT = "c" +COMPRESSED_STATE_LAST_CHANGED = "lc" +COMPRESSED_STATE_LAST_UPDATED = "lu" + # #### SERVICES #### SERVICE_HOMEASSISTANT_STOP: Final = "stop" SERVICE_HOMEASSISTANT_RESTART: Final = "restart" diff --git a/homeassistant/core.py b/homeassistant/core.py index fee2e482e91..97ffd3e7fe0 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -31,15 +31,13 @@ from typing import ( Any, Generic, NamedTuple, - Optional, + ParamSpec, TypeVar, - Union, cast, overload, ) from urllib.parse import urlparse -from typing_extensions import ParamSpec import voluptuous as vol import yarl @@ -50,6 +48,11 @@ from .const import ( ATTR_FRIENDLY_NAME, ATTR_SERVICE, ATTR_SERVICE_DATA, + COMPRESSED_STATE_ATTRIBUTES, + COMPRESSED_STATE_CONTEXT, + COMPRESSED_STATE_LAST_CHANGED, + COMPRESSED_STATE_LAST_UPDATED, + COMPRESSED_STATE_STATE, EVENT_CALL_SERVICE, EVENT_CORE_CONFIG_UPDATE, EVENT_HOMEASSISTANT_CLOSE, @@ -330,7 +333,7 @@ class HomeAssistant: await self.async_start() if attach_signals: - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .helpers.signal import async_register_signal_handling async_register_signal_handling(self) @@ -443,7 +446,7 @@ class HomeAssistant: # the type used for the cast. For history see: # https://github.com/home-assistant/core/pull/71960 if TYPE_CHECKING: - target = cast(Callable[..., Union[Coroutine[Any, Any, _R], _R]], target) + target = cast(Callable[..., Coroutine[Any, Any, _R] | _R], target) return self.async_add_hass_job(HassJob(target), *args) @overload @@ -621,7 +624,7 @@ class HomeAssistant: # the type used for the cast. For history see: # https://github.com/home-assistant/core/pull/71960 if TYPE_CHECKING: - target = cast(Callable[..., Union[Coroutine[Any, Any, _R], _R]], target) + target = cast(Callable[..., Coroutine[Any, Any, _R] | _R], target) return self.async_run_hass_job(HassJob(target), *args) def block_till_done(self) -> None: @@ -1115,6 +1118,7 @@ class State: "domain", "object_id", "_as_dict", + "_as_compressed_state", ] def __init__( @@ -1150,6 +1154,7 @@ class State: self.context = context or Context() self.domain, self.object_id = split_entity_id(self.entity_id) self._as_dict: ReadOnlyDict[str, Collection[Any]] | None = None + self._as_compressed_state: dict[str, Any] | None = None def __hash__(self) -> int: """Make the state hashable. @@ -1191,6 +1196,33 @@ class State: ) return self._as_dict + def as_compressed_state(self) -> dict[str, Any]: + """Build a compressed dict of a state for adds. + + Omits the lu (last_updated) if it matches (lc) last_changed. + + Sends c (context) as a string if it only contains an id. + """ + if self._as_compressed_state: + return self._as_compressed_state + state_context = self.context + if state_context.parent_id is None and state_context.user_id is None: + context: dict[str, Any] | str = state_context.id + else: + context = state_context.as_dict() + compressed_state = { + COMPRESSED_STATE_STATE: self.state, + COMPRESSED_STATE_ATTRIBUTES: self.attributes, + COMPRESSED_STATE_CONTEXT: context, + COMPRESSED_STATE_LAST_CHANGED: dt_util.utc_to_timestamp(self.last_changed), + } + if self.last_changed != self.last_updated: + compressed_state[COMPRESSED_STATE_LAST_UPDATED] = dt_util.utc_to_timestamp( + self.last_updated + ) + self._as_compressed_state = compressed_state + return compressed_state + @classmethod def from_dict(cls: type[_StateT], json_dict: dict[str, Any]) -> _StateT | None: """Initialize a state from a dict. @@ -1977,13 +2009,13 @@ class Config: if time_zone is not None: self.set_time_zone(time_zone) if external_url is not _UNDEF: - self.external_url = cast(Optional[str], external_url) + self.external_url = cast(str | None, external_url) if internal_url is not _UNDEF: - self.internal_url = cast(Optional[str], internal_url) + self.internal_url = cast(str | None, internal_url) if currency is not None: self.currency = currency if country is not _UNDEF: - self.country = cast(Optional[str], country) + self.country = cast(str | None, country) if language is not None: self.language = language @@ -2007,8 +2039,8 @@ class Config: if not (data := await self._store.async_load()): return - # In 2021.9 we fixed validation to disallow a path (because that's never correct) - # but this data still lives in storage, so we print a warning. + # In 2021.9 we fixed validation to disallow a path (because that's never + # correct) but this data still lives in storage, so we print a warning. if data.get("external_url") and urlparse(data["external_url"]).path not in ( "", "/", @@ -2091,7 +2123,8 @@ class Config: if data["unit_system_v2"] == _CONF_UNIT_SYSTEM_IMPERIAL: data["unit_system_v2"] = _CONF_UNIT_SYSTEM_US_CUSTOMARY if old_major_version == 1 and old_minor_version < 3: - # In 1.3, we add the key "language", initialize it from the owner account + # In 1.3, we add the key "language", initialize it from the + # owner account. data["language"] = "en" try: owner = await self.hass.auth.async_get_owner() diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index a98c616ffbc..ebe67e47103 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -2,7 +2,6 @@ from __future__ import annotations import abc -import asyncio from collections.abc import Iterable, Mapping import copy from dataclasses import dataclass @@ -10,6 +9,7 @@ import logging from types import MappingProxyType from typing import Any, TypedDict +from typing_extensions import Required import voluptuous as vol from .backports.enum import StrEnum @@ -54,7 +54,7 @@ class BaseServiceInfo: class FlowError(HomeAssistantError): - """Error while configuring an account.""" + """Base class for data entry errors.""" class UnknownHandler(FlowError): @@ -91,8 +91,8 @@ class FlowResult(TypedDict, total=False): description: str | None errors: dict[str, str] | None extra: str - flow_id: str - handler: str + flow_id: Required[str] + handler: Required[str] last_step: bool | None menu_options: list[str] | dict[str, str] options: Mapping[str, Any] @@ -136,18 +136,9 @@ class FlowManager(abc.ABC): ) -> None: """Initialize the flow manager.""" self.hass = hass - self._initializing: dict[str, list[asyncio.Future]] = {} - self._initialize_tasks: dict[str, list[asyncio.Task]] = {} self._progress: dict[str, FlowHandler] = {} self._handler_progress_index: dict[str, set[str]] = {} - async def async_wait_init_flow_finish(self, handler: str) -> None: - """Wait till all flows in progress are initialized.""" - if not (current := self._initializing.get(handler)): - return - - await asyncio.wait(current) - @abc.abstractmethod async def async_create_flow( self, @@ -165,7 +156,7 @@ class FlowManager(abc.ABC): async def async_finish_flow( self, flow: FlowHandler, result: FlowResult ) -> FlowResult: - """Finish a config flow and add an entry.""" + """Finish a data entry flow.""" async def async_post_init(self, flow: FlowHandler, result: FlowResult) -> None: """Entry has finished executing its first step asynchronously.""" @@ -174,7 +165,10 @@ class FlowManager(abc.ABC): def async_has_matching_flow( self, handler: str, context: dict[str, Any], data: Any ) -> bool: - """Check if an existing matching flow is in progress with the same handler, context, and data.""" + """Check if an existing matching flow is in progress. + + A flow with the same handler, context, and data. + """ return any( flow for flow in self._async_progress_by_handler(handler) @@ -215,35 +209,9 @@ class FlowManager(abc.ABC): async def async_init( self, handler: str, *, context: dict[str, Any] | None = None, data: Any = None ) -> FlowResult: - """Start a configuration flow.""" + """Start a data entry flow.""" if context is None: context = {} - - init_done: asyncio.Future = asyncio.Future() - self._initializing.setdefault(handler, []).append(init_done) - - task = asyncio.create_task(self._async_init(init_done, handler, context, data)) - self._initialize_tasks.setdefault(handler, []).append(task) - - try: - flow, result = await task - finally: - self._initialize_tasks[handler].remove(task) - self._initializing[handler].remove(init_done) - - if result["type"] != FlowResultType.ABORT: - await self.async_post_init(flow, result) - - return result - - async def _async_init( - self, - init_done: asyncio.Future, - handler: str, - context: dict, - data: Any, - ) -> tuple[FlowHandler, FlowResult]: - """Run the init in a task to allow it to be canceled at shutdown.""" flow = await self.async_create_flow(handler, context=context, data=data) if not flow: raise UnknownFlow("Flow was not created") @@ -253,19 +221,18 @@ class FlowManager(abc.ABC): flow.context = context flow.init_data = data self._async_add_flow_progress(flow) - result = await self._async_handle_step(flow, flow.init_step, data, init_done) - return flow, result - async def async_shutdown(self) -> None: - """Cancel any initializing flows.""" - for task_list in self._initialize_tasks.values(): - for task in task_list: - task.cancel() + result = await self._async_handle_step(flow, flow.init_step, data) + + if result["type"] != FlowResultType.ABORT: + await self.async_post_init(flow, result) + + return result async def async_configure( self, flow_id: str, user_input: dict | None = None ) -> FlowResult: - """Continue a configuration flow.""" + """Continue a data entry flow.""" if (flow := self._progress.get(flow_id)) is None: raise UnknownFlow @@ -350,22 +317,16 @@ class FlowManager(abc.ABC): try: flow.async_remove() except Exception as err: # pylint: disable=broad-except - _LOGGER.exception("Error removing %s config flow: %s", flow.handler, err) + _LOGGER.exception("Error removing %s flow: %s", flow.handler, err) async def _async_handle_step( - self, - flow: FlowHandler, - step_id: str, - user_input: dict | BaseServiceInfo | None, - step_done: asyncio.Future | None = None, + self, flow: FlowHandler, step_id: str, user_input: dict | BaseServiceInfo | None ) -> FlowResult: """Handle a step of a flow.""" method = f"async_step_{step_id}" if not hasattr(flow, method): self._async_remove_flow_progress(flow.flow_id) - if step_done: - step_done.set_result(None) raise UnknownStep( f"Handler {flow.__class__.__name__} doesn't support step {step_id}" ) @@ -377,13 +338,6 @@ class FlowManager(abc.ABC): flow.flow_id, flow.handler, err.reason, err.description_placeholders ) - # Mark the step as done. - # We do this before calling async_finish_flow because config entries will hit a - # circular dependency where async_finish_flow sets up new entry, which needs the - # integration to be set up, which is waiting for init to be done. - if step_done: - step_done.set_result(None) - if not isinstance(result["type"], FlowResultType): result["type"] = FlowResultType(result["type"]) # type: ignore[unreachable] report( @@ -420,7 +374,7 @@ class FlowManager(abc.ABC): class FlowHandler: - """Handle the configuration flow of a component.""" + """Handle a data entry flow.""" # Set by flow manager cur_step: FlowResult | None = None @@ -453,7 +407,7 @@ class FlowHandler: return self.context.get("show_advanced_options", False) def add_suggested_values_to_schema( - self, data_schema: vol.Schema, suggested_values: Mapping[str, Any] + self, data_schema: vol.Schema, suggested_values: Mapping[str, Any] | None ) -> vol.Schema: """Make a copy of the schema, populated with suggested values. @@ -473,7 +427,11 @@ class FlowHandler: continue new_key = key - if key in suggested_values and isinstance(key, vol.Marker): + if ( + suggested_values + and key in suggested_values + and isinstance(key, vol.Marker) + ): # Copy the marker to not modify the flow schema new_key = copy.copy(key) new_key.description = {"suggested_value": suggested_values[key]} @@ -511,7 +469,7 @@ class FlowHandler: description: str | None = None, description_placeholders: Mapping[str, str] | None = None, ) -> FlowResult: - """Finish config flow and create a config entry.""" + """Finish flow.""" flow_result = FlowResult( version=self.VERSION, type=FlowResultType.CREATE_ENTRY, @@ -533,7 +491,7 @@ class FlowHandler: reason: str, description_placeholders: Mapping[str, str] | None = None, ) -> FlowResult: - """Abort the config flow.""" + """Abort the flow.""" return _create_abort_data( self.flow_id, self.handler, reason, description_placeholders ) @@ -618,7 +576,7 @@ class FlowHandler: @callback def async_remove(self) -> None: - """Notification that the config flow has been removed.""" + """Notification that the flow has been removed.""" @callback diff --git a/homeassistant/generated/application_credentials.py b/homeassistant/generated/application_credentials.py index 87813c20189..b15642d46e1 100644 --- a/homeassistant/generated/application_credentials.py +++ b/homeassistant/generated/application_credentials.py @@ -7,6 +7,7 @@ APPLICATION_CREDENTIALS = [ "geocaching", "google", "google_assistant_sdk", + "google_mail", "google_sheets", "home_connect", "lametric", diff --git a/homeassistant/generated/bluetooth.py b/homeassistant/generated/bluetooth.py index dc50434f63f..efc05ff2831 100644 --- a/homeassistant/generated/bluetooth.py +++ b/homeassistant/generated/bluetooth.py @@ -42,6 +42,26 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "bthome", "service_data_uuid": "0000fcd2-0000-1000-8000-00805f9b34fb", }, + { + "domain": "eufylife_ble", + "local_name": "eufy T9140", + }, + { + "domain": "eufylife_ble", + "local_name": "eufy T9146", + }, + { + "domain": "eufylife_ble", + "local_name": "eufy T9147", + }, + { + "domain": "eufylife_ble", + "local_name": "eufy T9148", + }, + { + "domain": "eufylife_ble", + "local_name": "eufy T9149", + }, { "connectable": False, "domain": "fjaraskupan", @@ -201,6 +221,10 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "keymitt_ble", "local_name": "mib*", }, + { + "domain": "ld2410_ble", + "local_name": "HLK-LD2410B_*", + }, { "domain": "led_ble", "local_name": "LEDnet*", @@ -245,6 +269,24 @@ BLUETOOTH: list[dict[str, bool | str | int | list[int]]] = [ "domain": "moat", "local_name": "Moat_S*", }, + { + "connectable": False, + "domain": "mopeka", + "manufacturer_data_start": [ + 3, + ], + "manufacturer_id": 89, + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + }, + { + "connectable": False, + "domain": "mopeka", + "manufacturer_data_start": [ + 8, + ], + "manufacturer_id": 89, + "service_uuid": "0000fee5-0000-1000-8000-00805f9b34fb", + }, { "domain": "oralb", "manufacturer_id": 220, diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 980f7f1897e..22fd40c5101 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -34,7 +34,6 @@ FLOWS = { "airzone", "aladdin_connect", "alarmdecoder", - "almond", "amberelectric", "ambiclimate", "ambient_station", @@ -93,6 +92,7 @@ FLOWS = { "dialogflow", "directv", "discord", + "dlink", "dlna_dmr", "dlna_dms", "dnsip", @@ -112,12 +112,14 @@ FLOWS = { "elmax", "emonitor", "emulated_roku", + "energyzero", "enocean", "enphase_envoy", "environment_canada", "epson", "escea", "esphome", + "eufylife_ble", "evil_genius_labs", "ezviz", "faa_delays", @@ -156,6 +158,7 @@ FLOWS = { "goodwe", "google", "google_assistant_sdk", + "google_mail", "google_sheets", "google_travel_time", "govee_ble", @@ -189,6 +192,7 @@ FLOWS = { "ibeacon", "icloud", "ifttt", + "imap", "inkbird", "insteon", "intellifire", @@ -220,6 +224,7 @@ FLOWS = { "landisgyr_heat_meter", "launch_library", "laundrify", + "ld2410_ble", "led_ble", "lg_soundbar", "lidarr", @@ -258,6 +263,7 @@ FLOWS = { "moehlenhoff_alpha2", "monoprice", "moon", + "mopeka", "motion_blinds", "motioneye", "mqtt", @@ -292,12 +298,14 @@ FLOWS = { "onewire", "onvif", "open_meteo", + "openai_conversation", "openexchangerates", "opengarage", "opentherm_gw", "openuv", "openweathermap", "oralb", + "otbr", "overkiz", "ovo_energy", "owntracks", @@ -331,6 +339,7 @@ FLOWS = { "radarr", "radio_browser", "radiotherm", + "rainbird", "rainforest_eagle", "rainmachine", "rdw", @@ -349,7 +358,9 @@ FLOWS = { "rpi_power", "rtsp_to_webrtc", "ruckus_unleashed", + "ruuvi_gateway", "ruuvitag_ble", + "rympro", "sabnzbd", "samsungtv", "scrape", @@ -363,6 +374,7 @@ FLOWS = { "sensorpush", "sentry", "senz", + "sfr_box", "sharkiq", "shelly", "shopping_list", @@ -397,9 +409,11 @@ FLOWS = { "squeezebox", "srp_energy", "starline", + "starlink", "steam_online", "steamist", "stookalert", + "stookwijzer", "subaru", "sun", "surepetcare", @@ -419,6 +433,7 @@ FLOWS = { "tesla_wall_connector", "thermobeacon", "thermopro", + "thread", "tibber", "tile", "tilt_ble", @@ -486,6 +501,7 @@ FLOWS = { "youless", "zamg", "zerproc", + "zeversolar", "zha", "zwave_js", "zwave_me", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index f04fb56e32a..8956085a5ab 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -97,6 +97,10 @@ DHCP: list[dict[str, str | bool]] = [ "domain": "broadlink", "macaddress": "C8F742*", }, + { + "domain": "dlink", + "hostname": "dsp-w215", + }, { "domain": "elkm1", "registered_devices": True, @@ -239,6 +243,11 @@ DHCP: list[dict[str, str | bool]] = [ "hostname": "isy*", "macaddress": "0021B9*", }, + { + "domain": "isy994", + "hostname": "eisy*", + "macaddress": "0021B9*", + }, { "domain": "isy994", "hostname": "polisy*", @@ -375,6 +384,11 @@ DHCP: list[dict[str, str | bool]] = [ "domain": "rainforest_eagle", "macaddress": "D8D5B9*", }, + { + "domain": "reolink", + "hostname": "reolink*", + "macaddress": "EC71DB*", + }, { "domain": "ring", "hostname": "ring*", @@ -400,6 +414,10 @@ DHCP: list[dict[str, str | bool]] = [ "hostname": "roomba-*", "macaddress": "204EF6*", }, + { + "domain": "ruuvi_gateway", + "hostname": "ruuvigateway*", + }, { "domain": "samsungtv", "registered_devices": True, diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 40d6edc0d49..8ceabb723f8 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -158,12 +158,6 @@ "config_flow": false, "iot_class": "local_push" }, - "almond": { - "name": "Almond", - "integration_type": "hub", - "config_flow": true, - "iot_class": "local_polling" - }, "alpha_vantage": { "name": "Alpha Vantage", "integration_type": "hub", @@ -191,6 +185,12 @@ "iot_class": "cloud_push", "name": "Amazon Web Services (AWS)" }, + "fire_tv": { + "integration_type": "virtual", + "config_flow": false, + "supported_by": "androidtv", + "name": "Amazon Fire TV" + }, "route53": { "integration_type": "hub", "config_flow": false, @@ -258,6 +258,11 @@ "config_flow": true, "iot_class": "local_push" }, + "anwb_energie": { + "name": "ANWB Energie", + "integration_type": "virtual", + "supported_by": "energyzero" + }, "apache_kafka": { "name": "Apache Kafka", "integration_type": "hub", @@ -1083,8 +1088,8 @@ }, "dlink": { "name": "D-Link Wi-Fi Smart Plugs", - "integration_type": "hub", - "config_flow": false, + "integration_type": "device", + "config_flow": true, "iot_class": "local_polling" }, "dlna": { @@ -1363,6 +1368,17 @@ "config_flow": true, "iot_class": "local_push" }, + "energie_vanons": { + "name": "Energie VanOns", + "integration_type": "virtual", + "supported_by": "energyzero" + }, + "energyzero": { + "name": "EnergyZero", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "enigma2": { "name": "Enigma2 (OpenWebif)", "integration_type": "hub", @@ -1459,9 +1475,20 @@ }, "eufy": { "name": "eufy", - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_polling" + "integrations": { + "eufy": { + "integration_type": "hub", + "config_flow": false, + "iot_class": "local_polling", + "name": "EufyHome" + }, + "eufylife_ble": { + "integration_type": "device", + "config_flow": true, + "iot_class": "local_push", + "name": "EufyLife" + } + } }, "everlights": { "name": "EverLights", @@ -1979,6 +2006,12 @@ "iot_class": "cloud_polling", "name": "Google Domains" }, + "google_mail": { + "integration_type": "service", + "config_flow": true, + "iot_class": "cloud_polling", + "name": "Google Mail" + }, "google_maps": { "integration_type": "hub", "config_flow": false, @@ -2422,7 +2455,7 @@ "imap": { "name": "IMAP", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "cloud_push" }, "imap_email_content": { @@ -2543,7 +2576,7 @@ "iot_class": "cloud_polling" }, "isy994": { - "name": "Universal Devices ISY994", + "name": "Universal Devices ISY/IoX", "integration_type": "hub", "config_flow": true, "iot_class": "local_push" @@ -2656,6 +2689,12 @@ "config_flow": false, "iot_class": "local_push" }, + "kitchen_sink": { + "name": "Everything but the Kitchen Sink", + "integration_type": "hub", + "config_flow": false, + "iot_class": "calculated" + }, "kiwi": { "name": "KIWI", "integration_type": "hub", @@ -2764,6 +2803,12 @@ "config_flow": false, "iot_class": "local_push" }, + "ld2410_ble": { + "name": "LD2410 BLE", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_push" + }, "led_ble": { "name": "LED BLE", "integration_type": "hub", @@ -3240,6 +3285,11 @@ "config_flow": false, "iot_class": "local_polling" }, + "mijndomein_energie": { + "name": "Mijndomein Energie", + "integration_type": "virtual", + "supported_by": "energyzero" + }, "mikrotik": { "name": "Mikrotik", "integration_type": "hub", @@ -3333,6 +3383,12 @@ "config_flow": true, "iot_class": "local_polling" }, + "mopeka": { + "name": "Mopeka", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_push" + }, "motion_blinds": { "name": "Motion Blinds", "integration_type": "hub", @@ -3778,18 +3834,18 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "openai_conversation": { + "name": "OpenAI Conversation", + "integration_type": "service", + "config_flow": true, + "iot_class": "cloud_polling" + }, "openalpr_cloud": { "name": "OpenALPR Cloud", "integration_type": "hub", "config_flow": false, "iot_class": "cloud_push" }, - "openalpr_local": { - "name": "OpenALPR Local", - "integration_type": "hub", - "config_flow": false, - "iot_class": "local_push" - }, "opencv": { "name": "OpenCV", "integration_type": "hub", @@ -3915,6 +3971,12 @@ "config_flow": false, "iot_class": "local_polling" }, + "otbr": { + "name": "Open Thread Border Router", + "integration_type": "service", + "config_flow": true, + "iot_class": "local_polling" + }, "otp": { "name": "One-Time Password (OTP)", "integration_type": "hub", @@ -4314,7 +4376,7 @@ "rainbird": { "name": "Rain Bird", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "local_polling" }, "rainforest_eagle": { @@ -4569,12 +4631,24 @@ } } }, + "ruuvi_gateway": { + "name": "Ruuvi Gateway", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "ruuvitag_ble": { "name": "RuuviTag BLE", "integration_type": "hub", "config_flow": true, "iot_class": "local_push" }, + "rympro": { + "name": "Read Your Meter Pro", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "sabnzbd": { "name": "SABnzbd", "integration_type": "hub", @@ -4740,6 +4814,12 @@ "config_flow": false, "iot_class": "cloud_polling" }, + "sfr_box": { + "name": "SFR Box", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_polling" + }, "sharkiq": { "name": "Shark IQ", "integration_type": "hub", @@ -5127,6 +5207,12 @@ "config_flow": false, "iot_class": "cloud_polling" }, + "starlink": { + "name": "Starlink", + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_polling" + }, "startca": { "name": "Start.ca", "integration_type": "hub", @@ -5169,6 +5255,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "stookwijzer": { + "name": "Stookwijzer", + "integration_type": "service", + "config_flow": true, + "iot_class": "cloud_polling" + }, "streamlabswater": { "name": "StreamLabs", "integration_type": "hub", @@ -5232,7 +5324,7 @@ "name": "SwitchBee", "integration_type": "hub", "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_push" }, "switchbot": { "name": "SwitchBot", @@ -5491,6 +5583,12 @@ "config_flow": false, "iot_class": "local_polling" }, + "thread": { + "name": "Thread", + "integration_type": "service", + "config_flow": true, + "iot_class": "local_polling" + }, "tibber": { "name": "Tibber", "integration_type": "hub", @@ -5561,7 +5659,7 @@ "name": "Torque", "integration_type": "hub", "config_flow": false, - "iot_class": "cloud_polling" + "iot_class": "local_push" }, "totalconnect": { "name": "Total Connect", @@ -6012,7 +6110,7 @@ "iot_class": "local_push" }, "whirlpool": { - "name": "Whirlpool Sixth Sense", + "name": "Whirlpool Appliances", "integration_type": "hub", "config_flow": true, "iot_class": "cloud_push" @@ -6282,6 +6380,12 @@ "config_flow": false, "iot_class": "cloud_polling" }, + "zeversolar": { + "name": "Zeversolar", + "integration_type": "device", + "config_flow": true, + "iot_class": "local_polling" + }, "zha": { "name": "Zigbee Home Automation", "integration_type": "hub", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 6599136b747..ca6a22e85d6 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -155,20 +155,6 @@ SSDP = { "manufacturer": "SOYEA TECHNOLOGY CO., LTD.", }, ], - "hue": [ - { - "manufacturer": "Royal Philips Electronics", - "modelName": "Philips hue bridge 2012", - }, - { - "manufacturer": "Royal Philips Electronics", - "modelName": "Philips hue bridge 2015", - }, - { - "manufacturer": "Signify", - "modelName": "Philips hue bridge 2015", - }, - ], "hyperion": [ { "manufacturer": "Hyperion Open Source Ambient Lighting", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index e7c8bdaf1df..6032ce4bf7d 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -282,6 +282,12 @@ ZEROCONF = { "domain": "shelly", "name": "shelly*", }, + { + "domain": "synology_dsm", + "properties": { + "vendor": "synology*", + }, + }, ], "_hue._tcp.local.": [ { diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 2558b5d0896..0437dfc4e84 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -123,10 +123,15 @@ def _async_create_clientsession( # It's important that we identify as Home Assistant # 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[assignment] + # pylint: disable-next=protected-access + clientsession._default_headers = MappingProxyType( # type: ignore[assignment] + {USER_AGENT: SERVER_SOFTWARE}, + ) - clientsession.close = warn_use(clientsession.close, WARN_CLOSE_MSG) # type: ignore[assignment] + clientsession.close = warn_use( # type: ignore[assignment] + clientsession.close, + WARN_CLOSE_MSG, + ) if auto_cleanup_method: auto_cleanup_method(hass, clientsession) diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 55be6dc7d27..437cd418719 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -7,7 +7,7 @@ from collections.abc import Awaitable, Callable, Coroutine, Iterable from dataclasses import dataclass from itertools import groupby import logging -from typing import Any, Optional, cast +from typing import Any, cast import voluptuous as vol from voluptuous.humanize import humanize_error @@ -238,7 +238,7 @@ class StorageCollection(ObservableCollection, ABC): async def _async_load_data(self) -> dict | None: """Load the data.""" - return cast(Optional[dict], await self.store.async_load()) + return cast(dict | None, await self.store.async_load()) async def async_load(self) -> None: """Load the storage Manager.""" diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index feb7a2a17ae..30181751f8c 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -80,7 +80,7 @@ INPUT_ENTITY_ID = re.compile( r"^input_(?:select|text|number|boolean|datetime)\.(?!.+__)(?!_)[\da-z_]+(? TraceElement: @@ -139,7 +139,7 @@ def trace_condition_function(condition: ConditionCheckerType) -> ConditionChecke """Wrap a condition function to enable basic tracing.""" @ft.wraps(condition) - def wrapper(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool: + def wrapper(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool | None: """Trace condition.""" with trace_condition(variables): result = condition(hass, variables) @@ -173,9 +173,9 @@ async def async_from_config( @trace_condition_function def disabled_condition( hass: HomeAssistant, variables: TemplateVarsType = None - ) -> bool: - """Condition not enabled, will always pass.""" - return True + ) -> bool | None: + """Condition not enabled, will act as if it didn't exist.""" + return None return disabled_condition @@ -204,7 +204,7 @@ async def async_and_from_config( for index, check in enumerate(checks): try: with trace_path(["conditions", str(index)]): - if not check(hass, variables): + if check(hass, variables) is False: return False except ConditionError as ex: errors.append( @@ -235,7 +235,7 @@ async def async_or_from_config( for index, check in enumerate(checks): try: with trace_path(["conditions", str(index)]): - if check(hass, variables): + if check(hass, variables) is True: return True except ConditionError as ex: errors.append( @@ -611,8 +611,8 @@ def sun( # Special case: before sunrise OR after sunset # This will handle the very rare case in the polar region when the sun rises/sets # but does not set/rise. - # However this entire condition does not handle those full days of darkness or light, - # the following should be used instead: + # However this entire condition does not handle those full days of darkness + # or light, the following should be used instead: # # condition: # condition: state diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index c9d6ffe6065..6cdedf98f97 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable import logging -from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast from homeassistant import config_entries from homeassistant.components import onboarding @@ -176,7 +176,7 @@ def register_discovery_flow( ) -> None: """Register flow for discovered integrations that not require auth.""" - class DiscoveryFlow(DiscoveryFlowHandler[Union[Awaitable[bool], bool]]): + class DiscoveryFlow(DiscoveryFlowHandler[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 0a6356d310d..552fa29eb86 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -13,7 +13,7 @@ from collections.abc import Awaitable, Callable import logging import secrets import time -from typing import Any, cast +from typing import Any, Optional, cast from aiohttp import client, web import async_timeout @@ -437,7 +437,10 @@ class OAuth2AuthorizeCallbackView(http.HomeAssistantView): state = _decode_jwt(hass, request.query["state"]) if state is None: - return web.Response(text="Invalid state") + return web.Response( + text="Invalid state. Is My Home Assistant configured to go to the right instance?", + status=400, + ) user_input: dict[str, Any] = {"state": state} @@ -538,7 +541,10 @@ def _encode_jwt(hass: HomeAssistant, data: dict) -> str: @callback def _decode_jwt(hass: HomeAssistant, encoded: str) -> dict | None: """JWT encode data.""" - secret = cast(str, hass.data.get(DATA_JWT_SECRET)) + secret = cast(Optional[str], hass.data.get(DATA_JWT_SECRET)) + + if secret is None: + return None try: return jwt.decode(encoded, secret, algorithms=["HS256"]) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index ce2d0740d66..22022fbfa73 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -778,12 +778,12 @@ def _deprecated_or_removed( raise_if_present: bool, option_removed: bool, ) -> Callable[[dict], dict]: - """ - Log key as deprecated and provide a replacement (if exists) or fail. + """Log key as deprecated and provide a replacement (if exists) or fail. Expected behavior: - Outputs or throws the appropriate deprecation warning if key is detected - - Outputs or throws the appropriate error if key is detected and removed from support + - Outputs or throws the appropriate error if key is detected + and removed from support - Processes schema moving the value from key to replacement_key - Processes schema changing nothing if only replacement_key provided - No warning if only replacement_key provided @@ -809,7 +809,10 @@ def _deprecated_or_removed( """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[attr-defined] + near = ( + f"near {config.__config_file__}" # type: ignore[attr-defined] + f":{config.__line__} " + ) except AttributeError: near = "" arguments: tuple[str, ...] @@ -851,11 +854,11 @@ def deprecated( default: Any | None = None, raise_if_present: bool | None = False, ) -> Callable[[dict], dict]: - """ - Log key as deprecated and provide a replacement (if exists). + """Log key as deprecated and provide a replacement (if exists). Expected behavior: - - Outputs the appropriate deprecation warning if key is detected or raises an exception + - Outputs the appropriate deprecation warning if key is detected + or raises an exception - Processes schema moving the value from key to replacement_key - Processes schema changing nothing if only replacement_key provided - No warning if only replacement_key provided @@ -876,11 +879,11 @@ def removed( default: Any | None = None, raise_if_present: bool | None = True, ) -> Callable[[dict], dict]: - """ - Log key as deprecated and fail the config validation. + """Log key as deprecated and fail the config validation. Expected behavior: - - Outputs the appropriate error if key is detected and removed from support or raises an exception + - Outputs the appropriate error if key is detected and removed from + support or raises an exception. """ return _deprecated_or_removed( key, @@ -1071,7 +1074,7 @@ def make_entity_service_schema( SCRIPT_VARIABLES_SCHEMA = vol.All( vol.Schema({str: template_complex}), - # pylint: disable=unnecessary-lambda + # pylint: disable-next=unnecessary-lambda lambda val: script_variables_helper.ScriptVariables(val), ) @@ -1264,7 +1267,7 @@ AND_CONDITION_SCHEMA = vol.Schema( vol.Required(CONF_CONDITION): "and", vol.Required(CONF_CONDITIONS): vol.All( ensure_list, - # pylint: disable=unnecessary-lambda + # pylint: disable-next=unnecessary-lambda [lambda value: CONDITION_SCHEMA(value)], ), } @@ -1275,7 +1278,7 @@ AND_CONDITION_SHORTHAND_SCHEMA = vol.Schema( **CONDITION_BASE_SCHEMA, vol.Required("and"): vol.All( ensure_list, - # pylint: disable=unnecessary-lambda + # pylint: disable-next=unnecessary-lambda [lambda value: CONDITION_SCHEMA(value)], ), } @@ -1287,7 +1290,7 @@ OR_CONDITION_SCHEMA = vol.Schema( vol.Required(CONF_CONDITION): "or", vol.Required(CONF_CONDITIONS): vol.All( ensure_list, - # pylint: disable=unnecessary-lambda + # pylint: disable-next=unnecessary-lambda [lambda value: CONDITION_SCHEMA(value)], ), } @@ -1298,7 +1301,7 @@ OR_CONDITION_SHORTHAND_SCHEMA = vol.Schema( **CONDITION_BASE_SCHEMA, vol.Required("or"): vol.All( ensure_list, - # pylint: disable=unnecessary-lambda + # pylint: disable-next=unnecessary-lambda [lambda value: CONDITION_SCHEMA(value)], ), } @@ -1310,7 +1313,7 @@ NOT_CONDITION_SCHEMA = vol.Schema( vol.Required(CONF_CONDITION): "not", vol.Required(CONF_CONDITIONS): vol.All( ensure_list, - # pylint: disable=unnecessary-lambda + # pylint: disable-next=unnecessary-lambda [lambda value: CONDITION_SCHEMA(value)], ), } @@ -1321,7 +1324,7 @@ NOT_CONDITION_SHORTHAND_SCHEMA = vol.Schema( **CONDITION_BASE_SCHEMA, vol.Required("not"): vol.All( ensure_list, - # pylint: disable=unnecessary-lambda + # pylint: disable-next=unnecessary-lambda [lambda value: CONDITION_SCHEMA(value)], ), } @@ -1353,7 +1356,7 @@ CONDITION_SHORTHAND_SCHEMA = vol.Schema( **CONDITION_BASE_SCHEMA, vol.Required(CONF_CONDITION): vol.All( ensure_list, - # pylint: disable=unnecessary-lambda + # pylint: disable-next=unnecessary-lambda [lambda value: CONDITION_SCHEMA(value)], ), } diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index a132536b53f..08803aaded6 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -5,9 +5,7 @@ from collections.abc import Callable import functools import inspect import logging -from typing import Any, TypeVar - -from typing_extensions import ParamSpec +from typing import Any, ParamSpec, TypeVar from ..helpers.frame import MissingIntegrationFrame, get_integration_frame @@ -115,7 +113,7 @@ def deprecated_class( def deprecated_function( replacement: str, ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: - """Mark function as deprecated and provide a replacement function to be used instead.""" + """Mark function as deprecated and provide a replacement to be used instead.""" def deprecated_decorator(func: Callable[_P, _R]) -> Callable[_P, _R]: """Decorate function as deprecated.""" diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index a7c1ebdb434..a586a14d161 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections import UserDict -from collections.abc import Coroutine +from collections.abc import Coroutine, ValuesView import logging import time from typing import TYPE_CHECKING, Any, TypeVar, cast @@ -14,11 +14,16 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import Event, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, RequiredParameterMissing from homeassistant.loader import bind_hass +from homeassistant.util.json import ( + find_paths_unserializable_data, + format_unserializable_data, +) import homeassistant.util.uuid as uuid_util from . import storage from .debounce import Debouncer from .frame import report +from .json import JSON_DUMP from .typing import UNDEFINED, UndefinedType if TYPE_CHECKING: @@ -89,11 +94,53 @@ class DeviceEntry: # This value is not stored, just used to keep track of events to fire. is_new: bool = attr.ib(default=False) + _json_repr: str | None = attr.ib(cmp=False, default=None, init=False, repr=False) + @property def disabled(self) -> bool: """Return if entry is disabled.""" return self.disabled_by is not None + @property + def dict_repr(self) -> dict[str, Any]: + """Return a dict representation of the entry.""" + return { + "area_id": self.area_id, + "configuration_url": self.configuration_url, + "config_entries": list(self.config_entries), + "connections": list(self.connections), + "disabled_by": self.disabled_by, + "entry_type": self.entry_type, + "hw_version": self.hw_version, + "id": self.id, + "identifiers": list(self.identifiers), + "manufacturer": self.manufacturer, + "model": self.model, + "name_by_user": self.name_by_user, + "name": self.name, + "sw_version": self.sw_version, + "via_device_id": self.via_device_id, + } + + @property + def json_repr(self) -> str | None: + """Return a cached JSON representation of the entry.""" + if self._json_repr is not None: + return self._json_repr + + try: + dict_repr = self.dict_repr + object.__setattr__(self, "_json_repr", JSON_DUMP(dict_repr)) + except (ValueError, TypeError): + _LOGGER.error( + "Unable to serialize entry %s to JSON. Bad data found at %s", + self.id, + format_unserializable_data( + find_paths_unserializable_data(dict_repr, dump=JSON_DUMP) + ), + ) + return self._json_repr + @attr.s(slots=True, frozen=True) class DeletedDeviceEntry: @@ -161,7 +208,9 @@ class DeviceRegistryStore(storage.Store[dict[str, list[dict[str, Any]]]]): device.setdefault("configuration_url", None) device.setdefault("disabled_by", None) try: - device["entry_type"] = DeviceEntryType(device.get("entry_type")) # type: ignore[arg-type] + device["entry_type"] = DeviceEntryType( + device.get("entry_type"), # type: ignore[arg-type] + ) except ValueError: device["entry_type"] = None device.setdefault("name_by_user", None) @@ -197,6 +246,10 @@ class DeviceRegistryItems(UserDict[str, _EntryTypeT]): self._connections: dict[tuple[str, str], _EntryTypeT] = {} self._identifiers: dict[tuple[str, str], _EntryTypeT] = {} + def values(self) -> ValuesView[_EntryTypeT]: + """Return the underlying values to avoid __iter__ overhead.""" + return self.data.values() + def __setitem__(self, key: str, entry: _EntryTypeT) -> None: """Add an item.""" if key in self: @@ -397,7 +450,7 @@ class DeviceRegistry: ) -> DeviceEntry | None: """Update device attributes.""" # Circular dep - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from . import area_registry as ar old = self.devices[device_id] @@ -550,7 +603,10 @@ class DeviceRegistry: config_entries=set(device["config_entries"]), configuration_url=device["configuration_url"], # type ignores (if tuple arg was cast): likely https://github.com/python/mypy/issues/8625 - connections={tuple(conn) for conn in device["connections"]}, # type: ignore[misc] + connections={ + tuple(conn) # type: ignore[misc] + for conn in device["connections"] + }, disabled_by=DeviceEntryDisabler(device["disabled_by"]) if device["disabled_by"] else None, @@ -559,7 +615,10 @@ class DeviceRegistry: else None, hw_version=device["hw_version"], id=device["id"], - identifiers={tuple(iden) for iden in device["identifiers"]}, # type: ignore[misc] + identifiers={ + tuple(iden) # type: ignore[misc] + for iden in device["identifiers"] + }, manufacturer=device["manufacturer"], model=device["model"], name_by_user=device["name_by_user"], @@ -572,8 +631,14 @@ class DeviceRegistry: deleted_devices[device["id"]] = DeletedDeviceEntry( config_entries=set(device["config_entries"]), # type ignores (if tuple arg was cast): likely https://github.com/python/mypy/issues/8625 - connections={tuple(conn) for conn in device["connections"]}, # type: ignore[misc] - identifiers={tuple(iden) for iden in device["identifiers"]}, # type: ignore[misc] + connections={ + tuple(conn) # type: ignore[misc] + for conn in device["connections"] + }, + identifiers={ + tuple(iden) # type: ignore[misc] + for iden in device["identifiers"] + }, id=device["id"], orphaned_timestamp=device["orphaned_timestamp"], ) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 745f9b0ba53..f9bb1effeb2 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -185,10 +185,11 @@ class EntityCategory(StrEnum): - Not be included in indirect service calls to devices or areas """ - # Config: An entity which allows changing the configuration of a device + # Config: An entity which allows changing the configuration of a device. CONFIG = "config" - # Diagnostic: An entity exposing some configuration parameter or diagnostics of a device + # Diagnostic: An entity exposing some configuration parameter, + # or diagnostics of a device. DIAGNOSTIC = "diagnostic" @@ -198,13 +199,16 @@ ENTITY_CATEGORIES_SCHEMA: Final = vol.Coerce(EntityCategory) class EntityPlatformState(Enum): """The platform state of an entity.""" - # Not Added: Not yet added to a platform, polling updates are written to the state machine + # Not Added: Not yet added to a platform, polling updates + # are written to the state machine. NOT_ADDED = auto() - # Added: Added to a platform, polling updates are written to the state machine + # Added: Added to a platform, polling updates + # are written to the state machine. ADDED = auto() - # Removed: Removed from a platform, polling updates are not written to the state machine + # Removed: Removed from a platform, polling updates + # are not written to the state machine. REMOVED = auto() @@ -458,7 +462,10 @@ class Entity(ABC): @property def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" + """Return if the entity should be enabled when first added. + + This only applies when fist added to the entity registry. + """ if hasattr(self, "_attr_entity_registry_enabled_default"): return self._attr_entity_registry_enabled_default if hasattr(self, "entity_description"): @@ -467,7 +474,10 @@ class Entity(ABC): @property def entity_registry_visible_default(self) -> bool: - """Return if the entity should be visible when first added to the entity registry.""" + """Return if the entity should be visible when first added. + + This only applies when fist added to the entity registry. + """ if hasattr(self, "_attr_entity_registry_visible_default"): return self._attr_entity_registry_visible_default if hasattr(self, "entity_description"): diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 1932c0397a1..f9cd7d979c8 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -90,11 +90,11 @@ class EntityComponent(Generic[_EntityT]): @property def entities(self) -> Iterable[_EntityT]: - """ - Return an iterable that returns all entities. + """Return an iterable that returns all entities. - As the underlying dicts may change when async context is lost, callers that - iterate over this asynchronously should make a copy using list() before iterating. + As the underlying dicts may change when async context is lost, + callers that iterate over this asynchronously should make a copy + using list() before iterating. """ return chain.from_iterable( platform.entities.values() # type: ignore[misc] diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 622fa4c1751..18dd9aea344 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -158,12 +158,16 @@ class EntityPlatform: ) -> asyncio.Semaphore | None: """Get or create a semaphore for parallel updates. - Semaphore will be created on demand because we base it off if update method is async or not. + Semaphore will be created on demand because we base it off if update + method is async or not. - If parallel updates is set to 0, we skip the semaphore. - If parallel updates is set to a number, we initialize the semaphore to that number. - The default value for parallel requests is decided based on the first entity that is added to Home Assistant. - It's 0 if the entity defines the async_update method, else it's 1. + - If parallel updates is set to 0, we skip the semaphore. + - If parallel updates is set to a number, we initialize the semaphore + to that number. + + The default value for parallel requests is decided based on the first + entity that is added to Home Assistant. It's 0 if the entity defines + the async_update method, else it's 1. """ if self.parallel_updates_created: return self.parallel_updates @@ -566,7 +570,9 @@ class EntityPlatform: "via_device", ): if key in device_info: - processed_dev_info[key] = device_info[key] # type: ignore[literal-required] + processed_dev_info[key] = device_info[ + key # type: ignore[literal-required] + ] if "configuration_url" in device_info: if device_info["configuration_url"] is None: @@ -586,7 +592,9 @@ class EntityPlatform: ) try: - device = device_registry.async_get_or_create(**processed_dev_info) # type: ignore[arg-type] + device = device_registry.async_get_or_create( + **processed_dev_info # type: ignore[arg-type] + ) device_id = device.id except RequiredParameterMissing: pass diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index da4a8aae6e1..08130500f11 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -10,7 +10,7 @@ timer. from __future__ import annotations from collections import UserDict -from collections.abc import Callable, Iterable, Mapping +from collections.abc import Callable, Iterable, Mapping, ValuesView import logging from typing import TYPE_CHECKING, Any, TypeVar, cast @@ -42,10 +42,15 @@ from homeassistant.core import ( from homeassistant.exceptions import MaxLengthExceeded from homeassistant.loader import bind_hass from homeassistant.util import slugify, uuid as uuid_util +from homeassistant.util.json import ( + find_paths_unserializable_data, + format_unserializable_data, +) from . import device_registry as dr, storage from .device_registry import EVENT_DEVICE_REGISTRY_UPDATED from .frame import report +from .json import JSON_DUMP from .typing import UNDEFINED, UndefinedType if TYPE_CHECKING: @@ -119,7 +124,8 @@ class RegistryEntry: has_entity_name: bool = attr.ib(default=False) name: str | None = attr.ib(default=None) options: EntityOptionsType = attr.ib( - default=None, converter=attr.converters.default_if_none(factory=dict) # type: ignore[misc] + default=None, + converter=attr.converters.default_if_none(factory=dict), # type: ignore[misc] ) # As set by integration original_device_class: str | None = attr.ib(default=None) @@ -129,6 +135,8 @@ class RegistryEntry: translation_key: str | None = attr.ib(default=None) unit_of_measurement: str | None = attr.ib(default=None) + _json_repr: str | None = attr.ib(cmp=False, default=None, init=False, repr=False) + @domain.default def _domain_default(self) -> str: """Compute domain value.""" @@ -144,6 +152,46 @@ class RegistryEntry: """Return if entry is hidden.""" return self.hidden_by is not None + @property + def as_partial_dict(self) -> dict[str, Any]: + """Return a partial dict representation of the entry.""" + return { + "area_id": self.area_id, + "config_entry_id": self.config_entry_id, + "device_id": self.device_id, + "disabled_by": self.disabled_by, + "entity_category": self.entity_category, + "entity_id": self.entity_id, + "has_entity_name": self.has_entity_name, + "hidden_by": self.hidden_by, + "icon": self.icon, + "id": self.id, + "name": self.name, + "original_name": self.original_name, + "platform": self.platform, + "translation_key": self.translation_key, + "unique_id": self.unique_id, + } + + @property + def partial_json_repr(self) -> str | None: + """Return a cached partial JSON representation of the entry.""" + if self._json_repr is not None: + return self._json_repr + + try: + dict_repr = self.as_partial_dict + object.__setattr__(self, "_json_repr", JSON_DUMP(dict_repr)) + except (ValueError, TypeError): + _LOGGER.error( + "Unable to serialize entry %s to JSON. Bad data found at %s", + self.entity_id, + format_unserializable_data( + find_paths_unserializable_data(dict_repr, dump=JSON_DUMP) + ), + ) + return self._json_repr + @callback def write_unavailable_state(self, hass: HomeAssistant) -> None: """Write the unavailable state to the state machine.""" @@ -267,6 +315,10 @@ class EntityRegistryItems(UserDict[str, "RegistryEntry"]): self._entry_ids: dict[str, RegistryEntry] = {} self._index: dict[tuple[str, str, str], str] = {} + def values(self) -> ValuesView[RegistryEntry]: + """Return the underlying values to avoid __iter__ overhead.""" + return self.data.values() + def __setitem__(self, key: str, entry: RegistryEntry) -> None: """Add an item.""" if key in self: @@ -780,8 +832,7 @@ class EntityRegistry: new_unique_id: str | UndefinedType = UNDEFINED, new_device_id: str | None | UndefinedType = UNDEFINED, ) -> RegistryEntry: - """ - Update entity platform. + """Update entity platform. This should only be used when an entity needs to be migrated between integrations. diff --git a/homeassistant/helpers/entityfilter.py b/homeassistant/helpers/entityfilter.py index 109c5454cc2..d8b827bd24f 100644 --- a/homeassistant/helpers/entityfilter.py +++ b/homeassistant/helpers/entityfilter.py @@ -43,14 +43,16 @@ class EntityFilter: def explicitly_included(self, entity_id: str) -> bool: """Check if an entity is explicitly included.""" - return entity_id in self._include_e or _test_against_patterns( - self._include_eg, entity_id + return entity_id in self._include_e or ( + bool(self._include_eg) + and _test_against_patterns(self._include_eg, entity_id) ) def explicitly_excluded(self, entity_id: str) -> bool: """Check if an entity is explicitly excluded.""" - return entity_id in self._exclude_e or _test_against_patterns( - self._exclude_eg, entity_id + return entity_id in self._exclude_e or ( + bool(self._exclude_eg) + and _test_against_patterns(self._exclude_eg, entity_id) ) def __call__(self, entity_id: str) -> bool: @@ -189,7 +191,7 @@ def _generate_filter_from_sets_and_pattern_lists( return ( entity_id in include_e or domain in include_d - or _test_against_patterns(include_eg, entity_id) + or (bool(include_eg) and _test_against_patterns(include_eg, entity_id)) ) def entity_excluded(domain: str, entity_id: str) -> bool: @@ -197,7 +199,7 @@ def _generate_filter_from_sets_and_pattern_lists( return ( entity_id in exclude_e or domain in exclude_d - or _test_against_patterns(exclude_eg, entity_id) + or (bool(exclude_eg) and _test_against_patterns(exclude_eg, entity_id)) ) # Case 1 - No filter @@ -247,10 +249,12 @@ def _generate_filter_from_sets_and_pattern_lists( return entity_id in include_e or ( entity_id not in exclude_e and ( - _test_against_patterns(include_eg, entity_id) + (include_eg and _test_against_patterns(include_eg, entity_id)) or ( split_entity_id(entity_id)[0] in include_d - and not _test_against_patterns(exclude_eg, entity_id) + and not ( + exclude_eg and _test_against_patterns(exclude_eg, entity_id) + ) ) ) ) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index b42bf6c6913..d363f105642 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -10,10 +10,9 @@ import functools as ft import logging from random import randint import time -from typing import Any, Union, cast +from typing import Any, Concatenate, ParamSpec, cast import attr -from typing_extensions import Concatenate, ParamSpec from homeassistant.const import ( ATTR_ENTITY_ID, @@ -710,8 +709,8 @@ def async_track_state_change_filtered( Returns ------- - Object used to update the listeners (async_update_listeners) with a new TrackStates or - cancel the tracking (async_remove). + Object used to update the listeners (async_update_listeners) with a new + TrackStates or cancel the tracking (async_remove). """ tracker = _TrackStateChangeFiltered(hass, track_states, action) @@ -1129,7 +1128,7 @@ class TrackTemplateResultInfo: TrackTemplateResultListener = Callable[ [ - Union[Event, None], + Event | None, list[TrackTemplateResult], ], None, @@ -1326,7 +1325,7 @@ def async_track_point_in_utc_time( hass.async_run_hass_job(job, utc_point_in_time) job = action if isinstance(action, HassJob) else HassJob(action) - delta = utc_point_in_time.timestamp() - time.time() + delta = expected_fire_timestamp - time.time() cancel_callback = hass.loop.call_later(delta, run_action, job) @callback diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 100d64c8fb6..58252da4822 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -1,22 +1,26 @@ """Module to coordinate user intentions.""" from __future__ import annotations -from collections.abc import Callable, Iterable +import asyncio +from collections.abc import Collection, Iterable import dataclasses from dataclasses import dataclass from enum import Enum import logging -import re from typing import Any, TypeVar import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, +) from homeassistant.core import Context, HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass -from . import config_validation as cv +from . import area_registry, config_validation as cv, device_registry, entity_registry _LOGGER = logging.getLogger(__name__) _SlotsType = dict[str, Any] @@ -110,21 +114,166 @@ class IntentUnexpectedError(IntentError): """Unexpected error while handling intent.""" +def _is_device_class( + state: State, + entity: entity_registry.RegistryEntry | None, + device_classes: Collection[str], +) -> bool: + """Return true if entity device class matches.""" + # Try entity first + if (entity is not None) and (entity.device_class is not None): + # Entity device class can be None or blank as "unset" + if entity.device_class in device_classes: + return True + + # Fall back to state attribute + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + return (device_class is not None) and (device_class in device_classes) + + +def _has_name( + state: State, entity: entity_registry.RegistryEntry | None, name: str +) -> bool: + """Return true if entity name or alias matches.""" + if name in (state.entity_id, state.name.casefold()): + return True + + # Check name/aliases + if (entity is None) or (not entity.aliases): + return False + + for alias in entity.aliases: + if name == alias.casefold(): + return True + + return False + + +def _find_area( + id_or_name: str, areas: area_registry.AreaRegistry +) -> area_registry.AreaEntry | None: + """Find an area by id or name, checking aliases too.""" + area = areas.async_get_area(id_or_name) or areas.async_get_area_by_name(id_or_name) + if area is not None: + return area + + # Check area aliases + for maybe_area in areas.areas.values(): + if not maybe_area.aliases: + continue + + for area_alias in maybe_area.aliases: + if id_or_name == area_alias.casefold(): + return maybe_area + + return None + + +def _filter_by_area( + states_and_entities: list[tuple[State, entity_registry.RegistryEntry | None]], + area: area_registry.AreaEntry, + devices: device_registry.DeviceRegistry, +) -> Iterable[tuple[State, entity_registry.RegistryEntry | None]]: + """Filter state/entity pairs by an area.""" + entity_area_ids: dict[str, str | None] = {} + for _state, entity in states_and_entities: + if entity is None: + continue + + if entity.area_id: + # Use entity's area id first + entity_area_ids[entity.id] = entity.area_id + elif entity.device_id: + # Fall back to device area if not set on entity + device = devices.async_get(entity.device_id) + if device is not None: + entity_area_ids[entity.id] = device.area_id + + for state, entity in states_and_entities: + if (entity is not None) and (entity_area_ids.get(entity.id) == area.id): + yield (state, entity) + + @callback @bind_hass -def async_match_state( - hass: HomeAssistant, name: str, states: Iterable[State] | None = None -) -> State: - """Find a state that matches the name.""" +def async_match_states( + hass: HomeAssistant, + name: str | None = None, + area_name: str | None = None, + area: area_registry.AreaEntry | None = None, + domains: Collection[str] | None = None, + device_classes: Collection[str] | None = None, + states: Iterable[State] | None = None, + entities: entity_registry.EntityRegistry | None = None, + areas: area_registry.AreaRegistry | None = None, + devices: device_registry.DeviceRegistry | None = None, +) -> Iterable[State]: + """Find states that match the constraints.""" if states is None: + # All states states = hass.states.async_all() - state = _fuzzymatch(name, states, lambda state: state.name) + if entities is None: + entities = entity_registry.async_get(hass) - if state is None: - raise IntentHandleError(f"Unable to find an entity called {name}") + # Gather entities + states_and_entities: list[tuple[State, entity_registry.RegistryEntry | None]] = [] + for state in states: + entity = entities.async_get(state.entity_id) + if (entity is not None) and entity.entity_category: + # Skip diagnostic entities + continue - return state + states_and_entities.append((state, entity)) + + # Filter by domain and device class + if domains: + states_and_entities = [ + (state, entity) + for state, entity in states_and_entities + if state.domain in domains + ] + + if device_classes: + # Check device class in state attribute and in entity entry (if available) + states_and_entities = [ + (state, entity) + for state, entity in states_and_entities + if _is_device_class(state, entity, device_classes) + ] + + if (area is None) and (area_name is not None): + # Look up area by name + if areas is None: + areas = area_registry.async_get(hass) + + area = _find_area(area_name, areas) + assert area is not None, f"No area named {area_name}" + + if area is not None: + # Filter by states/entities by area + if devices is None: + devices = device_registry.async_get(hass) + + states_and_entities = list(_filter_by_area(states_and_entities, area, devices)) + + if name is not None: + if devices is None: + devices = device_registry.async_get(hass) + + # Filter by name + name = name.casefold() + + # Check states + for state, entity in states_and_entities: + if _has_name(state, entity, name): + yield state + break + + else: + # Not filtered by name + for state, _entity in states_and_entities: + yield state @callback @@ -173,32 +322,20 @@ class IntentHandler: return f"<{self.__class__.__name__} - {self.intent_type}>" -def _fuzzymatch(name: str, items: Iterable[_T], key: Callable[[_T], str]) -> _T | None: - """Fuzzy matching function.""" - matches = [] - pattern = ".*?".join(name) - regex = re.compile(pattern, re.IGNORECASE) - for idx, item in enumerate(items): - if match := regex.search(key(item)): - # Add key length so we prefer shorter keys with the same group and start. - # Add index so we pick first match in case same group, start, and key length. - matches.append( - (len(match.group()), match.start(), len(key(item)), idx, item) - ) - - return sorted(matches)[0][4] if matches else None - - class ServiceIntentHandler(IntentHandler): """Service Intent handler registration. Service specific intent handler that calls a service by name/entity_id. """ - slot_schema = {vol.Required("name"): cv.string} + slot_schema = { + vol.Any("name", "area"): cv.string, + vol.Optional("domain"): vol.All(cv.ensure_list, [cv.string]), + vol.Optional("device_class"): vol.All(cv.ensure_list, [cv.string]), + } def __init__( - self, intent_type: str, domain: str, service: str, speech: str + self, intent_type: str, domain: str, service: str, speech: str | None = None ) -> None: """Create Service Intent Handler.""" self.intent_type = intent_type @@ -210,8 +347,99 @@ class ServiceIntentHandler(IntentHandler): """Handle the hass intent.""" hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - state = async_match_state(hass, slots["name"]["value"]) + name: str | None = slots.get("name", {}).get("value") + if name == "all": + # Don't match on name if targeting all entities + name = None + + # Look up area first to fail early + area_name = slots.get("area", {}).get("value") + area: area_registry.AreaEntry | None = None + if area_name is not None: + areas = area_registry.async_get(hass) + area = areas.async_get_area(area_name) or areas.async_get_area_by_name( + area_name + ) + if area is None: + raise IntentHandleError(f"No area named {area_name}") + + # Optional domain/device class filters. + # Convert to sets for speed. + domains: set[str] | None = None + device_classes: set[str] | None = None + + if "domain" in slots: + domains = set(slots["domain"]["value"]) + + if "device_class" in slots: + device_classes = set(slots["device_class"]["value"]) + + states = list( + async_match_states( + hass, + name=name, + area=area, + domains=domains, + device_classes=device_classes, + ) + ) + + if not states: + raise IntentHandleError("No entities matched") + + response = await self.async_handle_states(intent_obj, states, area) + + return response + + async def async_handle_states( + self, + intent_obj: Intent, + states: list[State], + area: area_registry.AreaEntry | None = None, + ) -> IntentResponse: + """Complete action on matched entity states.""" + assert states + success_results: list[IntentResponseTarget] = [] + response = intent_obj.create_response() + + if area is not None: + success_results.append( + IntentResponseTarget( + type=IntentResponseTargetType.AREA, name=area.name, id=area.id + ) + ) + speech_name = area.name + else: + speech_name = states[0].name + + service_coros = [] + for state in states: + service_coros.append(self.async_call_service(intent_obj, state)) + success_results.append( + IntentResponseTarget( + type=IntentResponseTargetType.ENTITY, + name=state.name, + id=state.entity_id, + ), + ) + + # Handle service calls in parallel. + # We will need to handle partial failures here. + await asyncio.gather(*service_coros) + + response.async_set_results( + success_results=success_results, + ) + + if self.speech is not None: + response.async_set_speech(self.speech.format(speech_name)) + + return response + + async def async_call_service(self, intent_obj: Intent, state: State) -> None: + """Call service on entity.""" + hass = intent_obj.hass await hass.services.async_call( self.domain, self.service, @@ -219,19 +447,6 @@ class ServiceIntentHandler(IntentHandler): context=intent_obj.context, ) - response = intent_obj.create_response() - response.async_set_speech(self.speech.format(state.name)) - response.async_set_results( - success_results=[ - IntentResponseTarget( - type=IntentResponseTargetType.ENTITY, - name=state.name, - id=state.entity_id, - ), - ], - ) - return response - class IntentCategory(Enum): """Category of an intent.""" diff --git a/homeassistant/helpers/json.py b/homeassistant/helpers/json.py index 74a2f542910..2a499dc0d97 100644 --- a/homeassistant/helpers/json.py +++ b/homeassistant/helpers/json.py @@ -71,6 +71,40 @@ def json_bytes(data: Any) -> bytes: ) +def json_bytes_strip_null(data: Any) -> bytes: + """Dump json bytes after terminating strings at the first NUL.""" + + def process_dict(_dict: dict[Any, Any]) -> dict[Any, Any]: + """Strip NUL from items in a dict.""" + return {key: strip_null(o) for key, o in _dict.items()} + + def process_list(_list: list[Any]) -> list[Any]: + """Strip NUL from items in a list.""" + return [strip_null(o) for o in _list] + + def strip_null(obj: Any) -> Any: + """Strip NUL from an object.""" + if isinstance(obj, str): + return obj.split("\0", 1)[0] + if isinstance(obj, dict): + return process_dict(obj) + if isinstance(obj, list): + return process_list(obj) + return obj + + # We expect null-characters to be very rare, hence try encoding first and look + # for an escaped null-character in the output. + result = json_bytes(data) + if b"\\u0000" in result: + # We work on the processed result so we don't need to worry about + # Home Assistant extensions which allows encoding sets, tuples, etc. + data_processed = orjson.loads(result) + data_processed = strip_null(data_processed) + result = json_bytes(data_processed) + + return result + + def json_dumps(data: Any) -> str: """Dump json string. diff --git a/homeassistant/helpers/location.py b/homeassistant/helpers/location.py index a22d5fddf0c..086150115da 100644 --- a/homeassistant/helpers/location.py +++ b/homeassistant/helpers/location.py @@ -50,8 +50,11 @@ def find_coordinates( ) -> str | 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. + 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: @@ -70,7 +73,9 @@ def find_coordinates( zone_entity = hass.states.get(f"zone.{entity_state.state}") if has_location(zone_entity): # type: ignore[arg-type] _LOGGER.debug( - "%s is in %s, getting zone location", name, zone_entity.entity_id # type: ignore[union-attr] + "%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[arg-type] @@ -97,12 +102,16 @@ def find_coordinates( _LOGGER.debug("Resolving nested entity_id: %s", entity_state.state) return find_coordinates(hass, entity_state.state, recursion_history) - # Might be an address, coordinates or anything else. This has to be checked by the caller. + # Might be an address, coordinates or anything else. + # This has to be checked by the caller. 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.""" + """Get a lat/long from a zones friendly_name. + + None is returned if no zone is found by that friendly_name. + """ states = hass.states.async_all("zone") for state in states: if state.name == zone_name: diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index c31fe0f3ce4..073d1879a82 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -303,7 +303,9 @@ class RestoreEntity(Entity): """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] + _LOGGER.warning( # type: ignore[unreachable] + "Cannot get last state. Entity not added to hass" + ) return None data = await RestoreStateData.async_get_instance(self.hass) if self.entity_id not in data.last_states: diff --git a/homeassistant/helpers/schema_config_entry_flow.py b/homeassistant/helpers/schema_config_entry_flow.py index 4e319b20cb6..c4a274aa6bc 100644 --- a/homeassistant/helpers/schema_config_entry_flow.py +++ b/homeassistant/helpers/schema_config_entry_flow.py @@ -60,9 +60,9 @@ class SchemaFlowFormStep(SchemaFlowStep): """Optional property to identify next step. - If `next_step` is a function, it is called if the schema validates successfully or - if no schema is defined. The `next_step` function is passed the union of config entry - options and user input from previous steps. If the function returns None, the flow is - ended with `FlowResultType.CREATE_ENTRY`. + if no schema is defined. The `next_step` function is passed the union of + config entry options and user input from previous steps. If the function returns + None, the flow is ended with `FlowResultType.CREATE_ENTRY`. - If `next_step` is None, the flow is ended with `FlowResultType.CREATE_ENTRY`. """ @@ -71,11 +71,11 @@ class SchemaFlowFormStep(SchemaFlowStep): ] | None | UndefinedType = UNDEFINED """Optional property to populate suggested values. - - If `suggested_values` is UNDEFINED, each key in the schema will get a suggested value - from an option with the same key. + - If `suggested_values` is UNDEFINED, each key in the schema will get a suggested + value from an option with the same key. - Note: if a step is retried due to a validation failure, then the user input will have - priority over the suggested values. + Note: if a step is retried due to a validation failure, then the user input will + have priority over the suggested values. """ @@ -331,8 +331,8 @@ class SchemaConfigFlowHandler(config_entries.ConfigFlow, ABC): ) -> None: """Take necessary actions after the options flow is finished, if needed. - The options parameter contains config entry options, which is the union of stored - options and user input from the options flow steps. + The options parameter contains config entry options, which is the union of + stored options and user input from the options flow steps. """ @callback diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 66a042dbb3d..34a0d4de2d4 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -11,7 +11,7 @@ from functools import partial import itertools import logging from types import MappingProxyType -from typing import Any, TypedDict, Union, cast +from typing import Any, TypedDict, cast import async_timeout import voluptuous as vol @@ -375,7 +375,7 @@ class _ScriptRun: self._script._changed() # pylint: disable=protected-access async def _async_get_condition(self, config): - # pylint: disable=protected-access + # pylint: disable-next=protected-access return await self._script._async_get_condition(config) def _log( @@ -786,7 +786,7 @@ class _ScriptRun: repeat_vars["item"] = item self._variables["repeat"] = repeat_vars - # pylint: disable=protected-access + # pylint: disable-next=protected-access script = self._script._get_repeat_script(self._step) async def async_run_sequence(iteration, extra_msg=""): @@ -1110,7 +1110,7 @@ async def _async_stop_scripts_at_shutdown(hass, event): ) -_VarsType = Union[dict[str, Any], MappingProxyType] +_VarsType = dict[str, Any] | MappingProxyType def _referenced_extract_ids(data: Any, key: str, found: set[str]) -> None: diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index c7a4be175fb..0ba5ee363e9 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -5,6 +5,7 @@ from collections.abc import Callable, Mapping, Sequence from typing import Any, Generic, Literal, TypedDict, TypeVar, cast from uuid import UUID +from typing_extensions import Required import voluptuous as vol from homeassistant.backports.enum import StrEnum @@ -211,7 +212,7 @@ class AreaSelector(Selector[AreaSelectorConfig]): class AttributeSelectorConfig(TypedDict, total=False): """Class to represent an attribute selector config.""" - entity_id: str + entity_id: Required[str] hide_attributes: list[str] @@ -728,10 +729,11 @@ class SelectSelectorMode(StrEnum): class SelectSelectorConfig(TypedDict, total=False): """Class to represent a select selector config.""" - options: Sequence[SelectOptionDict] | Sequence[str] # required + options: Required[Sequence[SelectOptionDict] | Sequence[str]] multiple: bool custom_value: bool mode: SelectSelectorMode + translation_key: str @SELECTORS.register("select") @@ -748,6 +750,7 @@ class SelectSelector(Selector[SelectSelectorConfig]): vol.Optional("mode"): vol.All( vol.Coerce(SelectSelectorMode), lambda val: val.value ), + vol.Optional("translation_key"): cv.string, } ) @@ -788,7 +791,7 @@ class TargetSelectorConfig(TypedDict, total=False): class StateSelectorConfig(TypedDict, total=False): """Class to represent an state selector config.""" - entity_id: str + entity_id: Required[str] @SELECTORS.register("state") diff --git a/homeassistant/helpers/sensor.py b/homeassistant/helpers/sensor.py index f206ac55bdd..96e6b83a167 100644 --- a/homeassistant/helpers/sensor.py +++ b/homeassistant/helpers/sensor.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: def sensor_device_info_to_hass_device_info( sensor_device_info: SensorDeviceInfo, ) -> DeviceInfo: - """Convert a sensor_state_data sensor device info to a Home Assistant device info.""" + """Convert a sensor_state_data sensor device info to a HA device info.""" device_info = DeviceInfo() if sensor_device_info.name is not None: device_info[const.ATTR_NAME] = sensor_device_info.name diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 935f1840db5..ef8bee1fc7e 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -6,9 +6,8 @@ from collections.abc import Awaitable, Callable, Iterable import dataclasses from functools import partial, wraps import logging -from typing import TYPE_CHECKING, Any, TypedDict, TypeVar +from typing import TYPE_CHECKING, Any, TypedDict, TypeGuard, TypeVar -from typing_extensions import TypeGuard import voluptuous as vol from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL @@ -371,7 +370,8 @@ def async_extract_referenced_entity_ids( return selected for ent_entry in ent_reg.entities.values(): - # Do not add entities which are hidden or which are config or diagnostic entities + # Do not add entities which are hidden or which are config + # or diagnostic entities. if ent_entry.entity_category is not None or ent_entry.hidden_by is not None: continue @@ -489,7 +489,10 @@ 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[union-attr] + + yaml_description = domain_yaml.get( # type: ignore[union-attr] + service, {} + ) # Don't warn for missing services, because it triggers false # positives for things like scripts, that register as a service @@ -706,11 +709,14 @@ 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[arg-type] + result = hass.async_run_job( + partial(getattr(entity, func), **data) # type: ignore[arg-type] + ) else: result = hass.async_run_job(func, entity, data) - # Guard because callback functions do not return a task when passed to async_run_job. + # Guard because callback functions do not return a task when passed to + # async_run_job. if result is not None: await result diff --git a/homeassistant/helpers/service_info/mqtt.py b/homeassistant/helpers/service_info/mqtt.py index fcf5d4744f1..3626f9b5758 100644 --- a/homeassistant/helpers/service_info/mqtt.py +++ b/homeassistant/helpers/service_info/mqtt.py @@ -1,11 +1,10 @@ """MQTT Discovery data.""" from dataclasses import dataclass import datetime as dt -from typing import Union from homeassistant.data_entry_flow import BaseServiceInfo -ReceivePayloadType = Union[str, bytes] +ReceivePayloadType = str | bytes @dataclass diff --git a/homeassistant/helpers/significant_change.py b/homeassistant/helpers/significant_change.py index c1dbaf8c6e4..589d792f1f8 100644 --- a/homeassistant/helpers/significant_change.py +++ b/homeassistant/helpers/significant_change.py @@ -14,7 +14,7 @@ async def async_check_significant_change( new_state: str, new_attrs: dict, **kwargs, -) -> Optional[bool] +) -> bool | None ``` Return boolean to indicate if significantly changed. If don't know, return None. @@ -30,7 +30,7 @@ from __future__ import annotations from collections.abc import Callable from types import MappingProxyType -from typing import Any, Optional, Union +from typing import Any from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State, callback @@ -43,24 +43,24 @@ CheckTypeFunc = Callable[ [ HomeAssistant, str, - Union[dict, MappingProxyType], + dict | MappingProxyType, str, - Union[dict, MappingProxyType], + dict | MappingProxyType, ], - Optional[bool], + bool | None, ] ExtraCheckTypeFunc = Callable[ [ HomeAssistant, str, - Union[dict, MappingProxyType], + dict | MappingProxyType, Any, str, - Union[dict, MappingProxyType], + dict | MappingProxyType, Any, ], - Optional[bool], + bool | None, ] diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 75ca96ea246..21d060f4ba7 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -31,8 +31,7 @@ _LOGGER = logging.getLogger(__name__) class AsyncTrackStates: - """ - Record the time when the with-block is entered. + """Record the time when the with-block is entered. Add all states that have changed since the start time to the return list when with-block is exited. @@ -119,8 +118,7 @@ async def async_reproduce_state( def state_as_number(state: State) -> float: - """ - Try to coerce our state to a number. + """Try to coerce our state to a number. Raises ValueError if this is not possible. """ diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 44a0da7866b..087f98e4c42 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -9,7 +9,7 @@ import inspect from json import JSONEncoder import logging import os -from typing import Any, Generic, TypeVar, Union +from typing import Any, Generic, TypeVar from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) STORAGE_SEMAPHORE = "storage_semaphore" -_T = TypeVar("_T", bound=Union[Mapping[str, Any], Sequence[Any]]) +_T = TypeVar("_T", bound=Mapping[str, Any] | Sequence[Any]) @bind_hass @@ -104,8 +104,9 @@ class Store(Generic[_T]): async def async_load(self) -> _T | None: """Load data. - If the expected version and minor version do not match the given versions, the - migrate function will be invoked with migrate_func(version, minor_version, config). + If the expected version and minor version do not match the given + versions, the migrate function will be invoked with + migrate_func(version, minor_version, config). Will ensure that when a call comes in while another one is in progress, the second call will wait and return the result of the first call. @@ -276,12 +277,13 @@ class Store(Generic[_T]): self._data = None try: - await self.hass.async_add_executor_job( - self._write_data, self.path, data - ) + await self._async_write_data(self.path, data) except (json_util.SerializationError, json_util.WriteError) as err: _LOGGER.error("Error writing config for %s: %s", self.key, err) + async def _async_write_data(self, path: str, data: dict) -> None: + await self.hass.async_add_executor_job(self._write_data, self.path, data) + def _write_data(self, path: str, data: dict) -> None: """Write the data.""" os.makedirs(os.path.dirname(path), exist_ok=True) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 0c888784629..adbb8fe0d36 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -13,23 +13,32 @@ from functools import cache, lru_cache, partial, wraps import json import logging import math -from operator import attrgetter +from operator import attrgetter, contains import random import re import statistics from struct import error as StructError, pack, unpack_from import sys from types import CodeType -from typing import Any, Literal, NoReturn, TypeVar, cast, overload +from typing import ( + Any, + Concatenate, + Literal, + NoReturn, + ParamSpec, + TypeVar, + cast, + overload, +) from urllib.parse import urlencode as urllib_urlencode import weakref +import async_timeout from awesomeversion import AwesomeVersion import jinja2 from jinja2 import pass_context, pass_environment, pass_eval_context from jinja2.sandbox import ImmutableSandboxedEnvironment from jinja2.utils import Namespace -from typing_extensions import Concatenate, ParamSpec import voluptuous as vol from homeassistant.const import ( @@ -255,20 +264,39 @@ class RenderInfo: def __repr__(self) -> str: """Representation of RenderInfo.""" - return f" has_time={self.has_time}" + return ( + f"" + ) def _filter_domains_and_entities(self, entity_id: str) -> bool: - """Template should re-render if the entity state changes when we match specific domains or entities.""" + """Template should re-render if the entity state changes. + + Only when we match specific domains or entities. + """ return ( split_entity_id(entity_id)[0] in self.domains or entity_id in self.entities ) def _filter_entities(self, entity_id: str) -> bool: - """Template should re-render if the entity state changes when we match specific entities.""" + """Template should re-render if the entity state changes. + + Only when we match specific entities. + """ return entity_id in self.entities def _filter_lifecycle_domains(self, entity_id: str) -> bool: - """Template should re-render if the entity is added or removed with domains watched.""" + """Template should re-render if the entity is added or removed. + + Only with domains watched. + """ return split_entity_id(entity_id)[0] in self.domains_lifecycle def result(self) -> str: @@ -359,7 +387,11 @@ class Template: wanted_env = _ENVIRONMENT ret: TemplateEnvironment | None = self.hass.data.get(wanted_env) if ret is None: - ret = self.hass.data[wanted_env] = TemplateEnvironment(self.hass, self._limited, self._strict) # type: ignore[no-untyped-call] + ret = self.hass.data[wanted_env] = TemplateEnvironment( + self.hass, + self._limited, # type: ignore[no-untyped-call] + self._strict, + ) return ret def ensure_valid(self) -> None: @@ -382,7 +414,8 @@ class Template: ) -> Any: """Render given template. - If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine. + If limited is True, the template is not allowed to access any function + or filter depending on hass or the state machine. """ if self.is_static: if not parse_result or self.hass and self.hass.config.legacy_templates: @@ -407,7 +440,8 @@ class Template: This method must be run in the event loop. - If limited is True, the template is not allowed to access any function or filter depending on hass or the state machine. + If limited is True, the template is not allowed to access any function + or filter depending on hass or the state machine. """ if self.is_static: if not parse_result or self.hass and self.hass.config.legacy_templates: @@ -509,7 +543,8 @@ class Template: try: template_render_thread = ThreadWithException(target=_render_template) template_render_thread.start() - await asyncio.wait_for(finish_event.wait(), timeout=timeout) + async with async_timeout.timeout(timeout): + await finish_event.wait() if self._exc_info: raise TemplateError(self._exc_info[1].with_traceback(self._exc_info[2])) except asyncio.TimeoutError: @@ -1039,11 +1074,11 @@ def device_entities(hass: HomeAssistant, _device_id: str) -> Iterable[str]: def integration_entities(hass: HomeAssistant, entry_name: str) -> Iterable[str]: - """ - Get entity ids for entities tied to an integration/domain. + """Get entity ids for entities tied to an integration/domain. Provide entry_name as domain to get all entity id's for a integration/domain - or provide a config entry title for filtering between instances of the same integration. + or provide a config entry title for filtering between instances of the same + integration. """ # first try if this is a config entry match conf_entry = next( @@ -1060,7 +1095,7 @@ def integration_entities(hass: HomeAssistant, entry_name: str) -> Iterable[str]: return [entry.entity_id for entry in entries] # fallback to just returning all entities for a domain - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .entity import entity_sources return [ @@ -1643,8 +1678,7 @@ def fail_when_undefined(value): def min_max_from_filter(builtin_filter: Any, name: str) -> Any: - """ - Convert a built-in min/max Jinja filter to a global function. + """Convert a built-in min/max Jinja filter to a global function. The parameters may be passed as an iterable or as separate arguments. """ @@ -1667,16 +1701,17 @@ def min_max_from_filter(builtin_filter: Any, name: str) -> Any: def average(*args: Any, default: Any = _SENTINEL) -> Any: - """ - Filter and function to calculate the arithmetic mean of an iterable or of two or more arguments. + """Filter and function to calculate the arithmetic mean. + + Calculates of an iterable or of two or more arguments. The parameters may be passed as an iterable or as separate arguments. """ if len(args) == 0: raise TypeError("average expected at least 1 argument, got 0") - # If first argument is iterable and more than 1 argument provided but not a named default, - # then use 2nd argument as default. + # If first argument is iterable and more than 1 argument provided but not a named + # default, then use 2nd argument as default. if isinstance(args[0], Iterable): average_list = args[0] if len(args) > 1 and default is _SENTINEL: @@ -1884,8 +1919,7 @@ def today_at(time_str: str = "") -> datetime: def relative_time(value): - """ - Take a datetime and return its "age" as a string. + """Take a datetime and return its "age" as a string. The age can be in second, minute, hour, day, month or year. Only the biggest unit is considered, e.g. if it's 2 days and 3 hours, "2 days" will @@ -1953,7 +1987,7 @@ def _render_with_context( class LoggingUndefined(jinja2.Undefined): """Log on undefined variables.""" - def _log_message(self): + def _log_message(self) -> None: template, action = template_cv.get() or ("", "rendering or compiling") _LOGGER.warning( "Template variable warning: %s when %s '%s'", @@ -1975,7 +2009,7 @@ class LoggingUndefined(jinja2.Undefined): ) raise ex - def __str__(self): + def __str__(self) -> str: """Log undefined __str___.""" self._log_message() return super().__str__() @@ -1985,7 +2019,7 @@ class LoggingUndefined(jinja2.Undefined): self._log_message() return super().__iter__() - def __bool__(self): + def __bool__(self) -> bool: """Log undefined __bool___.""" self._log_message() return super().__bool__() @@ -1996,13 +2030,16 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): def __init__(self, hass, limited=False, strict=False): """Initialise template environment.""" + undefined: type[LoggingUndefined] | type[jinja2.StrictUndefined] if not strict: undefined = LoggingUndefined else: undefined = jinja2.StrictUndefined super().__init__(undefined=undefined) self.hass = hass - self.template_cache = weakref.WeakValueDictionary() + self.template_cache: weakref.WeakValueDictionary[ + str | jinja2.nodes.Template, CodeType | str | None + ] = weakref.WeakValueDictionary() self.filters["round"] = forgiving_round self.filters["multiply"] = multiply self.filters["log"] = logarithm @@ -2048,6 +2085,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["iif"] = iif self.filters["bool"] = forgiving_boolean self.filters["version"] = version + self.filters["contains"] = contains self.globals["log"] = logarithm self.globals["sin"] = sine self.globals["cos"] = cosine @@ -2084,6 +2122,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.tests["is_number"] = is_number self.tests["match"] = regex_match self.tests["search"] = regex_search + self.tests["contains"] = contains if hass is None: return @@ -2138,8 +2177,8 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): if limited: # Only device_entities is available to limited templates, mark other # functions and filters as unsupported. - def unsupported(name): - def warn_unsupported(*args, **kwargs): + def unsupported(name: str) -> Callable[[], NoReturn]: + def warn_unsupported(*args: Any, **kwargs: Any) -> NoReturn: raise TemplateError( f"Use of '{name}' is not supported in limited templates" ) @@ -2247,7 +2286,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): defer_init, ) - cached: CodeType | str | None if (cached := self.template_cache.get(source)) is None: cached = self.template_cache[source] = super().compile(source) diff --git a/homeassistant/helpers/template_entity.py b/homeassistant/helpers/template_entity.py index e824b2f2c8b..3de42f8fc98 100644 --- a/homeassistant/helpers/template_entity.py +++ b/homeassistant/helpers/template_entity.py @@ -268,8 +268,7 @@ class TemplateEntity(Entity): on_update: Callable[[Any], None] | None = None, none_on_template_error: bool = False, ) -> None: - """ - Call in the constructor to add a template linked to a attribute. + """Call in the constructor to add a template linked to a attribute. Parameters ---------- @@ -284,6 +283,8 @@ class TemplateEntity(Entity): Called to store the template result rather than storing it the supplied attribute. Passed the result of the validator, or None if the template or validator resulted in an error. + none_on_template_error + If True, the attribute will be set to None if the template errors. """ assert self.hass is not None, "hass cannot be None" diff --git a/homeassistant/helpers/translation.py b/homeassistant/helpers/translation.py index 049a37e0086..0c59035fc76 100644 --- a/homeassistant/helpers/translation.py +++ b/homeassistant/helpers/translation.py @@ -2,7 +2,6 @@ from __future__ import annotations import asyncio -from collections import ChainMap from collections.abc import Iterable, Mapping import logging from typing import Any @@ -258,7 +257,9 @@ class _TranslationCache: _merge_resources if category == "state" else _build_resources ) new_resources: Mapping[str, dict[str, Any] | str] - new_resources = resource_func(translation_strings, components, category) # type: ignore[assignment] + new_resources = resource_func( # type: ignore[assignment] + translation_strings, components, category + ) for component, resource in new_resources.items(): category_cache: dict[str, Any] = cached.setdefault( @@ -311,4 +312,7 @@ async def async_get_translations( cache = hass.data[TRANSLATION_FLATTEN_CACHE] = _TranslationCache(hass) cached = await cache.async_fetch(language, category, components) - return dict(ChainMap(*cached)) + result = {} + for entry in cached: + result.update(entry) + return result diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index 1bf9874988d..1e364878f03 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -127,7 +127,10 @@ class PluggableAction: action: TriggerActionType, variables: dict[str, Any], ) -> CALLBACK_TYPE: - """Attach an action to a trigger entry. Existing or future plugs registered will be attached.""" + """Attach an action to a trigger entry. + + Existing or future plugs registered will be attached. + """ reg = PluggableAction.async_get_registry(hass) key = tuple(sorted(trigger.items())) entry = reg[key] @@ -163,7 +166,10 @@ class PluggableAction: @callback def _remove() -> None: - """Remove plug from registration, and clean up entry if there are no actions or plugs registered.""" + """Remove plug from registration. + + Clean up entry if there are no actions or plugs registered. + """ assert self._entry self._entry.plugs.remove(self) if not self._entry.actions and not self._entry.plugs: diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 0e3edab71b0..326d2f98259 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -1,7 +1,7 @@ """Typing Helpers for Home Assistant.""" from collections.abc import Mapping from enum import Enum -from typing import Any, Optional, Union +from typing import Any import homeassistant.core @@ -11,8 +11,8 @@ ContextType = homeassistant.core.Context DiscoveryInfoType = dict[str, Any] EventType = homeassistant.core.Event ServiceDataType = dict[str, Any] -StateType = Union[None, str, int, float] -TemplateVarsType = Optional[Mapping[str, Any]] +StateType = str | int | float | None +TemplateVarsType = Mapping[str, Any] | None # Custom type for recorder Queries QueryType = Any diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 205a7848613..9f9b1a30ba6 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -1,13 +1,14 @@ """Helpers to help coordinate updates.""" from __future__ import annotations +from abc import abstractmethod import asyncio from collections.abc import Awaitable, Callable, Coroutine, Generator from datetime import datetime, timedelta import logging from random import randint from time import monotonic -from typing import Any, Generic, TypeVar +from typing import Any, Generic, Protocol, TypeVar import urllib.error import aiohttp @@ -29,6 +30,9 @@ REQUEST_REFRESH_DEFAULT_COOLDOWN = 10 REQUEST_REFRESH_DEFAULT_IMMEDIATE = True _T = TypeVar("_T") +_BaseDataUpdateCoordinatorT = TypeVar( + "_BaseDataUpdateCoordinatorT", bound="BaseDataUpdateCoordinatorProtocol" +) _DataUpdateCoordinatorT = TypeVar( "_DataUpdateCoordinatorT", bound="DataUpdateCoordinator[Any]" ) @@ -38,7 +42,17 @@ class UpdateFailed(Exception): """Raised when an update has failed.""" -class DataUpdateCoordinator(Generic[_T]): +class BaseDataUpdateCoordinatorProtocol(Protocol): + """Base protocol type for DataUpdateCoordinator.""" + + @callback + def async_add_listener( + self, update_callback: CALLBACK_TYPE, context: Any = None + ) -> Callable[[], None]: + """Listen for data updates.""" + + +class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_T]): """Class to manage fetching data from single endpoint.""" def __init__( @@ -346,11 +360,11 @@ class DataUpdateCoordinator(Generic[_T]): self.async_update_listeners() -class CoordinatorEntity(entity.Entity, Generic[_DataUpdateCoordinatorT]): - """A class for entities using DataUpdateCoordinator.""" +class BaseCoordinatorEntity(entity.Entity, Generic[_BaseDataUpdateCoordinatorT]): + """Base class for all Coordinator entities.""" def __init__( - self, coordinator: _DataUpdateCoordinatorT, context: Any = None + self, coordinator: _BaseDataUpdateCoordinatorT, context: Any = None ) -> None: """Create the entity with a DataUpdateCoordinator.""" self.coordinator = coordinator @@ -361,11 +375,6 @@ class CoordinatorEntity(entity.Entity, Generic[_DataUpdateCoordinatorT]): """No need to poll. Coordinator notifies entity of updates.""" return False - @property - def available(self) -> bool: - """Return if entity is available.""" - return self.coordinator.last_update_success - async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() @@ -380,6 +389,33 @@ class CoordinatorEntity(entity.Entity, Generic[_DataUpdateCoordinatorT]): """Handle updated data from the coordinator.""" self.async_write_ha_state() + @abstractmethod + async def async_update(self) -> None: + """Update the entity. + + Only used by the generic entity update service. + """ + + +class CoordinatorEntity(BaseCoordinatorEntity[_DataUpdateCoordinatorT]): + """A class for entities using DataUpdateCoordinator.""" + + def __init__( + self, coordinator: _DataUpdateCoordinatorT, context: Any = None + ) -> None: + """Create the entity with a DataUpdateCoordinator. + + Passthrough to BaseCoordinatorEntity. + + Necessary to bind TypeVar to correct scope. + """ + super().__init__(coordinator, context) + + @property + def available(self) -> bool: + """Return if entity is available.""" + return self.coordinator.last_update_success + async def async_update(self) -> None: """Update the entity. diff --git a/homeassistant/loader.py b/homeassistant/loader.py index da3f2aaa6d3..ef44f43f818 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -123,8 +123,9 @@ class Manifest(TypedDict, total=False): """ Integration manifest. - Note that none of the attributes are marked Optional here. However, some of them may be optional in manifest.json - in the sense that they can be omitted altogether. But when present, they should not have null values in it. + Note that none of the attributes are marked Optional here. However, some of + them may be optional in manifest.json in the sense that they can be omitted + altogether. But when present, they should not have null values in it. """ name: str @@ -228,7 +229,7 @@ async def async_get_config_flows( type_filter: Literal["device", "helper", "hub", "service"] | None = None, ) -> set[str]: """Return cached list of config flows.""" - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from .generated.config_flows import FLOWS integrations = await async_get_custom_components(hass) @@ -338,7 +339,9 @@ async def async_get_zeroconf( hass: HomeAssistant, ) -> dict[str, list[dict[str, str | dict[str, str]]]]: """Return cached list of zeroconf types.""" - zeroconf: dict[str, list[dict[str, str | dict[str, str]]]] = ZEROCONF.copy() # type: ignore[assignment] + zeroconf: dict[ + str, list[dict[str, str | dict[str, str]]] + ] = ZEROCONF.copy() # type: ignore[assignment] integrations = await async_get_custom_components(hass) for integration in integrations.values(): @@ -496,7 +499,8 @@ class Integration: ( "The custom integration '%s' does not have a version key in the" " manifest file and was blocked from loading. See" - " https://developers.home-assistant.io/blog/2021/01/29/custom-integration-changes#versions" + " https://developers.home-assistant.io" + "/blog/2021/01/29/custom-integration-changes#versions" " for more details" ), integration.domain, @@ -518,7 +522,8 @@ class Integration: ( "The custom integration '%s' does not have a valid version key" " (%s) in the manifest file and was blocked from loading. See" - " https://developers.home-assistant.io/blog/2021/01/29/custom-integration-changes#versions" + " https://developers.home-assistant.io" + "/blog/2021/01/29/custom-integration-changes#versions" " for more details" ), integration.domain, @@ -895,7 +900,9 @@ def _load_file( Async friendly. """ with suppress(KeyError): - return hass.data[DATA_COMPONENTS][comp_or_platform] # type: ignore[no-any-return] + return hass.data[DATA_COMPONENTS][ # type: ignore[no-any-return] + comp_or_platform + ] 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 717d2f3e9a4..9101375adb3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,42 +4,45 @@ aiodiscover==1.4.13 aiohttp==3.8.1 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.33.0 +async-upnp-client==0.33.1 async_timeout==4.0.2 atomicwrites-homeassistant==1.4.1 -attrs==22.1.0 +attrs==22.2.0 awesomeversion==22.9.0 -bcrypt==3.1.7 +bcrypt==4.0.1 bleak-retry-connector==2.13.0 -bleak==0.19.2 +bleak==0.19.5 bluetooth-adapters==0.15.2 bluetooth-auto-recovery==1.0.3 bluetooth-data-tools==0.3.1 certifi>=2021.5.30 ciso8601==2.3.0 -cryptography==38.0.3 -dbus-fast==1.82.0 +cryptography==39.0.0 +dbus-fast==1.84.0 fnvhash==0.1.0 hass-nabucasa==0.61.0 +hassil==0.2.6 home-assistant-bluetooth==1.9.2 -home-assistant-frontend==20230110.0 -httpx==0.23.2 +home-assistant-frontend==20230201.0 +home-assistant-intents==2023.1.31 +httpx==0.23.3 ifaddr==0.1.7 janus==1.0.0 jinja2==3.1.2 lru-dict==1.1.8 -orjson==3.8.1 +orjson==3.8.5 paho-mqtt==1.6.1 -pillow==9.3.0 +pillow==9.4.0 pip>=21.0,<22.4 psutil-home-assistant==0.0.1 +pyOpenSSL==23.0.0 pyserial==3.5 python-slugify==4.0.1 pyudev==0.23.2 pyyaml==6.0 requests==2.28.1 -scapy==2.4.5 -sqlalchemy==1.4.44 +scapy==2.5.0 +sqlalchemy==1.4.45 typing-extensions>=4.4.0,<5.0 voluptuous-serialize==2.5.0 voluptuous==0.13.1 @@ -62,6 +65,7 @@ httplib2>=0.19.0 # want to ensure we have wheels built. grpcio==1.51.1 grpcio-status==1.51.1 +grpcio-reflection==1.51.1 # 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, @@ -133,3 +137,11 @@ pandas==1.4.3 # uamqp 1.6.1, has 1 failing test during built on armv7/armhf uamqp==1.6.0 + +# Matplotlib 3.6.2 has issues building wheels on armhf/armv7 +# We need at least >=2.1.0 (tensorflow integration -> pycocotools) +matplotlib==3.6.1 + +# pyOpenSSL 23.0.0 or later required to avoid import errors when +# cryptography 39.0.0 is installed with botocore +pyOpenSSL>=23.0.0 diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 5710c313903..30c5d0a2448 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -15,7 +15,8 @@ from .helpers.typing import UNDEFINED, UndefinedType from .loader import Integration, IntegrationNotFound, async_get_integration from .util import package as pkg_util -PIP_TIMEOUT = 60 # The default is too low when the internet connection is satellite or high latency +# The default is too low when the internet connection is satellite or high latency +PIP_TIMEOUT = 60 MAX_INSTALL_FAILURES = 3 DATA_REQUIREMENTS_MANAGER = "requirements_manager" CONSTRAINT_FILE = "package_constraints.txt" @@ -132,7 +133,7 @@ class RequirementsManager: async def async_get_integration_with_requirements( self, domain: str, done: set[str] | None = None ) -> Integration: - """Get an integration with all requirements installed, including the dependencies. + """Get an integration with all requirements installed, including dependencies. This can raise IntegrationNotFound if manifest or integration is invalid, RequirementNotFound if there was some type of @@ -257,7 +258,11 @@ class RequirementsManager: def _raise_for_failed_requirements( self, integration: str, missing: list[str] ) -> None: - """Raise RequirementsNotFound so we do not keep trying requirements that have already failed.""" + """Raise for failed installing integration requirements. + + Raise RequirementsNotFound so we do not keep trying requirements + that have already failed. + """ for req in missing: if req in self.install_failure_history: _LOGGER.info( diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 51b47e0fe2d..702a5c04501 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -90,11 +90,18 @@ 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[arg-type] + "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[arg-type] + logger.error( + "Error doing job: %s", + context["message"], + **kwargs, # type: ignore[arg-type] + ) async def setup_and_run_hass(runtime_config: RuntimeConfig) -> int: @@ -105,7 +112,8 @@ async def setup_and_run_hass(runtime_config: RuntimeConfig) -> int: return 1 # threading._shutdown can deadlock forever - threading._shutdown = deadlock_safe_shutdown # type: ignore[attr-defined] # pylint: disable=protected-access + # pylint: disable-next=protected-access + threading._shutdown = deadlock_safe_shutdown # type: ignore[attr-defined] return await hass.async_run() diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index d3165ad6cac..3627e4096d3 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -191,7 +191,10 @@ async def state_changed_event_helper(hass): @benchmark async def state_changed_event_filter_helper(hass): - """Run a million events through state changed event helper with 1000 entities that all get filtered.""" + """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 @@ -350,7 +353,7 @@ def _create_state_changed_event_from_old_new( row.old_state_id = old_state and 1 row.state_id = new_state and 1 - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from homeassistant.components import logbook return logbook.LazyEventPartialState(row, {}) diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index cff40d2535c..85d0e77a4e3 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -40,7 +40,7 @@ ERROR_STR = "General Errors" def color(the_color, *args, reset=None): """Color helper.""" - # pylint: disable=import-outside-toplevel + # pylint: disable-next=import-outside-toplevel from colorlog.escape_codes import escape_codes, parse_colors try: diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 13467713e2d..9740d338eff 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -30,24 +30,27 @@ ATTR_COMPONENT = "component" BASE_PLATFORMS = {platform.value for platform in Platform} # DATA_SETUP is a dict[str, asyncio.Task[bool]], indicating domains which are currently -# being setup or which failed to setup -# - Tasks are added to DATA_SETUP by `async_setup_component`, the key is the domain being setup -# and the Task is the `_async_setup_component` helper. -# - Tasks are removed from DATA_SETUP if setup was successful, that is, the task returned True +# being setup or which failed to setup: +# - Tasks are added to DATA_SETUP by `async_setup_component`, the key is the domain +# being setup and the Task is the `_async_setup_component` helper. +# - Tasks are removed from DATA_SETUP if setup was successful, that is, +# the task returned True. DATA_SETUP = "setup_tasks" -# DATA_SETUP_DONE is a dict [str, asyncio.Event], indicating components which will be setup -# - Events are added to DATA_SETUP_DONE during bootstrap by async_set_domains_to_be_loaded, -# the key is the domain which will be loaded -# - Events are set and removed from DATA_SETUP_DONE when async_setup_component is finished, -# regardless of if the setup was successful or not. +# DATA_SETUP_DONE is a dict [str, asyncio.Event], indicating components which +# will be setup: +# - Events are added to DATA_SETUP_DONE during bootstrap by +# async_set_domains_to_be_loaded, the key is the domain which will be loaded. +# - Events are set and removed from DATA_SETUP_DONE when async_setup_component +# is finished, regardless of if the setup was successful or not. DATA_SETUP_DONE = "setup_done" -# DATA_SETUP_DONE is a dict [str, datetime], indicating when an attempt to setup a component -# started +# DATA_SETUP_DONE is a dict [str, datetime], indicating when an attempt +# to setup a component started. DATA_SETUP_STARTED = "setup_started" -# DATA_SETUP_TIME is a dict [str, timedelta], indicating how time was spent setting up a component +# DATA_SETUP_TIME is a dict [str, timedelta], indicating how time was spent +# setting up a component. DATA_SETUP_TIME = "setup_time" DATA_DEPS_REQS = "deps_reqs_processed" @@ -283,7 +286,7 @@ async def _async_setup_component( # Flush out async_setup calling create_task. Fragile but covered by test. await asyncio.sleep(0) - await hass.config_entries.flow.async_wait_init_flow_finish(domain) + await hass.config_entries.flow.async_wait_import_flow_initialized(domain) # Add to components before the entry.async_setup # call to avoid a deadlock when forwarding platforms diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index eb3dabe75a0..0a4b156fc8c 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -23,8 +23,7 @@ RE_SANITIZE_PATH = re.compile(r"(~|\.(\.)+)") def raise_if_invalid_filename(filename: str) -> None: - """ - Check if a filename is valid. + """Check if a filename is valid. Raises a ValueError if the filename is invalid. """ @@ -33,8 +32,7 @@ def raise_if_invalid_filename(filename: str) -> None: def raise_if_invalid_path(path: str) -> None: - """ - Check if a path is valid. + """Check if a path is valid. Raises a ValueError if the path is invalid. """ @@ -172,7 +170,7 @@ class Throttle: else: host = args[0] if args else wrapper - # pylint: disable=protected-access # to _throttle + # pylint: disable=protected-access if not hasattr(host, "_throttle"): host._throttle = {} diff --git a/homeassistant/util/aiohttp.py b/homeassistant/util/aiohttp.py index 05ade335a53..8248864fd0e 100644 --- a/homeassistant/util/aiohttp.py +++ b/homeassistant/util/aiohttp.py @@ -84,7 +84,7 @@ def serialize_response(response: web.Response) -> dict[str, Any]: if (body := response.body) is None: body_decoded = None elif isinstance(body, payload.StringPayload): - # pylint: disable=protected-access + # pylint: disable-next=protected-access body_decoded = body._value.decode(body.encoding) elif isinstance(body, bytes): body_decoded = body.decode(response.charset or "utf-8") diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index 53c788436fe..f5164da4808 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -9,9 +9,7 @@ import functools import logging import threading from traceback import extract_stack -from typing import Any, TypeVar - -from typing_extensions import ParamSpec +from typing import Any, ParamSpec, TypeVar _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 3823c0e45bd..6ccb7f14ea2 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -510,8 +510,7 @@ def color_temperature_to_hs(color_temperature_kelvin: float) -> tuple[float, flo def color_temperature_to_rgb( color_temperature_kelvin: float, ) -> tuple[float, float, float]: - """ - Return an RGB color from a color temperature in Kelvin. + """Return an RGB color from a color temperature in Kelvin. This is a rough approximation based on the formula provided by T. Helland http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/ @@ -581,8 +580,7 @@ def _white_levels_to_color_temperature( def _clamp(color_component: float, minimum: float = 0, maximum: float = 255) -> float: - """ - Clamp the given color component value between the given min and max values. + """Clamp the given color component value between the given min and max values. The range defined by the minimum and maximum values is inclusive, i.e. given a color_component of 0 and a minimum of 10, the returned value is 10. @@ -644,8 +642,7 @@ def get_distance_between_two_points(one: XYPoint, two: XYPoint) -> float: def get_closest_point_to_line(A: XYPoint, B: XYPoint, P: XYPoint) -> XYPoint: - """ - Find the closest point from P to a line defined by A and B. + """Find the closest point from P to a line defined by A and B. This point will be reproducible by the lamp as it is on the edge of the gamut. @@ -667,8 +664,7 @@ def get_closest_point_to_line(A: XYPoint, B: XYPoint, P: XYPoint) -> XYPoint: def get_closest_point_to_point( xy_tuple: tuple[float, float], Gamut: GamutType ) -> tuple[float, float]: - """ - Get the closest matching color within the gamut of the light. + """Get the closest matching color within the gamut of the light. Should only be used if the supplied color is outside of the color gamut. """ diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 44e4403d689..26f001236ec 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -4,6 +4,7 @@ from __future__ import annotations import bisect from contextlib import suppress import datetime as dt +from functools import partial import platform import re import time @@ -98,9 +99,10 @@ def get_time_zone(time_zone_str: str) -> dt.tzinfo | None: return None -def utcnow() -> dt.datetime: - """Get now in UTC time.""" - return dt.datetime.now(UTC) +# We use a partial here since it is implemented in native code +# and avoids the global lookup of UTC +utcnow: partial[dt.datetime] = partial(dt.datetime.now, UTC) +utcnow.__doc__ = "Get now in UTC time." def now(time_zone: dt.tzinfo | None = None) -> dt.datetime: @@ -265,8 +267,7 @@ def parse_time(time_str: str) -> dt.time | None: def get_age(date: dt.datetime) -> str: - """ - Take a datetime and return its "age" as a string. + """Take a datetime and return its "age" as a string. The age can be in second, minute, hour, day, month or year. Only the biggest unit is considered, e.g. if it's 2 days and 3 hours, "2 days" will @@ -326,7 +327,9 @@ def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> lis def _dst_offset_diff(dattim: dt.datetime) -> dt.timedelta: """Return the offset when crossing the DST barrier.""" delta = dt.timedelta(hours=24) - return (dattim + delta).utcoffset() - (dattim - delta).utcoffset() # type: ignore[operator] + return (dattim + delta).utcoffset() - ( # type: ignore[operator] + dattim - delta + ).utcoffset() def _lower_bound(arr: list[int], cmp: int) -> int | None: @@ -358,7 +361,8 @@ def find_next_time_expression_time( raise ValueError("Cannot find a next time: Time expression never matches!") while True: - # Reset microseconds and fold; fold (for ambiguous DST times) will be handled later + # Reset microseconds and fold; fold (for ambiguous DST times) will be + # handled later. result = now.replace(microsecond=0, fold=0) # Match next second @@ -406,11 +410,12 @@ def find_next_time_expression_time( # -> trigger on the next time that 1. matches the pattern and 2. does exist # for example: # on 2021.03.28 02:00:00 in CET timezone clocks are turned forward an hour - # with pattern "02:30", don't run on 28 mar (such a wall time does not exist on this day) - # instead run at 02:30 the next day + # with pattern "02:30", don't run on 28 mar (such a wall time does not + # exist on this day) instead run at 02:30 the next day - # We solve this edge case by just iterating one second until the result exists - # (max. 3600 operations, which should be fine for an edge case that happens once a year) + # We solve this edge case by just iterating one second until the result + # exists (max. 3600 operations, which should be fine for an edge case that + # happens once a year) now += dt.timedelta(seconds=1) continue @@ -418,29 +423,34 @@ def find_next_time_expression_time( return result # When leaving DST and clocks are turned backward. - # Then there are wall clock times that are ambiguous i.e. exist with DST and without DST - # The logic above does not take into account if a given pattern matches _twice_ - # in a day. - # Example: on 2021.10.31 02:00:00 in CET timezone clocks are turned backward an hour + # Then there are wall clock times that are ambiguous i.e. exist with DST and + # without DST. The logic above does not take into account if a given pattern + # matches _twice_ in a day. + # Example: on 2021.10.31 02:00:00 in CET timezone clocks are turned + # backward an hour. if _datetime_ambiguous(result): # `now` and `result` are both ambiguous, so the next match happens # _within_ the current fold. # Examples: - # 1. 2021.10.31 02:00:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+02:00 - # 2. 2021.10.31 02:00:00+01:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 + # 1. 2021.10.31 02:00:00+02:00 with pattern 02:30 + # -> 2021.10.31 02:30:00+02:00 + # 2. 2021.10.31 02:00:00+01:00 with pattern 02:30 + # -> 2021.10.31 02:30:00+01:00 return result.replace(fold=now.fold) if now.fold == 0: - # `now` is in the first fold, but result is not ambiguous (meaning it no longer matches - # within the fold). - # -> Check if result matches in the next fold. If so, emit that match + # `now` is in the first fold, but result is not ambiguous (meaning it no + # longer matches within the fold). + # -> Check if result matches in the next fold. If so, emit that match - # Turn back the time by the DST offset, effectively run the algorithm on the first fold - # If it matches on the first fold, that means it will also match on the second one. + # Turn back the time by the DST offset, effectively run the algorithm on + # the first fold. If it matches on the first fold, that means it will also + # match on the second one. - # Example: 2021.10.31 02:45:00+02:00 with pattern 02:30 -> 2021.10.31 02:30:00+01:00 + # Example: 2021.10.31 02:45:00+02:00 with pattern 02:30 + # -> 2021.10.31 02:30:00+01:00 check_result = find_next_time_expression_time( now + _dst_offset_diff(now), seconds, minutes, hours @@ -466,8 +476,8 @@ def _datetime_ambiguous(dattim: dt.datetime) -> bool: return _datetime_exists(dattim) and dattim.utcoffset() != opposite_fold.utcoffset() -def __monotonic_time_coarse() -> float: - """Return a monotonic time in seconds. +def __gen_monotonic_time_coarse() -> partial[float]: + """Return a function that provides monotonic time in seconds. This is the coarse version of time_monotonic, which is faster but less accurate. @@ -477,13 +487,16 @@ def __monotonic_time_coarse() -> float: https://lore.kernel.org/lkml/20170404171826.25030-1-marc.zyngier@arm.com/ """ - return time.clock_gettime(CLOCK_MONOTONIC_COARSE) + # We use a partial here since its implementation is in native code + # which allows us to avoid the overhead of the global lookup + # of CLOCK_MONOTONIC_COARSE. + return partial(time.clock_gettime, CLOCK_MONOTONIC_COARSE) monotonic_time_coarse = time.monotonic with suppress(Exception): if ( platform.system() == "Linux" - and abs(time.monotonic() - __monotonic_time_coarse()) < 1 + and abs(time.monotonic() - __gen_monotonic_time_coarse()()) < 1 ): - monotonic_time_coarse = __monotonic_time_coarse + monotonic_time_coarse = __gen_monotonic_time_coarse() diff --git a/homeassistant/util/json.py b/homeassistant/util/json.py index eb71d9da7eb..a31db4f8d1b 100644 --- a/homeassistant/util/json.py +++ b/homeassistant/util/json.py @@ -124,7 +124,8 @@ def find_paths_unserializable_data( except (ValueError, TypeError): pass - # We convert objects with as_dict to their dict values so we can find bad data inside it + # We convert objects with as_dict to their dict values + # so we can find bad data inside it if hasattr(obj, "as_dict"): desc = obj.__class__.__name__ if isinstance(obj, State): diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index b4d7274ded7..407ad3881cd 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -79,8 +79,7 @@ def distance( def vincenty( point1: tuple[float, float], point2: tuple[float, float], miles: bool = False ) -> float | None: - """ - Vincenty formula (inverse method) to calculate the distance. + """Vincenty formula (inverse method) to calculate the distance. Result in kilometers or miles between two points on the surface of a spheroid. diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index e493a3378fd..0595e4bb90a 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -11,7 +11,6 @@ import queue import traceback from typing import Any, TypeVar, cast, overload -from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.core import HomeAssistant, callback, is_callback _T = TypeVar("_T") @@ -35,6 +34,8 @@ class HideSensitiveDataFilter(logging.Filter): class HomeAssistantQueueHandler(logging.handlers.QueueHandler): """Process the log in another thread.""" + listener: logging.handlers.QueueListener | None = None + def prepare(self, record: logging.LogRecord) -> logging.LogRecord: """Prepare a record for queuing. @@ -62,6 +63,18 @@ class HomeAssistantQueueHandler(logging.handlers.QueueHandler): self.emit(record) return return_value + def close(self) -> None: + """ + Tidy up any resources used by the handler. + + This adds shutdown of the QueueListener + """ + super().close() + if not self.listener: + return + self.listener.stop() + self.listener = None + @callback def async_activate_log_queue_handler(hass: HomeAssistant) -> None: @@ -83,20 +96,10 @@ def async_activate_log_queue_handler(hass: HomeAssistant) -> None: migrated_handlers.append(handler) listener = logging.handlers.QueueListener(simple_queue, *migrated_handlers) + queue_handler.listener = listener listener.start() - @callback - def _async_stop_queue_handler(_: Any) -> None: - """Cleanup handler.""" - # Ensure any messages that happen after close still get logged - for original_handler in migrated_handlers: - logging.root.addHandler(original_handler) - logging.root.removeHandler(queue_handler) - listener.stop() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_stop_queue_handler) - def log_exception(format_err: Callable[..., Any], *args: Any) -> None: """Log an exception with additional context.""" diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index 49ab3c10f8c..b67e9923b9c 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -52,7 +52,9 @@ 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[unreachable] + _LOGGER.error( # type: ignore[unreachable] + "Installed version for %s resolved to None", req.project_name + ) return False return installed_version in req except PackageNotFoundError: diff --git a/homeassistant/util/pil.py b/homeassistant/util/pil.py index 7caeac15458..068b807cbe5 100644 --- a/homeassistant/util/pil.py +++ b/homeassistant/util/pil.py @@ -15,8 +15,7 @@ def draw_box( text: str = "", color: tuple[int, int, int] = (255, 255, 0), ) -> None: - """ - Draw a bounding box on and image. + """Draw a bounding box on and image. The bounding box is defined by the tuple (y_min, x_min, y_max, x_max) where the coordinates are floats in the range [0.0, 1.0] and diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index eccd358ad81..78a69e15a34 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -20,7 +20,7 @@ from homeassistant.helpers.frame import report from .unit_conversion import PressureConverter # pylint: disable-next=protected-access -UNIT_CONVERSION: dict[str, float] = PressureConverter._UNIT_CONVERSION +UNIT_CONVERSION: dict[str | None, float] = PressureConverter._UNIT_CONVERSION VALID_UNITS = PressureConverter.VALID_UNITS diff --git a/homeassistant/util/speed.py b/homeassistant/util/speed.py index de076701c55..a1b6e0a7227 100644 --- a/homeassistant/util/speed.py +++ b/homeassistant/util/speed.py @@ -27,7 +27,7 @@ from .unit_conversion import ( # pylint: disable=unused-import # noqa: F401 ) # pylint: disable-next=protected-access -UNIT_CONVERSION: dict[str, float] = SpeedConverter._UNIT_CONVERSION +UNIT_CONVERSION: dict[str | None, float] = SpeedConverter._UNIT_CONVERSION VALID_UNITS = SpeedConverter.VALID_UNITS diff --git a/homeassistant/util/ssl.py b/homeassistant/util/ssl.py index 4f10809ff21..ffeefe3d2c9 100644 --- a/homeassistant/util/ssl.py +++ b/homeassistant/util/ssl.py @@ -8,12 +8,12 @@ import certifi def client_context() -> ssl.SSLContext: """Return an SSL context for making requests.""" - # Reuse environment variable definition from requests, since it's already a requirement - # If the environment variable has no value, fall back to using certs from certifi package + # Reuse environment variable definition from requests, since it's already a + # requirement. If the environment variable has no value, fall back to using + # certs from certifi package. cafile = environ.get("REQUESTS_CA_BUNDLE", certifi.where()) - context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=cafile) - return context + return ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=cafile) def server_context_modern() -> ssl.SSLContext: diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index f9f4d78899a..0fde15acd71 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -2,6 +2,7 @@ from __future__ import annotations from homeassistant.const import ( + PERCENTAGE, UNIT_NOT_RECOGNIZED_TEMPLATE, UnitOfDataRate, UnitOfElectricCurrent, @@ -56,13 +57,13 @@ class BaseUnitConverter: """Define the format of a conversion utility.""" UNIT_CLASS: str - NORMALIZED_UNIT: str - VALID_UNITS: set[str] + NORMALIZED_UNIT: str | None + VALID_UNITS: set[str | None] - _UNIT_CONVERSION: dict[str, float] + _UNIT_CONVERSION: dict[str | None, float] @classmethod - def convert(cls, value: float, from_unit: str, to_unit: str) -> float: + def convert(cls, value: float, from_unit: str | None, to_unit: str | None) -> float: """Convert one unit of measurement to another.""" if from_unit == to_unit: return value @@ -85,7 +86,7 @@ class BaseUnitConverter: return new_value * to_ratio @classmethod - def get_unit_ratio(cls, from_unit: str, to_unit: str) -> float: + def get_unit_ratio(cls, from_unit: str | None, to_unit: str | None) -> float: """Get unit ratio between units of measurement.""" return cls._UNIT_CONVERSION[from_unit] / cls._UNIT_CONVERSION[to_unit] @@ -96,7 +97,7 @@ class DataRateConverter(BaseUnitConverter): UNIT_CLASS = "data_rate" NORMALIZED_UNIT = UnitOfDataRate.BITS_PER_SECOND # Units in terms of bits - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfDataRate.BITS_PER_SECOND: 1, UnitOfDataRate.KILOBITS_PER_SECOND: 1 / 1e3, UnitOfDataRate.MEGABITS_PER_SECOND: 1 / 1e6, @@ -117,7 +118,7 @@ class DistanceConverter(BaseUnitConverter): UNIT_CLASS = "distance" NORMALIZED_UNIT = UnitOfLength.METERS - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfLength.METERS: 1, UnitOfLength.MILLIMETERS: 1 / _MM_TO_M, UnitOfLength.CENTIMETERS: 1 / _CM_TO_M, @@ -144,7 +145,7 @@ class ElectricCurrentConverter(BaseUnitConverter): UNIT_CLASS = "electric_current" NORMALIZED_UNIT = UnitOfElectricCurrent.AMPERE - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfElectricCurrent.AMPERE: 1, UnitOfElectricCurrent.MILLIAMPERE: 1e3, } @@ -156,7 +157,7 @@ class ElectricPotentialConverter(BaseUnitConverter): UNIT_CLASS = "voltage" NORMALIZED_UNIT = UnitOfElectricPotential.VOLT - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfElectricPotential.VOLT: 1, UnitOfElectricPotential.MILLIVOLT: 1e3, } @@ -171,16 +172,18 @@ class EnergyConverter(BaseUnitConverter): UNIT_CLASS = "energy" NORMALIZED_UNIT = UnitOfEnergy.KILO_WATT_HOUR - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfEnergy.WATT_HOUR: 1 * 1000, UnitOfEnergy.KILO_WATT_HOUR: 1, UnitOfEnergy.MEGA_WATT_HOUR: 1 / 1000, + UnitOfEnergy.MEGA_JOULE: 3.6, UnitOfEnergy.GIGA_JOULE: 3.6 / 1000, } VALID_UNITS = { UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, UnitOfEnergy.MEGA_WATT_HOUR, + UnitOfEnergy.MEGA_JOULE, UnitOfEnergy.GIGA_JOULE, } @@ -191,7 +194,7 @@ class InformationConverter(BaseUnitConverter): UNIT_CLASS = "information" NORMALIZED_UNIT = UnitOfInformation.BITS # Units in terms of bits - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfInformation.BITS: 1, UnitOfInformation.KILOBITS: 1 / 1e3, UnitOfInformation.MEGABITS: 1 / 1e6, @@ -222,7 +225,7 @@ class MassConverter(BaseUnitConverter): UNIT_CLASS = "mass" NORMALIZED_UNIT = UnitOfMass.GRAMS - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfMass.MICROGRAMS: 1 * 1000 * 1000, UnitOfMass.MILLIGRAMS: 1 * 1000, UnitOfMass.GRAMS: 1, @@ -247,7 +250,7 @@ class PowerConverter(BaseUnitConverter): UNIT_CLASS = "power" NORMALIZED_UNIT = UnitOfPower.WATT - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfPower.WATT: 1, UnitOfPower.KILO_WATT: 1 / 1000, } @@ -262,7 +265,7 @@ class PressureConverter(BaseUnitConverter): UNIT_CLASS = "pressure" NORMALIZED_UNIT = UnitOfPressure.PA - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfPressure.PA: 1, UnitOfPressure.HPA: 1 / 100, UnitOfPressure.KPA: 1 / 1000, @@ -293,7 +296,7 @@ class SpeedConverter(BaseUnitConverter): UNIT_CLASS = "speed" NORMALIZED_UNIT = UnitOfSpeed.METERS_PER_SECOND - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfVolumetricFlux.INCHES_PER_DAY: _DAYS_TO_SECS / _IN_TO_M, UnitOfVolumetricFlux.INCHES_PER_HOUR: _HRS_TO_SECS / _IN_TO_M, UnitOfVolumetricFlux.MILLIMETERS_PER_DAY: _DAYS_TO_SECS / _MM_TO_M, @@ -334,7 +337,7 @@ class TemperatureConverter(BaseUnitConverter): } @classmethod - def convert(cls, value: float, from_unit: str, to_unit: str) -> float: + def convert(cls, value: float, from_unit: str | None, to_unit: str | None) -> float: """Convert a temperature from one unit to another. eg. 10°C will return 50°F @@ -342,8 +345,8 @@ class TemperatureConverter(BaseUnitConverter): For converting an interval between two temperatures, please use `convert_interval` instead. """ - # We cannot use the implementation from BaseUnitConverter here because the temperature - # units do not use the same floor: 0°C, 0°F and 0K do not align + # We cannot use the implementation from BaseUnitConverter here because the + # temperature units do not use the same floor: 0°C, 0°F and 0K do not align if from_unit == to_unit: return value @@ -411,13 +414,28 @@ class TemperatureConverter(BaseUnitConverter): return celsius + 273.15 +class UnitlessRatioConverter(BaseUnitConverter): + """Utility to convert unitless ratios.""" + + UNIT_CLASS = "unitless" + NORMALIZED_UNIT = None + _UNIT_CONVERSION: dict[str | None, float] = { + None: 1, + PERCENTAGE: 100, + } + VALID_UNITS = { + None, + PERCENTAGE, + } + + class VolumeConverter(BaseUnitConverter): """Utility to convert volume values.""" UNIT_CLASS = "volume" NORMALIZED_UNIT = UnitOfVolume.CUBIC_METERS # Units in terms of m³ - _UNIT_CONVERSION: dict[str, float] = { + _UNIT_CONVERSION: dict[str | None, float] = { UnitOfVolume.LITERS: 1 / _L_TO_CUBIC_METER, UnitOfVolume.MILLILITERS: 1 / _ML_TO_CUBIC_METER, UnitOfVolume.GALLONS: 1 / _GALLON_TO_CUBIC_METER, diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index 7aa910e90b2..194b8d82dbb 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -195,7 +195,9 @@ class UnitSystem: raise TypeError(f"{wind_speed!s} is not a numeric value.") # type ignore: https://github.com/python/mypy/issues/7207 - return SpeedConverter.convert(wind_speed, from_unit, self.wind_speed_unit) # type: ignore[unreachable] + return SpeedConverter.convert( # type: ignore[unreachable] + wind_speed, from_unit, self.wind_speed_unit + ) def volume(self, volume: float | None, from_unit: str) -> float: """Convert the given volume to this unit system.""" @@ -203,7 +205,9 @@ class UnitSystem: raise TypeError(f"{volume!s} is not a numeric value.") # type ignore: https://github.com/python/mypy/issues/7207 - return VolumeConverter.convert(volume, from_unit, self.volume_unit) # type: ignore[unreachable] + return VolumeConverter.convert( # type: ignore[unreachable] + volume, from_unit, self.volume_unit + ) def as_dict(self) -> dict[str, str]: """Convert the unit system to a dictionary.""" diff --git a/homeassistant/util/variance.py b/homeassistant/util/variance.py index 2e0835d1cfe..d28bdc9d63e 100644 --- a/homeassistant/util/variance.py +++ b/homeassistant/util/variance.py @@ -4,9 +4,7 @@ from __future__ import annotations from collections.abc import Callable from datetime import datetime, timedelta import functools -from typing import Any, TypeVar, overload - -from typing_extensions import ParamSpec +from typing import Any, ParamSpec, TypeVar, overload _R = TypeVar("_R", int, float, datetime) _P = ParamSpec("_P") diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index 626cf65d1e2..bf8a4e9541a 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -8,7 +8,7 @@ from io import StringIO, TextIOWrapper import logging import os from pathlib import Path -from typing import Any, TextIO, TypeVar, Union, overload +from typing import Any, TextIO, TypeVar, overload import yaml @@ -18,7 +18,9 @@ try: HAS_C_LOADER = True except ImportError: HAS_C_LOADER = False - from yaml import SafeLoader as FastestAvailableSafeLoader # type: ignore[assignment] + from yaml import ( # type: ignore[assignment] + SafeLoader as FastestAvailableSafeLoader, + ) from homeassistant.exceptions import HomeAssistantError @@ -27,7 +29,7 @@ from .objects import Input, NodeListClass, NodeStrClass # mypy: allow-untyped-calls, no-warn-return-any -JSON_TYPE = Union[list, dict, str] # pylint: disable=invalid-name +JSON_TYPE = list | dict | str # pylint: disable=invalid-name _DictT = TypeVar("_DictT", bound=dict) _LOGGER = logging.getLogger(__name__) @@ -132,10 +134,14 @@ class SafeLineLoader(yaml.SafeLoader): super().__init__(stream) self.secrets = secrets - def compose_node(self, parent: yaml.nodes.Node, index: int) -> yaml.nodes.Node: # type: ignore[override] + def compose_node( # type: ignore[override] + 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: yaml.nodes.Node = super().compose_node( # type: ignore[assignment] + parent, index + ) node.__line__ = last_line + 1 # type: ignore[attr-defined] return node @@ -148,7 +154,7 @@ class SafeLineLoader(yaml.SafeLoader): return getattr(self.stream, "name", "") -LoaderType = Union[SafeLineLoader, SafeLoader] +LoaderType = SafeLineLoader | SafeLoader def load_yaml(fname: str, secrets: Secrets | None = None) -> JSON_TYPE: @@ -226,7 +232,9 @@ def _add_reference(obj: _DictT, loader: LoaderType, node: yaml.nodes.Node) -> _D ... -def _add_reference(obj, loader: LoaderType, node: yaml.nodes.Node): # type: ignore[no-untyped-def] +def _add_reference( # type: ignore[no-untyped-def] + obj, loader: LoaderType, node: yaml.nodes.Node +): """Add file reference information to an object.""" if isinstance(obj, list): obj = NodeListClass(obj) @@ -337,7 +345,9 @@ def _ordered_dict(loader: LoaderType, node: yaml.nodes.MappingNode) -> OrderedDi fname = loader.get_stream_name() raise yaml.MarkedYAMLError( context=f'invalid key: "{key}"', - context_mark=yaml.Mark(fname, 0, line, -1, None, None), # type: ignore[arg-type] + context_mark=yaml.Mark( + fname, 0, line, -1, None, None # type: ignore[arg-type] + ), ) from exc if key in seen: diff --git a/mypy.ini b/mypy.ini index 2e0c5d2ae0d..27d9513d5df 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3,7 +3,7 @@ # To update, run python3 -m script.hassfest -p mypy_config [mypy] -python_version = 3.9 +python_version = 3.10 show_error_codes = true follow_imports = silent ignore_missing_imports = true @@ -333,6 +333,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.apcupsd.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.aqualogic.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -433,6 +443,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.bitcoin.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.blockchain.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -854,6 +874,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.filter.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.fitbit.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1474,6 +1504,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.lacrosse.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.lacrosse_view.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1514,6 +1554,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ld2410_ble.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.lidarr.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1734,6 +1784,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.mopeka.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.mqtt.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -1944,6 +2004,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.otbr.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.overkiz.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2204,6 +2274,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.rss_feed_template.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.rtsp_to_webrtc.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2214,6 +2294,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.ruuvi_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 +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.ruuvitag_ble.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2254,6 +2344,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.scrape.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.select.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -2314,6 +2414,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.sfr_box.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.shelly.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/pylint/plugins/hass_constructor.py b/pylint/plugins/hass_constructor.py index 23496b68de3..b2db7ba429b 100644 --- a/pylint/plugins/hass_constructor.py +++ b/pylint/plugins/hass_constructor.py @@ -22,7 +22,7 @@ class HassConstructorFormatChecker(BaseChecker): # type: ignore[misc] options = () def visit_functiondef(self, node: nodes.FunctionDef) -> None: - """Called when a FunctionDef node is visited.""" + """Check for improperly typed `__init__` definitions.""" if not node.is_method() or node.name != "__init__": return diff --git a/pylint/plugins/hass_enforce_type_hints.py b/pylint/plugins/hass_enforce_type_hints.py index 1e3ab900793..0ecf9f58398 100644 --- a/pylint/plugins/hass_enforce_type_hints.py +++ b/pylint/plugins/hass_enforce_type_hints.py @@ -20,7 +20,7 @@ if TYPE_CHECKING: class _Special(Enum): - """Sentinel values""" + """Sentinel values.""" UNDEFINED = 1 @@ -375,7 +375,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 0: "HomeAssistant", 1: "ConfigEntry", }, - return_type=_Special.UNDEFINED, + return_type="Mapping[str, Any]", ), TypeHintMatch( function_name="async_get_device_diagnostics", @@ -384,7 +384,7 @@ _FUNCTION_MATCH: dict[str, list[TypeHintMatch]] = { 1: "ConfigEntry", 2: "DeviceEntry", }, - return_type=_Special.UNDEFINED, + return_type="Mapping[str, Any]", ), ], "notify": [ @@ -1441,7 +1441,7 @@ _INHERITANCE_MATCH: dict[str, list[ClassTypeHintMatch]] = { ), TypeHintMatch( function_name="set_humidity", - arg_types={1: "str"}, + arg_types={1: "int"}, return_type=None, has_async_counterpart=True, ), @@ -2837,7 +2837,7 @@ def _has_valid_annotations( def _get_module_platform(module_name: str) -> str | None: - """Called when a Module node is visited.""" + """Return the platform for the module name.""" if not (module_match := _MODULE_REGEX.match(module_name)): # Ensure `homeassistant.components.` # Or `homeassistant.components..` @@ -2878,12 +2878,13 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] ) def __init__(self, linter: PyLinter | None = None) -> None: + """Initialize the HassTypeHintChecker.""" super().__init__(linter) self._function_matchers: list[TypeHintMatch] = [] self._class_matchers: list[ClassTypeHintMatch] = [] def visit_module(self, node: nodes.Module) -> None: - """Called when a Module node is visited.""" + """Populate matchers for a Module node.""" self._function_matchers = [] self._class_matchers = [] @@ -2907,7 +2908,7 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] self._class_matchers.reverse() def visit_classdef(self, node: nodes.ClassDef) -> None: - """Called when a ClassDef node is visited.""" + """Apply relevant type hint checks on a ClassDef node.""" ancestor: nodes.ClassDef checked_class_methods: set[str] = set() ancestors = list(node.ancestors()) # cache result for inside loop @@ -2934,7 +2935,7 @@ class HassTypeHintChecker(BaseChecker): # type: ignore[misc] checked_class_methods.add(function_node.name) def visit_functiondef(self, node: nodes.FunctionDef) -> None: - """Called when a FunctionDef node is visited.""" + """Apply relevant type hint checks on a FunctionDef node.""" for match in self._function_matchers: if not match.need_to_check_function(node) or node.is_method(): continue diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index 0a9291ec5ec..6bde2193b59 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -396,11 +396,12 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] options = () def __init__(self, linter: PyLinter | None = None) -> None: + """Initialize the HassImportsFormatChecker.""" super().__init__(linter) self.current_package: str | None = None def visit_module(self, node: nodes.Module) -> None: - """Called when a Module node is visited.""" + """Determine current package.""" if node.package: self.current_package = node.name else: @@ -408,7 +409,7 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] self.current_package = node.name[: node.name.rfind(".")] def visit_import(self, node: nodes.Import) -> None: - """Called when a Import node is visited.""" + """Check for improper `import _` invocations.""" if self.current_package is None: return for module, _alias in node.names: @@ -430,7 +431,7 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] def _visit_importfrom_relative( self, current_package: str, node: nodes.ImportFrom ) -> None: - """Called when a ImportFrom node is visited.""" + """Check for improper 'from ._ import _' invocations.""" if ( node.level <= 1 or not current_package.startswith("homeassistant.components.") @@ -449,7 +450,7 @@ class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] self.add_message("hass-absolute-import", node=node) def visit_importfrom(self, node: nodes.ImportFrom) -> None: - """Called when a ImportFrom node is visited.""" + """Check for improper 'from _ import _' invocations.""" if not self.current_package: return if node.level is not None: diff --git a/pylint/plugins/hass_logger.py b/pylint/plugins/hass_logger.py index 0135720a792..bfa05001304 100644 --- a/pylint/plugins/hass_logger.py +++ b/pylint/plugins/hass_logger.py @@ -29,13 +29,13 @@ class HassLoggerFormatChecker(BaseChecker): # type: ignore[misc] options = () def visit_call(self, node: nodes.Call) -> None: - """Called when a Call node is visited.""" + """Check for improper log messages.""" if not isinstance(node.func, nodes.Attribute) or not isinstance( node.func.expr, nodes.Name ): return - if not node.func.expr.name in LOGGER_NAMES: + if node.func.expr.name not in LOGGER_NAMES: return if not node.args: diff --git a/pyproject.toml b/pyproject.toml index 7f970771255..5c014aabd4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.1.7" +version = "2023.2.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" @@ -18,32 +18,33 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Topic :: Home Automation", ] -requires-python = ">=3.9.0" +requires-python = ">=3.10.0" dependencies = [ "aiohttp==3.8.1", "astral==2.2", "async_timeout==4.0.2", - "attrs==22.1.0", + "attrs==22.2.0", "atomicwrites-homeassistant==1.4.1", "awesomeversion==22.9.0", - "bcrypt==3.1.7", + "bcrypt==4.0.1", "certifi>=2021.5.30", "ciso8601==2.3.0", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all - "httpx==0.23.2", + "httpx==0.23.3", "home-assistant-bluetooth==1.9.2", "ifaddr==0.1.7", "jinja2==3.1.2", "lru-dict==1.1.8", "PyJWT==2.5.0", # PyJWT has loose dependency. We want the latest one. - "cryptography==38.0.3", - "orjson==3.8.1", + "cryptography==39.0.0", + # pyOpenSSL 23.0.0 is required to work with cryptography 39+ + "pyOpenSSL==23.0.0", + "orjson==3.8.5", "pip>=21.0,<22.4", "python-slugify==4.0.1", "pyyaml==6.0", @@ -91,7 +92,7 @@ forced_separate = [ combine_as_imports = true [tool.pylint.MAIN] -py-version = "3.9" +py-version = "3.10" ignore = [ "tests", ] @@ -115,6 +116,7 @@ load-plugins = [ "hass_enforce_type_hints", "hass_imports", "hass_logger", + "pylint_per_file_ignores", ] persistent = false extension-pkg-allow-list = [ @@ -162,6 +164,9 @@ good-names = [ # Enable once current issues are fixed: # consider-using-namedtuple-or-dataclass (Pylint CodeStyle extension) # consider-using-assignment-expr (Pylint CodeStyle extension) +# --- +# Temporary for the Python 3.10 update +# consider-alternative-union-syntax disable = [ "format", "abstract-method", @@ -186,6 +191,7 @@ disable = [ "consider-using-f-string", "consider-using-namedtuple-or-dataclass", "consider-using-assignment-expr", + "consider-alternative-union-syntax", ] enable = [ #"useless-suppression", # temporarily every now and then to clean them up @@ -217,6 +223,9 @@ runtime-typing = false [tool.pylint.CODE_STYLE] max-line-length-suggestions = 72 +[tool.pylint-per-file-ignores] +"/tests/"="protected-access,redefined-outer-name" + [tool.pytest.ini_options] testpaths = [ "tests", @@ -228,3 +237,52 @@ norecursedirs = [ log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s" log_date_format = "%Y-%m-%d %H:%M:%S" asyncio_mode = "auto" + +[tool.ruff] +target-version = "py310" +exclude = [] + +ignore = [ + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D212", # Multi-line docstring summary should start at the first line + "D213", # Multi-line docstring summary should start at the second line + "D404", # First word of the docstring should not be This + "D406", # Section name should end with a newline + "D407", # Section name underlining + "D411", # Missing blank line before section + "D418", # Function decorated with `@overload` shouldn't contain a docstring + "E501", # line too long + "E713", # Test for membership should be 'not in' + "E731", # do not assign a lambda expression, use a def + "UP024", # Replace aliased errors with `OSError` +] +select = [ + "C", # complexity + "D", # docstrings + "E", # pycodestyle + "F", # pyflakes/autoflake + "T20", # flake8-print + "W", # pycodestyle + "UP", # pyupgrade + "PGH004", # Use specific rule codes when using noqa +] + +[tool.ruff.per-file-ignores] + +# TODO: these files have functions that are too complex, but flake8's and ruff's +# complexity (and/or nested-function) handling differs; trying to add a noqa doesn't work +# because the flake8-noqa plugin then disagrees on whether there should be a C901 noqa +# on that line. So, for now, we just ignore C901s on these files as far as ruff is concerned. + +"homeassistant/components/light/__init__.py" = ["C901"] +"homeassistant/components/mqtt/discovery.py" = ["C901"] +"homeassistant/components/websocket_api/http.py" = ["C901"] + +# Allow for main entry & scripts to write to stdout +"homeassistant/__main__.py" = ["T201"] +"homeassistant/scripts/*" = ["T201"] +"script/*" = ["T20"] + +[tool.ruff.mccabe] +max-complexity = 25 diff --git a/requirements.txt b/requirements.txt index 8438cedd87d..c423388bbbd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,20 +4,21 @@ aiohttp==3.8.1 astral==2.2 async_timeout==4.0.2 -attrs==22.1.0 +attrs==22.2.0 atomicwrites-homeassistant==1.4.1 awesomeversion==22.9.0 -bcrypt==3.1.7 +bcrypt==4.0.1 certifi>=2021.5.30 ciso8601==2.3.0 -httpx==0.23.2 +httpx==0.23.3 home-assistant-bluetooth==1.9.2 ifaddr==0.1.7 jinja2==3.1.2 lru-dict==1.1.8 PyJWT==2.5.0 -cryptography==38.0.3 -orjson==3.8.1 +cryptography==39.0.0 +pyOpenSSL==23.0.0 +orjson==3.8.5 pip>=21.0,<22.4 python-slugify==4.0.1 pyyaml==6.0 diff --git a/requirements_all.txt b/requirements_all.txt index c906fbbd94e..6ccb4d457e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -5,7 +5,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.48 +AIOAladdinConnect==0.1.55 # homeassistant.components.adax Adax-local==0.1.5 @@ -70,11 +70,8 @@ WSDiscovery==2.0.0 # homeassistant.components.waze_travel_time WazeRouteCalculator==0.14 -# homeassistant.components.abode -abodepy==1.2.0 - # homeassistant.components.accuweather -accuweather==0.4.0 +accuweather==0.5.0 # homeassistant.components.adax adax==0.2.0 @@ -101,16 +98,16 @@ agent-py==0.0.23 aio_geojson_generic_client==0.1 # homeassistant.components.geonetnz_quakes -aio_geojson_geonetnz_quakes==0.13 +aio_geojson_geonetnz_quakes==0.15 # homeassistant.components.geonetnz_volcano -aio_geojson_geonetnz_volcano==0.6 +aio_geojson_geonetnz_volcano==0.8 # homeassistant.components.nsw_rural_fire_service_feed -aio_geojson_nsw_rfs_incidents==0.4 +aio_geojson_nsw_rfs_incidents==0.6 # homeassistant.components.usgs_earthquakes_feed -aio_geojson_usgs_earthquakes==0.1 +aio_geojson_usgs_earthquakes==0.2 # homeassistant.components.gdacs aio_georss_gdacs==0.7 @@ -153,13 +150,13 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2022.11.0 +aioecowitt==2023.01.0 # homeassistant.components.emonitor aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.0.2 +aioesphomeapi==13.1.0 # homeassistant.components.flo aioflo==2021.11.0 @@ -184,7 +181,7 @@ aiohomekit==2.4.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.5.0 +aiohue==4.6.1 # homeassistant.components.imap aioimaplib==1.0.1 @@ -217,7 +214,7 @@ aiolyric==1.0.9 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.14.4 +aiomusiccast==0.14.7 # homeassistant.components.nanoleaf aionanoleaf==0.2.1 @@ -244,7 +241,7 @@ aiopurpleair==2022.12.1 aiopvapi==2.0.4 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==3.0.0 +aiopvpc==4.0.1 # homeassistant.components.lidarr # homeassistant.components.radarr @@ -258,7 +255,10 @@ aioqsw==0.3.1 aiorecollect==1.0.8 # homeassistant.components.ridwell -aioridwell==2022.11.0 +aioridwell==2023.01.0 + +# homeassistant.components.ruuvi_gateway +aioruuvigateway==0.0.2 # homeassistant.components.senseme aiosenseme==0.6.1 @@ -267,7 +267,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==5.2.1 +aioshelly==5.3.1 # homeassistant.components.skybell aioskybell==22.7.0 @@ -275,6 +275,9 @@ aioskybell==22.7.0 # homeassistant.components.slimproto aioslimproto==2.1.1 +# homeassistant.components.honeywell +aiosomecomfort==0.0.3 + # homeassistant.components.steamist aiosteamist==0.3.2 @@ -288,7 +291,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==43 +aiounifi==44 # homeassistant.components.vlc_telnet aiovlc==0.1.0 @@ -368,7 +371,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.33.0 +async-upnp-client==0.33.1 # homeassistant.components.supla asyncpysupla==0.0.5 @@ -392,7 +395,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==44 +axis==46 # homeassistant.components.azure_event_hub azure-eventhub==5.7.0 @@ -419,7 +422,7 @@ beautifulsoup4==4.11.1 # beewi_smartclim==0.0.10 # homeassistant.components.zha -bellows==0.34.6 +bellows==0.34.7 # homeassistant.components.bmw_connected_drive bimmer_connected==0.12.0 @@ -431,10 +434,10 @@ bizkaibus==0.1.1 bleak-retry-connector==2.13.0 # homeassistant.components.bluetooth -bleak==0.19.2 +bleak==0.19.5 # homeassistant.components.blebox -blebox_uniapi==2.1.3 +blebox_uniapi==2.1.4 # homeassistant.components.blink blinkpy==0.19.2 @@ -446,7 +449,7 @@ blinkstick==1.2.0 blockchain==1.4.4 # homeassistant.components.bluemaestro -bluemaestro-ble==0.2.0 +bluemaestro-ble==0.2.1 # homeassistant.components.decora # homeassistant.components.zengge @@ -459,6 +462,7 @@ bluetooth-adapters==0.15.2 bluetooth-auto-recovery==1.0.3 # homeassistant.components.bluetooth +# homeassistant.components.ld2410_ble # homeassistant.components.led_ble bluetooth-data-tools==0.3.1 @@ -488,7 +492,7 @@ brunt==1.2.0 bt_proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==2.4.1 +bthome-ble==2.5.1 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 @@ -500,7 +504,7 @@ btsmarthub_devicelist==0.2.3 buienradar==1.0.5 # homeassistant.components.caldav -caldav==0.9.1 +caldav==1.0.1 # homeassistant.components.circuit circuit-webhook==1.0.1 @@ -559,10 +563,10 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.82.0 +dbus-fast==1.84.0 # homeassistant.components.debugpy -debugpy==1.6.4 +debugpy==1.6.6 # homeassistant.components.decora # decora==0.6 @@ -588,7 +592,7 @@ denonavr==0.10.12 devolo-home-control-api==0.18.2 # homeassistant.components.devolo_home_network -devolo-plc-api==0.9.0 +devolo-plc-api==1.1.0 # homeassistant.components.directv directv==0.4.0 @@ -644,6 +648,9 @@ emulated_roku==0.2.1 # homeassistant.components.huisbaasje energyflip-client==0.2.2 +# homeassistant.components.energyzero +energyzero==0.3.1 + # homeassistant.components.enocean enocean==0.50 @@ -651,7 +658,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env_canada==0.5.22 +env_canada==0.5.27 # homeassistant.components.enphase_envoy envoy_reader==0.20.1 @@ -665,9 +672,15 @@ epson-projector==0.5.0 # homeassistant.components.epsonworkforce epsonprinter==0.0.9 +# homeassistant.components.esphome +esphome-dashboard-api==1.2.3 + # homeassistant.components.netgear_lte eternalegypt==0.0.12 +# homeassistant.components.eufylife_ble +eufylife_ble_client==0.1.7 + # homeassistant.components.keyboard_remote # evdev==1.4.0 @@ -712,7 +725,7 @@ fjaraskupan==2.2.0 flipr-api==1.4.4 # homeassistant.components.flux_led -flux_led==0.28.34 +flux_led==0.28.35 # homeassistant.components.homekit # homeassistant.components.recorder @@ -741,13 +754,13 @@ fritzconnection==1.10.3 gTTS==2.2.4 # homeassistant.components.google_assistant_sdk -gassist-text==0.0.7 +gassist-text==0.0.10 # homeassistant.components.google gcal-sync==4.1.2 # homeassistant.components.geniushub -geniushub-client==0.6.30 +geniushub-client==0.7.0 # homeassistant.components.geocaching geocachingapi==0.2.1 @@ -759,7 +772,7 @@ geopy==2.3.0 georss_generic_client==0.6 # homeassistant.components.ign_sismologia -georss_ign_sismologia_client==0.3 +georss_ign_sismologia_client==0.6 # homeassistant.components.qld_bushfire georss_qld_bushfire_alert_client==0.5 @@ -787,6 +800,9 @@ goalzero==0.2.1 # homeassistant.components.goodwe goodwe==0.2.18 +# homeassistant.components.google_mail +google-api-python-client==2.71.0 + # homeassistant.components.google_pubsub google-cloud-pubsub==2.13.11 @@ -794,7 +810,7 @@ google-cloud-pubsub==2.13.11 google-cloud-texttospeech==2.12.3 # homeassistant.components.nest -google-nest-sdm==2.2.2 +google-nest-sdm==2.2.4 # homeassistant.components.google_travel_time googlemaps==2.5.1 @@ -846,7 +862,7 @@ ha-av==10.0.0 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==2.9.0 +ha-philipsjs==3.0.0 # homeassistant.components.habitica habitipy==0.2.0 @@ -857,6 +873,9 @@ hass-nabucasa==0.61.0 # homeassistant.components.splunk hass_splunk==0.1.1 +# homeassistant.components.conversation +hassil==0.2.6 + # homeassistant.components.tasmota hatasmota==0.6.3 @@ -885,10 +904,13 @@ hlk-sw16==0.0.9 hole==0.8.0 # homeassistant.components.workday -holidays==0.17.2 +holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230110.0 +home-assistant-frontend==20230201.0 + +# homeassistant.components.conversation +home-assistant-intents==2023.1.31 # homeassistant.components.home_connect homeconnect==0.7.2 @@ -974,6 +996,9 @@ ismartgate==4.0.4 # homeassistant.components.file_upload janus==1.0.0 +# homeassistant.components.abode +jaraco.abode==3.2.1 + # homeassistant.components.jellyfin jellyfin-apiclient-python==1.9.2 @@ -998,9 +1023,6 @@ kiwiki-client==0.1.1 # homeassistant.components.konnected konnected==1.2.0 -# homeassistant.components.kostal_plenticore -kostal_plenticore==0.2.0 - # homeassistant.components.kraken krakenex==2.1.0 @@ -1013,6 +1035,9 @@ lakeside==0.12 # homeassistant.components.laundrify laundrify_aio==1.1.2 +# homeassistant.components.ld2410_ble +ld2410-ble==0.1.1 + # homeassistant.components.led_ble led-ble==1.0.0 @@ -1056,7 +1081,7 @@ london-tube-status==0.5 luftdaten==0.7.4 # homeassistant.components.lupusec -lupupy==0.2.4 +lupupy==0.2.5 # homeassistant.components.lw12wifi lw12==0.9.2 @@ -1116,13 +1141,16 @@ minio==7.1.12 moat-ble==0.1.1 # homeassistant.components.moehlenhoff_alpha2 -moehlenhoff-alpha2==1.2.1 +moehlenhoff-alpha2==1.3.0 + +# homeassistant.components.mopeka +mopeka_iot_ble==0.4.0 # homeassistant.components.motion_blinds motionblinds==0.6.15 # homeassistant.components.motioneye -motioneye-client==0.3.12 +motioneye-client==0.3.14 # homeassistant.components.mullvad mullvad-api==1.0.0 @@ -1140,7 +1168,7 @@ mycroftapi==2.0 nad_receiver==0.3.0 # homeassistant.components.keenetic_ndms2 -ndms2_client==0.1.1 +ndms2_client==0.1.2 # homeassistant.components.ness_alarm nessclient==0.10.0 @@ -1243,11 +1271,14 @@ open-garage==0.2.0 # homeassistant.components.open_meteo open-meteo==0.2.1 +# homeassistant.components.openai_conversation +openai==0.26.2 + # homeassistant.components.opencv # opencv-python-headless==4.6.0.66 # homeassistant.components.openerz -openerz-api==0.1.0 +openerz-api==0.2.0 # homeassistant.components.openevse openevsewifi==1.1.2 @@ -1268,7 +1299,7 @@ openwrt-luci-rpc==1.1.11 openwrt-ubus-rpc==0.0.2 # homeassistant.components.oralb -oralb-ble==0.14.3 +oralb-ble==0.17.1 # homeassistant.components.oru oru==0.1.11 @@ -1327,7 +1358,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.3.0 +pillow==9.4.0 # homeassistant.components.dominos pizzapi==0.0.3 @@ -1342,7 +1373,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.25.14 +plugwise==0.27.5 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1411,7 +1442,7 @@ py-schluter==0.1.7 py-sucks==0.9.8 # homeassistant.components.synology_dsm -py-synologydsm-api==1.0.8 +py-synologydsm-api==2.0.2 # homeassistant.components.zabbix py-zabbix==1.1.7 @@ -1439,7 +1470,7 @@ pyRFXtrx==0.30.0 pySwitchmate==0.5.1 # homeassistant.components.tibber -pyTibber==0.26.11 +pyTibber==0.26.12 # homeassistant.components.dlink pyW215==0.7.0 @@ -1466,9 +1497,6 @@ pyairnow==1.1.0 # homeassistant.components.airvisual_pro pyairvisual==2022.12.1 -# homeassistant.components.almond -pyalmond==0.0.2 - # homeassistant.components.atag pyatag==0.3.5.3 @@ -1500,7 +1528,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.5 +pybravia==0.3.1 # homeassistant.components.nissan_leaf pycarwings2==2.14 @@ -1527,7 +1555,7 @@ pycocotools==2.0.1 pycomfoconnect==0.5.1 # homeassistant.components.coolmaster -pycoolmasternet-async==0.1.2 +pycoolmasternet-async==0.1.5 # homeassistant.components.microsoft pycsspeechtts==1.0.8 @@ -1674,7 +1702,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.10 +pyisy==3.1.11 # homeassistant.components.itach pyitachip2ir==0.0.7 @@ -1691,6 +1719,9 @@ pykmtronic==0.3.0 # homeassistant.components.kodi pykodi==0.2.7 +# homeassistant.components.kostal_plenticore +pykoplenti==1.0.0 + # homeassistant.components.kraken pykrakenapi==0.1.8 @@ -1716,13 +1747,13 @@ pylgnetcast==0.3.7 pylibrespot-java==0.1.1 # homeassistant.components.litejet -pylitejet==0.3.0 +pylitejet==0.5.0 # homeassistant.components.litterrobot pylitterbot==2023.1.1 # homeassistant.components.lutron_caseta -pylutron-caseta==0.17.1 +pylutron-caseta==0.18.0 # homeassistant.components.lutron pylutron==0.2.8 @@ -1752,7 +1783,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==2.5.3 +pymodbus==3.1.1 # homeassistant.components.monoprice pymonoprice==0.4 @@ -1865,7 +1896,7 @@ pyqwikswitch==0.93 pyrail==0.0.3 # homeassistant.components.rainbird -pyrainbird==0.7.1 +pyrainbird==1.1.0 # homeassistant.components.recswitch pyrecswitch==1.0.2 @@ -1882,6 +1913,9 @@ pyrituals==0.0.6 # homeassistant.components.ruckus_unleashed pyruckus==0.16 +# homeassistant.components.rympro +pyrympro==0.0.4 + # homeassistant.components.sabnzbd pysabnzbd==1.1.1 @@ -1892,7 +1926,7 @@ pysaj==0.0.16 pysdcp==1 # homeassistant.components.sensibo -pysensibo==1.0.22 +pysensibo==1.0.25 # homeassistant.components.serial # homeassistant.components.zha @@ -1945,7 +1979,7 @@ pysnmplib==5.0.20 pysnooz==0.8.3 # homeassistant.components.soma -pysoma==0.0.10 +pysoma==0.0.12 # homeassistant.components.spc pyspcwebgw==0.4.0 @@ -1960,7 +1994,7 @@ pystiebeleltron==0.0.1.dev2 pysuez==0.1.19 # homeassistant.components.switchbee -pyswitchbee==1.7.3 +pyswitchbee==1.7.19 # homeassistant.components.syncthru pysyncthru==0.7.10 @@ -1981,7 +2015,7 @@ pythinkingcleaner==0.0.3 python-blockchain-api==0.0.2 # homeassistant.components.bsblan -python-bsblan==0.5.8 +python-bsblan==0.5.9 # homeassistant.components.clementine python-clementine-remote==1.0.1 @@ -2017,7 +2051,7 @@ python-gc100==1.0.3a0 python-gitlab==1.6.0 # homeassistant.components.homewizard -python-homewizard-energy==1.3.1 +python-homewizard-energy==1.8.0 # homeassistant.components.hp_ilo python-hpilo==4.3 @@ -2038,7 +2072,7 @@ python-kasa==0.5.0 # python-lirc==1.2.3 # homeassistant.components.matter -python-matter-server==1.0.8 +python-matter-server==2.0.2 # homeassistant.components.xiaomi_miio python-miio==0.5.12 @@ -2052,6 +2086,9 @@ python-mystrom==1.1.2 # homeassistant.components.nest python-nest==4.2.0 +# homeassistant.components.otbr +python-otbr-api==1.0.2 + # homeassistant.components.picnic python-picnic-api==1.1.0 @@ -2098,18 +2135,18 @@ pytouchline==0.7 pytraccar==1.0.0 # homeassistant.components.tradfri -pytradfri[async]==9.0.0 +pytradfri[async]==9.0.1 # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation -pytrafikverket==0.2.2 +pytrafikverket==0.2.3 # homeassistant.components.usb pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.6.1 +pyunifiprotect==4.6.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 @@ -2190,10 +2227,10 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.2.1 +reolink-aio==0.3.0 # homeassistant.components.python_script -restrictedpython==5.2 +restrictedpython==6.0 # homeassistant.components.idteck_prox rfk101py==0.0.1 @@ -2220,7 +2257,7 @@ rokuecp==0.17.0 roombapy==1.6.5 # homeassistant.components.roon -roonapi==0.1.2 +roonapi==0.1.3 # homeassistant.components.rova rova==0.3.0 @@ -2253,10 +2290,10 @@ samsungtvws[async,encrypted]==2.5.0 satel_integra==0.3.7 # homeassistant.components.dhcp -scapy==2.4.5 +scapy==2.5.0 # homeassistant.components.screenlogic -screenlogicpy==0.5.4 +screenlogicpy==0.6.4 # homeassistant.components.scsgate scsgate==0.1.0 @@ -2281,7 +2318,10 @@ sensorpro-ble==0.5.1 sensorpush-ble==1.5.2 # homeassistant.components.sentry -sentry-sdk==1.12.1 +sentry-sdk==1.13.0 + +# homeassistant.components.sfr_box +sfrbox-api==0.0.5 # homeassistant.components.sharkiq sharkiq==0.0.1 @@ -2320,7 +2360,7 @@ smhi-pkg==1.0.16 snapcast==2.3.0 # homeassistant.components.sonos -soco==0.28.1 +soco==0.29.0 # homeassistant.components.solaredge_local solaredge-local==0.2.0 @@ -2331,9 +2371,6 @@ solaredge==0.0.2 # homeassistant.components.solax solax==0.3.0 -# homeassistant.components.honeywell -somecomfort==0.8.0 - # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 @@ -2347,11 +2384,11 @@ speedtest-cli==2.1.3 spiderpy==1.6.1 # homeassistant.components.spotify -spotipy==2.22.0 +spotipy==2.22.1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.44 +sqlalchemy==1.4.45 # homeassistant.components.srp_energy srpenergy==1.3.6 @@ -2362,6 +2399,9 @@ starline==0.1.5 # homeassistant.components.starlingbank starlingbank==3.2 +# homeassistant.components.starlink +starlink-grpc-core==1.1.1 + # homeassistant.components.statsd statsd==3.2.1 @@ -2371,6 +2411,9 @@ steamodd==4.21 # homeassistant.components.stookalert stookalert==0.1.4 +# homeassistant.components.stookwijzer +stookwijzer==1.3.0 + # homeassistant.components.streamlabswater streamlabswater==1.0.1 @@ -2426,7 +2469,7 @@ temperusb==1.6.0 # tensorflow==2.5.0 # homeassistant.components.powerwall -tesla-powerwall==0.3.18 +tesla-powerwall==0.3.19 # homeassistant.components.tesla_wall_connector tesla-wall-connector==1.0.2 @@ -2456,16 +2499,16 @@ tilt-ble==0.2.3 tmb==0.0.4 # homeassistant.components.todoist -todoist-python==8.0.0 +todoist-api-python==2.0.2 # homeassistant.components.tolo -tololib==0.1.0b3 +tololib==0.1.0b4 # homeassistant.components.toon toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2022.10 +total_connect_client==2023.1 # homeassistant.components.tplink_lte tp-connected==0.0.4 @@ -2521,7 +2564,7 @@ vallox-websocket-api==3.0.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.10.4 +velbus-aio==2022.12.0 # homeassistant.components.venstar venstarcolortouch==0.19 @@ -2530,7 +2573,7 @@ venstarcolortouch==0.19 vilfo-api-client==0.3.2 # homeassistant.components.volkszaehler -volkszaehler==0.3.2 +volkszaehler==0.4.0 # homeassistant.components.volvooncall volvooncall==0.10.1 @@ -2558,7 +2601,7 @@ wallbox==0.4.12 waqiasync==1.0.0 # homeassistant.components.folder_watcher -watchdog==2.2.0 +watchdog==2.2.1 # homeassistant.components.waterfurnace waterfurnace==1.1.0 @@ -2567,10 +2610,10 @@ waterfurnace==1.1.0 webexteamssdk==1.1.1 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.17.0 +whirlpool-sixth-sense==0.18.2 # homeassistant.components.whois -whois==0.9.16 +whois==0.9.23 # homeassistant.components.wiffi wiffi==1.1.0 @@ -2594,10 +2637,10 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.12.2 +xiaomi-ble==0.15.0 # homeassistant.components.knx -xknx==2.2.0 +xknx==2.3.0 # homeassistant.components.bluesound # homeassistant.components.fritz @@ -2614,13 +2657,13 @@ xs1-api-client==3.0.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.12.5 +yalexs-ble==1.12.8 # homeassistant.components.august yalexs==1.2.6 # homeassistant.components.august -yalexs_ble==1.12.5 +yalexs_ble==1.12.8 # homeassistant.components.yeelight yeelight==0.7.10 @@ -2646,8 +2689,11 @@ zengge==0.2 # homeassistant.components.zeroconf zeroconf==0.47.1 +# homeassistant.components.zeversolar +zeversolar==0.2.0 + # homeassistant.components.zha -zha-quirks==0.0.90 +zha-quirks==0.0.92 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test.txt b/requirements_test.txt index 72720b04a4c..5cf07345d49 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -# linters such as flake8 and pylint should be pinned, as new releases +# linters such as pylint should be pinned, as new releases # make new things fail. Manually update these pins when pulling in a # new version @@ -7,14 +7,15 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt -astroid==2.12.13 +astroid==2.12.14 codecov==2.1.12 -coverage==7.0.0 +coverage==7.0.5 freezegun==1.2.2 mock-open==1.4.0 mypy==0.991 -pre-commit==2.20.0 -pylint==2.15.8 +pre-commit==3.0.0 +pylint==2.15.10 +pylint-per-file-ignores==1.1.0 pipdeptree==2.3.1 pytest-asyncio==0.20.2 pytest-aiohttp==1.0.4 @@ -26,10 +27,9 @@ pytest-sugar==0.9.5 pytest-timeout==2.1.0 pytest-unordered==0.5.2 pytest-xdist==2.5.0 -pytest==7.2.0 +pytest==7.2.1 requests_mock==1.10.0 respx==0.20.1 -stdlib-list==0.7.0 tomli==2.0.1;python_version<"3.11" tqdm==4.64.0 types-atomicwrites==1.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1547c3dffcb..b91f5d6d144 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ AEMET-OpenData==0.2.1 # homeassistant.components.aladdin_connect -AIOAladdinConnect==0.1.48 +AIOAladdinConnect==0.1.55 # homeassistant.components.adax Adax-local==0.1.5 @@ -60,11 +60,8 @@ WSDiscovery==2.0.0 # homeassistant.components.waze_travel_time WazeRouteCalculator==0.14 -# homeassistant.components.abode -abodepy==1.2.0 - # homeassistant.components.accuweather -accuweather==0.4.0 +accuweather==0.5.0 # homeassistant.components.adax adax==0.2.0 @@ -88,16 +85,16 @@ agent-py==0.0.23 aio_geojson_generic_client==0.1 # homeassistant.components.geonetnz_quakes -aio_geojson_geonetnz_quakes==0.13 +aio_geojson_geonetnz_quakes==0.15 # homeassistant.components.geonetnz_volcano -aio_geojson_geonetnz_volcano==0.6 +aio_geojson_geonetnz_volcano==0.8 # homeassistant.components.nsw_rural_fire_service_feed -aio_geojson_nsw_rfs_incidents==0.4 +aio_geojson_nsw_rfs_incidents==0.6 # homeassistant.components.usgs_earthquakes_feed -aio_geojson_usgs_earthquakes==0.1 +aio_geojson_usgs_earthquakes==0.2 # homeassistant.components.gdacs aio_georss_gdacs==0.7 @@ -140,13 +137,13 @@ aioeafm==0.1.2 aioeagle==1.1.0 # homeassistant.components.ecowitt -aioecowitt==2022.11.0 +aioecowitt==2023.01.0 # homeassistant.components.emonitor aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==13.0.2 +aioesphomeapi==13.1.0 # homeassistant.components.flo aioflo==2021.11.0 @@ -168,7 +165,10 @@ aiohomekit==2.4.4 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==4.5.0 +aiohue==4.6.1 + +# homeassistant.components.imap +aioimaplib==1.0.1 # homeassistant.components.apache_kafka aiokafka==0.7.2 @@ -195,7 +195,7 @@ aiolyric==1.0.9 aiomodernforms==0.1.8 # homeassistant.components.yamaha_musiccast -aiomusiccast==0.14.4 +aiomusiccast==0.14.7 # homeassistant.components.nanoleaf aionanoleaf==0.2.1 @@ -219,7 +219,7 @@ aiopurpleair==2022.12.1 aiopvapi==2.0.4 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==3.0.0 +aiopvpc==4.0.1 # homeassistant.components.lidarr # homeassistant.components.radarr @@ -233,7 +233,10 @@ aioqsw==0.3.1 aiorecollect==1.0.8 # homeassistant.components.ridwell -aioridwell==2022.11.0 +aioridwell==2023.01.0 + +# homeassistant.components.ruuvi_gateway +aioruuvigateway==0.0.2 # homeassistant.components.senseme aiosenseme==0.6.1 @@ -242,7 +245,7 @@ aiosenseme==0.6.1 aiosenz==1.0.0 # homeassistant.components.shelly -aioshelly==5.2.1 +aioshelly==5.3.1 # homeassistant.components.skybell aioskybell==22.7.0 @@ -250,6 +253,9 @@ aioskybell==22.7.0 # homeassistant.components.slimproto aioslimproto==2.1.1 +# homeassistant.components.honeywell +aiosomecomfort==0.0.3 + # homeassistant.components.steamist aiosteamist==0.3.2 @@ -263,7 +269,7 @@ aiosyncthing==0.5.1 aiotractive==0.5.5 # homeassistant.components.unifi -aiounifi==43 +aiounifi==44 # homeassistant.components.vlc_telnet aiovlc==0.1.0 @@ -322,7 +328,7 @@ arcam-fmj==1.0.1 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.33.0 +async-upnp-client==0.33.1 # homeassistant.components.sleepiq asyncsleepiq==1.2.3 @@ -334,7 +340,7 @@ auroranoaa==0.0.2 aurorapy==0.2.7 # homeassistant.components.axis -axis==44 +axis==46 # homeassistant.components.azure_event_hub azure-eventhub==5.7.0 @@ -346,7 +352,7 @@ base36==0.1.1 beautifulsoup4==4.11.1 # homeassistant.components.zha -bellows==0.34.6 +bellows==0.34.7 # homeassistant.components.bmw_connected_drive bimmer_connected==0.12.0 @@ -355,16 +361,16 @@ bimmer_connected==0.12.0 bleak-retry-connector==2.13.0 # homeassistant.components.bluetooth -bleak==0.19.2 +bleak==0.19.5 # homeassistant.components.blebox -blebox_uniapi==2.1.3 +blebox_uniapi==2.1.4 # homeassistant.components.blink blinkpy==0.19.2 # homeassistant.components.bluemaestro -bluemaestro-ble==0.2.0 +bluemaestro-ble==0.2.1 # homeassistant.components.bluetooth bluetooth-adapters==0.15.2 @@ -373,6 +379,7 @@ bluetooth-adapters==0.15.2 bluetooth-auto-recovery==1.0.3 # homeassistant.components.bluetooth +# homeassistant.components.ld2410_ble # homeassistant.components.led_ble bluetooth-data-tools==0.3.1 @@ -392,13 +399,13 @@ brother==2.1.1 brunt==1.2.0 # homeassistant.components.bthome -bthome-ble==2.4.1 +bthome-ble==2.5.1 # homeassistant.components.buienradar buienradar==1.0.5 # homeassistant.components.caldav -caldav==0.9.1 +caldav==1.0.1 # homeassistant.components.co2signal co2signal==0.4.2 @@ -439,10 +446,10 @@ datadog==0.15.0 datapoint==0.9.8 # homeassistant.components.bluetooth -dbus-fast==1.82.0 +dbus-fast==1.84.0 # homeassistant.components.debugpy -debugpy==1.6.4 +debugpy==1.6.6 # homeassistant.components.ihc # homeassistant.components.namecheapdns @@ -462,7 +469,7 @@ denonavr==0.10.12 devolo-home-control-api==0.18.2 # homeassistant.components.devolo_home_network -devolo-plc-api==0.9.0 +devolo-plc-api==1.1.0 # homeassistant.components.directv directv==0.4.0 @@ -497,11 +504,14 @@ emulated_roku==0.2.1 # homeassistant.components.huisbaasje energyflip-client==0.2.2 +# homeassistant.components.energyzero +energyzero==0.3.1 + # homeassistant.components.enocean enocean==0.50 # homeassistant.components.environment_canada -env_canada==0.5.22 +env_canada==0.5.27 # homeassistant.components.enphase_envoy envoy_reader==0.20.1 @@ -512,6 +522,12 @@ ephem==4.1.2 # homeassistant.components.epson epson-projector==0.5.0 +# homeassistant.components.esphome +esphome-dashboard-api==1.2.3 + +# homeassistant.components.eufylife_ble +eufylife_ble_client==0.1.7 + # homeassistant.components.faa_delays faadelays==0.0.7 @@ -534,7 +550,7 @@ fjaraskupan==2.2.0 flipr-api==1.4.4 # homeassistant.components.flux_led -flux_led==0.28.34 +flux_led==0.28.35 # homeassistant.components.homekit # homeassistant.components.recorder @@ -557,7 +573,7 @@ fritzconnection==1.10.3 gTTS==2.2.4 # homeassistant.components.google_assistant_sdk -gassist-text==0.0.7 +gassist-text==0.0.10 # homeassistant.components.google gcal-sync==4.1.2 @@ -572,7 +588,7 @@ geopy==2.3.0 georss_generic_client==0.6 # homeassistant.components.ign_sismologia -georss_ign_sismologia_client==0.3 +georss_ign_sismologia_client==0.6 # homeassistant.components.qld_bushfire georss_qld_bushfire_alert_client==0.5 @@ -597,11 +613,14 @@ goalzero==0.2.1 # homeassistant.components.goodwe goodwe==0.2.18 +# homeassistant.components.google_mail +google-api-python-client==2.71.0 + # homeassistant.components.google_pubsub google-cloud-pubsub==2.13.11 # homeassistant.components.nest -google-nest-sdm==2.2.2 +google-nest-sdm==2.2.4 # homeassistant.components.google_travel_time googlemaps==2.5.1 @@ -638,7 +657,7 @@ ha-av==10.0.0 ha-ffmpeg==3.0.2 # homeassistant.components.philips_js -ha-philipsjs==2.9.0 +ha-philipsjs==3.0.0 # homeassistant.components.habitica habitipy==0.2.0 @@ -646,6 +665,9 @@ habitipy==0.2.0 # homeassistant.components.cloud hass-nabucasa==0.61.0 +# homeassistant.components.conversation +hassil==0.2.6 + # homeassistant.components.tasmota hatasmota==0.6.3 @@ -665,10 +687,13 @@ hlk-sw16==0.0.9 hole==0.8.0 # homeassistant.components.workday -holidays==0.17.2 +holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230110.0 +home-assistant-frontend==20230201.0 + +# homeassistant.components.conversation +home-assistant-intents==2023.1.31 # homeassistant.components.home_connect homeconnect==0.7.2 @@ -727,6 +752,9 @@ ismartgate==4.0.4 # homeassistant.components.file_upload janus==1.0.0 +# homeassistant.components.abode +jaraco.abode==3.2.1 + # homeassistant.components.jellyfin jellyfin-apiclient-python==1.9.2 @@ -742,9 +770,6 @@ kegtron-ble==0.4.0 # homeassistant.components.konnected konnected==1.2.0 -# homeassistant.components.kostal_plenticore -kostal_plenticore==0.2.0 - # homeassistant.components.kraken krakenex==2.1.0 @@ -754,6 +779,9 @@ lacrosse-view==0.0.9 # homeassistant.components.laundrify laundrify_aio==1.1.2 +# homeassistant.components.ld2410_ble +ld2410-ble==0.1.1 + # homeassistant.components.led_ble led-ble==1.0.0 @@ -818,13 +846,16 @@ minio==7.1.12 moat-ble==0.1.1 # homeassistant.components.moehlenhoff_alpha2 -moehlenhoff-alpha2==1.2.1 +moehlenhoff-alpha2==1.3.0 + +# homeassistant.components.mopeka +mopeka_iot_ble==0.4.0 # homeassistant.components.motion_blinds motionblinds==0.6.15 # homeassistant.components.motioneye -motioneye-client==0.3.12 +motioneye-client==0.3.14 # homeassistant.components.mullvad mullvad-api==1.0.0 @@ -836,7 +867,7 @@ mutagen==1.46.0 mutesync==0.0.1 # homeassistant.components.keenetic_ndms2 -ndms2_client==0.1.1 +ndms2_client==0.1.2 # homeassistant.components.ness_alarm nessclient==0.10.0 @@ -909,11 +940,14 @@ open-garage==0.2.0 # homeassistant.components.open_meteo open-meteo==0.2.1 +# homeassistant.components.openai_conversation +openai==0.26.2 + # homeassistant.components.openerz -openerz-api==0.1.0 +openerz-api==0.2.0 # homeassistant.components.oralb -oralb-ble==0.14.3 +oralb-ble==0.17.1 # homeassistant.components.ovo_energy ovoenergy==1.2.0 @@ -957,7 +991,7 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==9.3.0 +pillow==9.4.0 # homeassistant.components.plex plexapi==4.13.2 @@ -969,7 +1003,7 @@ plexauth==0.0.6 plexwebsocket==0.0.13 # homeassistant.components.plugwise -plugwise==0.25.14 +plugwise==0.27.5 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1017,7 +1051,7 @@ py-melissa-climate==2.1.4 py-nightscout==1.2.2 # homeassistant.components.synology_dsm -py-synologydsm-api==1.0.8 +py-synologydsm-api==2.0.2 # homeassistant.components.seventeentrack py17track==2021.12.2 @@ -1039,7 +1073,10 @@ pyMetno==0.9.0 pyRFXtrx==0.30.0 # homeassistant.components.tibber -pyTibber==0.26.11 +pyTibber==0.26.12 + +# homeassistant.components.dlink +pyW215==0.7.0 # homeassistant.components.nextbus py_nextbusnext==0.1.5 @@ -1054,9 +1091,6 @@ pyairnow==1.1.0 # homeassistant.components.airvisual_pro pyairvisual==2022.12.1 -# homeassistant.components.almond -pyalmond==0.0.2 - # homeassistant.components.atag pyatag==0.3.5.3 @@ -1079,7 +1113,7 @@ pyblackbird==0.5 pybotvac==0.0.23 # homeassistant.components.braviatv -pybravia==0.2.5 +pybravia==0.3.1 # homeassistant.components.cloudflare pycfdns==2.0.1 @@ -1091,7 +1125,7 @@ pychromecast==13.0.4 pycomfoconnect==0.5.1 # homeassistant.components.coolmaster -pycoolmasternet-async==0.1.2 +pycoolmasternet-async==0.1.5 # homeassistant.components.daikin pydaikin==2.8.0 @@ -1187,7 +1221,7 @@ pyiqvia==2022.04.0 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==3.0.10 +pyisy==3.1.11 # homeassistant.components.kaleidescape pykaleidescape==1.0.1 @@ -1201,6 +1235,9 @@ pykmtronic==0.3.0 # homeassistant.components.kodi pykodi==0.2.7 +# homeassistant.components.kostal_plenticore +pykoplenti==1.0.0 + # homeassistant.components.kraken pykrakenapi==0.1.8 @@ -1217,13 +1254,13 @@ pylaunches==1.3.0 pylibrespot-java==0.1.1 # homeassistant.components.litejet -pylitejet==0.3.0 +pylitejet==0.5.0 # homeassistant.components.litterrobot pylitterbot==2023.1.1 # homeassistant.components.lutron_caseta -pylutron-caseta==0.17.1 +pylutron-caseta==0.18.0 # homeassistant.components.mailgun pymailgunner==1.4 @@ -1244,7 +1281,7 @@ pymeteoclimatic==0.0.6 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==2.5.3 +pymodbus==3.1.1 # homeassistant.components.monoprice pymonoprice==0.4 @@ -1330,7 +1367,7 @@ pyps4-2ndscreen==1.3.1 pyqwikswitch==0.93 # homeassistant.components.rainbird -pyrainbird==0.7.1 +pyrainbird==1.1.0 # homeassistant.components.risco pyrisco==0.5.7 @@ -1341,11 +1378,14 @@ pyrituals==0.0.6 # homeassistant.components.ruckus_unleashed pyruckus==0.16 +# homeassistant.components.rympro +pyrympro==0.0.4 + # homeassistant.components.sabnzbd pysabnzbd==1.1.1 # homeassistant.components.sensibo -pysensibo==1.0.22 +pysensibo==1.0.25 # homeassistant.components.serial # homeassistant.components.zha @@ -1383,7 +1423,7 @@ pysnmplib==5.0.20 pysnooz==0.8.3 # homeassistant.components.soma -pysoma==0.0.10 +pysoma==0.0.12 # homeassistant.components.spc pyspcwebgw==0.4.0 @@ -1392,7 +1432,7 @@ pyspcwebgw==0.4.0 pysqueezebox==0.6.1 # homeassistant.components.switchbee -pyswitchbee==1.7.3 +pyswitchbee==1.7.19 # homeassistant.components.syncthru pysyncthru==0.7.10 @@ -1404,7 +1444,7 @@ pytankerkoenig==0.0.6 pytautulli==21.11.0 # homeassistant.components.bsblan -python-bsblan==0.5.8 +python-bsblan==0.5.9 # homeassistant.components.ecobee python-ecobee-api==0.2.14 @@ -1416,7 +1456,7 @@ python-forecastio==1.4.0 python-fullykiosk==0.0.12 # homeassistant.components.homewizard -python-homewizard-energy==1.3.1 +python-homewizard-energy==1.8.0 # homeassistant.components.izone python-izone==1.2.9 @@ -1428,7 +1468,7 @@ python-juicenet==1.1.0 python-kasa==0.5.0 # homeassistant.components.matter -python-matter-server==1.0.8 +python-matter-server==2.0.2 # homeassistant.components.xiaomi_miio python-miio==0.5.12 @@ -1436,6 +1476,9 @@ python-miio==0.5.12 # homeassistant.components.nest python-nest==4.2.0 +# homeassistant.components.otbr +python-otbr-api==1.0.2 + # homeassistant.components.picnic python-picnic-api==1.1.0 @@ -1464,18 +1507,18 @@ pytomorrowio==0.3.5 pytraccar==1.0.0 # homeassistant.components.tradfri -pytradfri[async]==9.0.0 +pytradfri[async]==9.0.1 # homeassistant.components.trafikverket_ferry # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation -pytrafikverket==0.2.2 +pytrafikverket==0.2.3 # homeassistant.components.usb pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.6.1 +pyunifiprotect==4.6.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 @@ -1529,10 +1572,10 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.2.1 +reolink-aio==0.3.0 # homeassistant.components.python_script -restrictedpython==5.2 +restrictedpython==6.0 # homeassistant.components.rflink rflink==0.0.63 @@ -1547,7 +1590,7 @@ rokuecp==0.17.0 roombapy==1.6.5 # homeassistant.components.roon -roonapi==0.1.2 +roonapi==0.1.3 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 @@ -1568,10 +1611,10 @@ samsungctl[websocket]==0.7.1 samsungtvws[async,encrypted]==2.5.0 # homeassistant.components.dhcp -scapy==2.4.5 +scapy==2.5.0 # homeassistant.components.screenlogic -screenlogicpy==0.5.4 +screenlogicpy==0.6.4 # homeassistant.components.backup securetar==2022.2.0 @@ -1590,7 +1633,10 @@ sensorpro-ble==0.5.1 sensorpush-ble==1.5.2 # homeassistant.components.sentry -sentry-sdk==1.12.1 +sentry-sdk==1.13.0 + +# homeassistant.components.sfr_box +sfrbox-api==0.0.5 # homeassistant.components.sharkiq sharkiq==0.0.1 @@ -1614,7 +1660,7 @@ smart-meter-texas==0.4.7 smhi-pkg==1.0.16 # homeassistant.components.sonos -soco==0.28.1 +soco==0.29.0 # homeassistant.components.solaredge solaredge==0.0.2 @@ -1622,9 +1668,6 @@ solaredge==0.0.2 # homeassistant.components.solax solax==0.3.0 -# homeassistant.components.honeywell -somecomfort==0.8.0 - # homeassistant.components.somfy_mylink somfy-mylink-synergy==1.0.6 @@ -1638,11 +1681,11 @@ speedtest-cli==2.1.3 spiderpy==1.6.1 # homeassistant.components.spotify -spotipy==2.22.0 +spotipy==2.22.1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.4.44 +sqlalchemy==1.4.45 # homeassistant.components.srp_energy srpenergy==1.3.6 @@ -1650,6 +1693,9 @@ srpenergy==1.3.6 # homeassistant.components.starline starline==0.1.5 +# homeassistant.components.starlink +starlink-grpc-core==1.1.1 + # homeassistant.components.statsd statsd==3.2.1 @@ -1659,6 +1705,9 @@ steamodd==4.21 # homeassistant.components.stookalert stookalert==0.1.4 +# homeassistant.components.stookwijzer +stookwijzer==1.3.0 + # homeassistant.components.huawei_lte # homeassistant.components.solaredge # homeassistant.components.thermoworks_smoke @@ -1687,7 +1736,7 @@ tellduslive==0.10.11 temescal==0.5 # homeassistant.components.powerwall -tesla-powerwall==0.3.18 +tesla-powerwall==0.3.19 # homeassistant.components.tesla_wall_connector tesla-wall-connector==1.0.2 @@ -1702,16 +1751,16 @@ thermopro-ble==0.4.3 tilt-ble==0.2.3 # homeassistant.components.todoist -todoist-python==8.0.0 +todoist-api-python==2.0.2 # homeassistant.components.tolo -tololib==0.1.0b3 +tololib==0.1.0b4 # homeassistant.components.toon toonapi==0.2.1 # homeassistant.components.totalconnect -total_connect_client==2022.10 +total_connect_client==2023.1 # homeassistant.components.transmission transmission-rpc==3.4.0 @@ -1761,7 +1810,7 @@ vallox-websocket-api==3.0.0 vehicle==0.4.0 # homeassistant.components.velbus -velbus-aio==2022.10.4 +velbus-aio==2022.12.0 # homeassistant.components.venstar venstarcolortouch==0.19 @@ -1789,13 +1838,13 @@ wakeonlan==2.1.0 wallbox==0.4.12 # homeassistant.components.folder_watcher -watchdog==2.2.0 +watchdog==2.2.1 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.17.0 +whirlpool-sixth-sense==0.18.2 # homeassistant.components.whois -whois==0.9.16 +whois==0.9.23 # homeassistant.components.wiffi wiffi==1.1.0 @@ -1813,10 +1862,10 @@ wolf_smartset==0.1.11 xbox-webapi==2.0.11 # homeassistant.components.xiaomi_ble -xiaomi-ble==0.12.2 +xiaomi-ble==0.15.0 # homeassistant.components.knx -xknx==2.2.0 +xknx==2.3.0 # homeassistant.components.bluesound # homeassistant.components.fritz @@ -1830,13 +1879,13 @@ xmltodict==0.13.0 yalesmartalarmclient==0.3.9 # homeassistant.components.yalexs_ble -yalexs-ble==1.12.5 +yalexs-ble==1.12.8 # homeassistant.components.august yalexs==1.2.6 # homeassistant.components.august -yalexs_ble==1.12.5 +yalexs_ble==1.12.8 # homeassistant.components.yeelight yeelight==0.7.10 @@ -1853,8 +1902,11 @@ zamg==0.2.2 # homeassistant.components.zeroconf zeroconf==0.47.1 +# homeassistant.components.zeversolar +zeversolar==0.2.0 + # homeassistant.components.zha -zha-quirks==0.0.90 +zha-quirks==0.0.92 # homeassistant.components.zha zigpy-deconz==0.19.2 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index cb80f4544c2..a852f1b3161 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -8,10 +8,11 @@ flake8-comprehensions==3.10.1 flake8-docstrings==1.6.0 flake8-noqa==1.3.0 flake8==6.0.0 -isort==5.11.4 +isort==5.12.0 mccabe==0.7.0 pycodestyle==2.10.0 -pydocstyle==6.1.1 +pydocstyle==6.2.3 pyflakes==3.0.1 pyupgrade==3.3.1 +ruff==0.0.231 yamllint==1.28.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 513960c2030..4456a3312b5 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -73,6 +73,7 @@ httplib2>=0.19.0 # want to ensure we have wheels built. grpcio==1.51.1 grpcio-status==1.51.1 +grpcio-reflection==1.51.1 # 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, @@ -144,6 +145,14 @@ pandas==1.4.3 # uamqp 1.6.1, has 1 failing test during built on armv7/armhf uamqp==1.6.0 + +# Matplotlib 3.6.2 has issues building wheels on armhf/armv7 +# We need at least >=2.1.0 (tensorflow integration -> pycocotools) +matplotlib==3.6.1 + +# pyOpenSSL 23.0.0 or later required to avoid import errors when +# cryptography 39.0.0 is installed with botocore +pyOpenSSL>=23.0.0 """ IGNORE_PRE_COMMIT_HOOK_ID = ( diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index 71d2e3ce57c..7d958307307 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -10,6 +10,7 @@ DONT_IGNORE = ( "device_action.py", "device_condition.py", "device_trigger.py", + "diagnostics.py", "group.py", "intent.py", "logbook.py", @@ -17,24 +18,12 @@ DONT_IGNORE = ( "scene.py", ) -# They were violating when we introduced this check -# Need to be fixed in a future PR. -ALLOWED_IGNORE_VIOLATIONS = { - ("doorbird", "logbook.py"), - ("elkm1", "scene.py"), - ("fibaro", "scene.py"), - ("lcn", "scene.py"), - ("lutron", "scene.py"), - ("tuya", "scene.py"), - ("velux", "scene.py"), -} - def validate(integrations: dict[str, Integration], config: Config) -> None: """Validate coverage.""" coverage_path = config.root / ".coveragerc" - not_found = [] + not_found: list[str] = [] checking = False with coverage_path.open("rt") as fp: @@ -64,19 +53,24 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: not_found.append(line) continue - if ( - not line.startswith("homeassistant/components/") - or len(path.parts) != 4 - or path.parts[-1] != "*" - ): + if not line.startswith("homeassistant/components/") or len(path.parts) != 4: continue integration_path = path.parent integration = integrations[integration_path.name] + if ( + path.parts[-1] == "*" + and Path(f"tests/components/{integration.domain}/__init__.py").exists() + ): + integration.add_error( + "coverage", + "has tests and should not use wildcard in .coveragerc file", + ) + for check in DONT_IGNORE: - if (integration_path.name, check) in ALLOWED_IGNORE_VIOLATIONS: + if path.parts[-1] not in {"*", check}: continue if (integration_path / check).exists(): @@ -85,14 +79,7 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: f"{check} must not be ignored by the .coveragerc file", ) - if not not_found: - return - - errors = [] - if not_found: - errors.append( + raise RuntimeError( f".coveragerc references files that don't exist: {', '.join(not_found)}." ) - - raise RuntimeError(" ".join(errors)) diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index 9993d5c52a9..cadb007e12c 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -163,6 +163,12 @@ def calc_allowed_references(integration: Integration) -> set[str]: | set(manifest.get("dependencies", [])) | set(manifest.get("after_dependencies", [])) ) + # bluetooth_adapters is a wrapper to ensure + # that all the integrations that provide bluetooth + # adapters are setup before loading integrations + # that use them. + if "bluetooth_adapters" in allowed_references: + allowed_references.add("bluetooth") # Discovery requirements are ok if referenced in manifest for check_domain, to_check in DISCOVERY_INTEGRATIONS.items(): diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 5a41d71be21..6f41b02878c 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -1,6 +1,7 @@ """Manifest validation.""" from __future__ import annotations +from enum import IntEnum from pathlib import Path from typing import Any from urllib.parse import urlparse @@ -23,7 +24,17 @@ DOCUMENTATION_URL_HOST = "www.home-assistant.io" DOCUMENTATION_URL_PATH_PREFIX = "/integrations/" DOCUMENTATION_URL_EXCEPTIONS = {"https://www.home-assistant.io/hassio"} -SUPPORTED_QUALITY_SCALES = ["gold", "internal", "platinum", "silver"] + +class QualityScale(IntEnum): + """Supported manifest quality scales.""" + + INTERNAL = -1 + SILVER = 1 + GOLD = 2 + PLATINUM = 3 + + +SUPPORTED_QUALITY_SCALES = [enum.name.lower() for enum in QualityScale] SUPPORTED_IOT_CLASSES = [ "assumed_state", "calculated", @@ -309,25 +320,19 @@ def validate_manifest(integration: Integration, core_components_dir: Path) -> No "manifest", f"Invalid manifest: {humanize_error(integration.manifest, err)}" ) - if integration.manifest["domain"] != integration.path.name: + if (domain := integration.manifest["domain"]) != integration.path.name: integration.add_error("manifest", "Domain does not match dir name") - if ( - not integration.core - and (core_components_dir / integration.manifest["domain"]).exists() - ): + if not integration.core and (core_components_dir / domain).exists(): integration.add_warning( "manifest", "Domain collides with built-in core integration" ) - if ( - integration.manifest["domain"] in NO_IOT_CLASS - and "iot_class" in integration.manifest - ): + if domain in NO_IOT_CLASS and "iot_class" in integration.manifest: integration.add_error("manifest", "Domain should not have an IoT Class") if ( - integration.manifest["domain"] not in NO_IOT_CLASS + domain not in NO_IOT_CLASS and "iot_class" not in integration.manifest and integration.manifest.get("integration_type") != "virtual" ): @@ -343,6 +348,16 @@ def validate_manifest(integration: Integration, core_components_dir: Path) -> No "Virtual integration points to non-existing supported_by integration", ) + if ( + (quality_scale := integration.manifest.get("quality_scale")) + and QualityScale[quality_scale.upper()] > QualityScale.SILVER + and not integration.manifest.get("codeowners") + ): + integration.add_error( + "manifest", + f"{quality_scale} integration does not have a code owner", + ) + if not integration.core: validate_version(integration) diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 27dd0654dc6..8e4ac524b2f 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -11,10 +11,8 @@ import sys from typing import Any from awesomeversion import AwesomeVersion, AwesomeVersionStrategy -from stdlib_list import stdlib_list from tqdm import tqdm -from homeassistant.const import REQUIRED_NEXT_PYTHON_VER, REQUIRED_PYTHON_VER import homeassistant.util.package as pkg_util from script.gen_requirements_all import COMMENT_REQUIREMENTS, normalize_package_name @@ -28,17 +26,6 @@ PACKAGE_REGEX = re.compile( ) PIP_REGEX = re.compile(r"^(--.+\s)?([-_\.\w\d]+.*(?:==|>=|<=|~=|!=|<|>|===)?.*$)") PIP_VERSION_RANGE_SEPARATOR = re.compile(r"^(==|>=|<=|~=|!=|<|>|===)?(.*)$") -SUPPORTED_PYTHON_TUPLES = [ - REQUIRED_PYTHON_VER[:2], -] -if REQUIRED_PYTHON_VER[0] == REQUIRED_NEXT_PYTHON_VER[0]: - for minor in range(REQUIRED_PYTHON_VER[1] + 1, REQUIRED_NEXT_PYTHON_VER[1] + 1): - if minor < 10: # stdlib list does not support 3.10+ - SUPPORTED_PYTHON_TUPLES.append((REQUIRED_PYTHON_VER[0], minor)) -SUPPORTED_PYTHON_VERSIONS = [ - ".".join(map(str, version_tuple)) for version_tuple in SUPPORTED_PYTHON_TUPLES -] -STD_LIBS = {version: set(stdlib_list(version)) for version in SUPPORTED_PYTHON_VERSIONS} IGNORE_VIOLATIONS = { # Still has standard library requirements. @@ -161,13 +148,12 @@ def validate_requirements(integration: Integration) -> None: return # Check for requirements incompatible with standard library. - for version, std_libs in STD_LIBS.items(): - for req in all_integration_requirements: - if req in std_libs: - integration.add_error( - "requirements", - f"Package {req} is not compatible with Python {version} standard library", - ) + for req in all_integration_requirements: + if req in sys.stlib_module_names: + integration.add_error( + "requirements", + f"Package {req} is not compatible with the Python standard library", + ) @cache diff --git a/script/hassfest/translations.py b/script/hassfest/translations.py index a9612cf1943..be73692cb26 100644 --- a/script/hassfest/translations.py +++ b/script/hassfest/translations.py @@ -21,6 +21,7 @@ REQUIRED = 1 REMOVED = 2 RE_REFERENCE = r"\[\%key:(.+)\%\]" +RE_TRANSLATION_KEY = re.compile(r"^(?!.+[_-]{2})(?![_-])[a-z0-9-_]+(? str: - """Validate value is lowercase.""" - if value.lower() != value: - raise vol.Invalid("Needs to be lowercase") +def translation_key_validator(value: str) -> str: + """Validate value is valid translation key.""" + if RE_TRANSLATION_KEY.match(value) is None: + raise vol.Invalid( + f"Invalid translation key '{value}', need to be [a-z0-9-_]+ and" + " cannot start or end with a hyphen or underscore." + ) return value @@ -214,6 +216,14 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: flow_title=UNDEFINED, require_step_title=False, ), + vol.Optional("selector"): cv.schema_with_slug_keys( + { + "options": cv.schema_with_slug_keys( + cv.string_with_no_html, slug_validator=translation_key_validator + ) + }, + slug_validator=vol.Any("_", cv.slug), + ), vol.Optional("device_automation"): { vol.Optional("action_type"): {str: cv.string_with_no_html}, vol.Optional("condition_type"): {str: cv.string_with_no_html}, @@ -221,7 +231,9 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: vol.Optional("trigger_subtype"): {str: cv.string_with_no_html}, }, vol.Optional("state"): cv.schema_with_slug_keys( - cv.schema_with_slug_keys(str, slug_validator=lowercase_validator), + cv.schema_with_slug_keys( + cv.string_with_no_html, slug_validator=translation_key_validator + ), slug_validator=vol.Any("_", cv.slug), ), vol.Optional("state_attributes"): cv.schema_with_slug_keys( @@ -229,19 +241,22 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: { vol.Optional("name"): str, vol.Optional("state"): cv.schema_with_slug_keys( - str, slug_validator=lowercase_validator + cv.string_with_no_html, + slug_validator=translation_key_validator, ), }, - slug_validator=lowercase_validator, + slug_validator=translation_key_validator, ), slug_validator=vol.Any("_", cv.slug), ), vol.Optional("system_health"): { - vol.Optional("info"): {str: cv.string_with_no_html} + vol.Optional("info"): cv.schema_with_slug_keys( + cv.string_with_no_html, slug_validator=translation_key_validator + ), }, vol.Optional("config_panel"): cv.schema_with_slug_keys( cv.schema_with_slug_keys( - cv.string_with_no_html, slug_validator=lowercase_validator + cv.string_with_no_html, slug_validator=translation_key_validator ), slug_validator=vol.Any("_", cv.slug), ), @@ -273,10 +288,16 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema: vol.Optional("state_attributes"): { str: { vol.Optional("name"): cv.string_with_no_html, - vol.Optional("state"): {str: cv.string_with_no_html}, + vol.Optional("state"): cv.schema_with_slug_keys( + cv.string_with_no_html, + slug_validator=translation_key_validator, + ), } }, - vol.Optional("state"): {str: cv.string_with_no_html}, + vol.Optional("state"): cv.schema_with_slug_keys( + cv.string_with_no_html, + slug_validator=translation_key_validator, + ), } } }, @@ -352,7 +373,7 @@ def gen_platform_strings_schema(config: Config, integration: Integration) -> vol return vol.Schema( { vol.Optional("state"): cv.schema_with_slug_keys( - cv.schema_with_slug_keys(str, slug_validator=lowercase_validator), + cv.schema_with_slug_keys(str, slug_validator=translation_key_validator), slug_validator=device_class_validator, ) } diff --git a/script/lint b/script/lint index 378c8c68d39..450733cecfd 100755 --- a/script/lint +++ b/script/lint @@ -16,6 +16,10 @@ echo "================" echo "LINT with flake8" echo "================" pre-commit run flake8 --files $files +echo "==============" +echo "LINT with ruff" +echo "==============" +pre-commit run ruff --files $files echo "================" echo "LINT with pylint" echo "================" diff --git a/script/lint_and_test.py b/script/lint_and_test.py index 97108e1c630..03765701530 100755 --- a/script/lint_and_test.py +++ b/script/lint_and_test.py @@ -6,6 +6,7 @@ This is NOT a full CI/linting replacement, only a quick check during development """ import asyncio from collections import namedtuple +import itertools import os import re import shlex @@ -39,7 +40,7 @@ def printc(the_color, *args): def validate_requirements_ok(): """Validate requirements, returns True of ok.""" - # pylint: disable=import-error,import-outside-toplevel + # pylint: disable-next=import-error,import-outside-toplevel from gen_requirements_all import main as req_main return req_main(True) == 0 @@ -115,9 +116,9 @@ async def pylint(files): return res -async def flake8(files): - """Exec flake8.""" - _, log = await async_exec("pre-commit", "run", "flake8", "--files", *files) +async def _ruff_or_flake8(tool, files): + """Exec ruff or flake8.""" + _, log = await async_exec("pre-commit", "run", tool, "--files", *files) res = [] for line in log.splitlines(): line = line.split(":") @@ -128,17 +129,33 @@ async def flake8(files): return res +async def flake8(files): + """Exec flake8.""" + return await _ruff_or_flake8("flake8", files) + + +async def ruff(files): + """Exec ruff.""" + return await _ruff_or_flake8("ruff", files) + + async def lint(files): """Perform lint.""" files = [file for file in files if os.path.isfile(file)] - fres, pres = await asyncio.gather(flake8(files), pylint(files)) - - res = fres + pres - res.sort(key=lambda item: item.file) + res = sorted( + itertools.chain( + *await asyncio.gather( + flake8(files), + pylint(files), + ruff(files), + ) + ), + key=lambda item: item.file, + ) if res: - print("Pylint & Flake8 errors:") + print("Lint errors:") else: - printc(PASS, "Pylint and Flake8 passed") + printc(PASS, "Lint passed") lint_ok = True for err in res: diff --git a/script/pip_check b/script/pip_check index cbe6a3851e0..cbbe7ffeeae 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 resolves one! -DEPENDENCY_CONFLICTS=4 +DEPENDENCY_CONFLICTS=3 PIP_CHECK=$(pip check --cache-dir=$PIP_CACHE) LINE_COUNT=$(echo "$PIP_CHECK" | wc -l) diff --git a/script/setup b/script/setup index 210779eec45..5a5bc84bb27 100755 --- a/script/setup +++ b/script/setup @@ -23,6 +23,11 @@ fi script/bootstrap +# Avoid unsafe git error when running inside devcontainer +if [ -n "$DEVCONTAINER" ];then + git config --global --add safe.directory "$PWD" +fi + pre-commit install python3 -m pip install -e . --constraint homeassistant/package_constraints.txt --use-deprecated=legacy-resolver diff --git a/setup.cfg b/setup.cfg index 709b9e4286a..1193bbd44d8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,3 +21,13 @@ ignore = D202, W504 noqa-require-code = True + +# Ignores, that are currently caused by mismatching configurations +# between ruff and flake8 configurations. Once ruff becomes permanent flake8 +# will be removed, including these ignores below. +# In case we decide not to continue with ruff, we should remove these +# and probably need to clean up a couple of noqa comments. +per-file-ignores = + homeassistant/config.py:NQA102 + tests/components/tts/conftest.py:NQA102 + tests/helpers/test_icon.py:NQA102 diff --git a/tests/common.py b/tests/common.py index 69c569e445f..52356357779 100644 --- a/tests/common.py +++ b/tests/common.py @@ -3,9 +3,9 @@ from __future__ import annotations import asyncio from collections import OrderedDict -from collections.abc import Awaitable, Callable, Collection +from collections.abc import Awaitable, Callable, Collection, Mapping, Sequence from contextlib import contextmanager -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import functools as ft from io import StringIO import json @@ -16,13 +16,13 @@ import threading import time from time import monotonic import types -from typing import Any +from typing import Any, NoReturn from unittest.mock import AsyncMock, Mock, patch from aiohttp.test_utils import unused_port as get_test_instance_port # noqa: F401 import voluptuous as vol -from homeassistant import auth, bootstrap, config_entries, core as ha, loader +from homeassistant import auth, bootstrap, config_entries, loader from homeassistant.auth import ( auth_store, models as auth_models, @@ -30,11 +30,10 @@ from homeassistant.auth import ( providers as auth_providers, ) from homeassistant.auth.permissions import system_policies -from homeassistant.components import device_automation, recorder +from homeassistant.components import device_automation from homeassistant.components.device_automation import ( # noqa: F401 _async_get_device_automation_capabilities as async_get_device_automation_capabilities, ) -from homeassistant.components.mqtt.models import ReceiveMessage from homeassistant.config import async_process_component_config from homeassistant.const import ( DEVICE_DEFAULT_NAME, @@ -43,7 +42,15 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import BLOCK_LOG_TIMEOUT, HomeAssistant, ServiceCall, State +from homeassistant.core import ( + BLOCK_LOG_TIMEOUT, + CoreState, + Event, + HomeAssistant, + ServiceCall, + State, + callback, +) from homeassistant.helpers import ( area_registry, device_registry, @@ -58,7 +65,7 @@ from homeassistant.helpers import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.json import JSONEncoder -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, StateType from homeassistant.setup import setup_component from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as date_util @@ -133,7 +140,7 @@ def get_test_home_assistant(): def run_loop(): """Run event loop.""" - # pylint: disable=protected-access + loop._thread_ident = threading.get_ident() loop.run_forever() loop_stop_event.set() @@ -159,10 +166,9 @@ def get_test_home_assistant(): return hass -# pylint: disable=protected-access async def async_test_home_assistant(event_loop, load_registries=True): """Return a Home Assistant object pointing at test config dir.""" - hass = ha.HomeAssistant() + hass = HomeAssistant() store = auth_store.AuthStore(hass) hass.auth = auth.AuthManager(hass, store, {}, {}) ensure_auth_manager_loaded(hass.auth) @@ -294,7 +300,10 @@ async def async_test_home_assistant(event_loop, load_registries=True): hass.config_entries = config_entries.ConfigEntries( hass, { - "_": "Not empty or else some bad checks for hass config in discovery.py breaks" + "_": ( + "Not empty or else some bad checks for hass config in discovery.py" + " breaks" + ) }, ) @@ -309,7 +318,7 @@ async def async_test_home_assistant(event_loop, load_registries=True): await hass.async_block_till_done() hass.data[bootstrap.DATA_REGISTRIES_LOADED] = None - hass.state = ha.CoreState.running + hass.state = CoreState.running # Mock async_start orig_start = hass.async_start @@ -322,7 +331,7 @@ async def async_test_home_assistant(event_loop, load_registries=True): hass.async_start = mock_async_start - @ha.callback + @callback def clear_instance(event): """Clear global instance.""" INSTANCES.remove(hass) @@ -338,7 +347,7 @@ def async_mock_service( """Set up a fake service & return a calls log list to this service.""" calls = [] - @ha.callback + @callback def mock_service_log(call): # pylint: disable=unnecessary-lambda """Mock service call.""" calls.append(call) @@ -351,7 +360,7 @@ def async_mock_service( mock_service = threadsafe_callback_factory(async_mock_service) -@ha.callback +@callback def async_mock_intent(hass, intent_typ): """Set up a fake intent handler.""" intents = [] @@ -369,9 +378,13 @@ def async_mock_intent(hass, intent_typ): return intents -@ha.callback +@callback def async_fire_mqtt_message(hass, topic, payload, qos=0, retain=False): """Fire the MQTT message.""" + # Local import to avoid processing MQTT modules when running a testcase + # which does not use MQTT. + from homeassistant.components.mqtt.models import ReceiveMessage + if isinstance(payload, str): payload = payload.encode("utf-8") msg = ReceiveMessage(topic, payload, qos, retain) @@ -381,7 +394,7 @@ def async_fire_mqtt_message(hass, topic, payload, qos=0, retain=False): fire_mqtt_message = threadsafe_callback_factory(async_fire_mqtt_message) -@ha.callback +@callback def async_fire_time_changed_exact( hass: HomeAssistant, datetime_: datetime | None = None, fire_all: bool = False ) -> None: @@ -393,14 +406,14 @@ def async_fire_time_changed_exact( approach, as this is only for testing. """ if datetime_ is None: - utc_datetime = date_util.utcnow() + utc_datetime = datetime.now(timezone.utc) else: utc_datetime = date_util.as_utc(datetime_) _async_fire_time_changed(hass, utc_datetime, fire_all) -@ha.callback +@callback def async_fire_time_changed( hass: HomeAssistant, datetime_: datetime | None = None, fire_all: bool = False ) -> None: @@ -415,7 +428,7 @@ def async_fire_time_changed( for an exact microsecond, use async_fire_time_changed_exact. """ if datetime_ is None: - utc_datetime = date_util.utcnow() + utc_datetime = datetime.now(timezone.utc) else: utc_datetime = date_util.as_utc(datetime_) @@ -429,7 +442,7 @@ def async_fire_time_changed( _async_fire_time_changed(hass, utc_datetime, fire_all) -@ha.callback +@callback def _async_fire_time_changed( hass: HomeAssistant, utc_datetime: datetime | None, fire_all: bool ) -> None: @@ -488,7 +501,7 @@ def mock_state_change_event( hass.bus.fire(EVENT_STATE_CHANGED, event_data, context=new_state.context) -@ha.callback +@callback def mock_component(hass: HomeAssistant, component: str) -> None: """Mock a component is setup.""" if component in hass.config.components: @@ -621,7 +634,7 @@ async def register_auth_provider( return provider -@ha.callback +@callback def ensure_auth_manager_loaded(auth_mgr): """Ensure an auth manager is considered loaded.""" store = auth_mgr._store @@ -966,11 +979,15 @@ def assert_setup_component(count, domain=None): ), f"setup_component failed, expected {count} got {res_len}: {res}" -SetupRecorderInstanceT = Callable[..., Awaitable[recorder.Recorder]] +SetupRecorderInstanceT = Callable[..., Awaitable[Any]] def init_recorder_component(hass, add_config=None, db_url="sqlite://"): """Initialize the recorder.""" + # Local import to avoid processing recorder and SQLite modules when running a + # testcase which does not use the recorder. + from homeassistant.components import recorder + config = dict(add_config) if add_config else {} if recorder.CONF_DB_URL not in config: config[recorder.CONF_DB_URL] = db_url @@ -988,7 +1005,7 @@ def init_recorder_component(hass, add_config=None, db_url="sqlite://"): ) -def mock_restore_cache(hass, states): +def mock_restore_cache(hass: HomeAssistant, states: Sequence[State]) -> None: """Mock the DATA_RESTORE_CACHE.""" key = restore_state.DATA_RESTORE_STATE_TASK data = restore_state.RestoreStateData(hass) @@ -1013,7 +1030,9 @@ def mock_restore_cache(hass, states): hass.data[key] = data -def mock_restore_cache_with_extra_data(hass, states): +def mock_restore_cache_with_extra_data( + hass: HomeAssistant, states: Sequence[tuple[State, Mapping[str, Any]]] +) -> None: """Mock the DATA_RESTORE_CACHE.""" key = restore_state.DATA_RESTORE_STATE_TASK data = restore_state.RestoreStateData(hass) @@ -1041,7 +1060,7 @@ def mock_restore_cache_with_extra_data(hass, states): class MockEntity(entity.Entity): """Mock Entity class.""" - def __init__(self, **values): + def __init__(self, **values: Any) -> None: """Initialize an entity.""" self._values = values @@ -1049,86 +1068,86 @@ class MockEntity(entity.Entity): self.entity_id = values["entity_id"] @property - def available(self): + def available(self) -> bool: """Return True if entity is available.""" return self._handle("available") @property - def capability_attributes(self): + def capability_attributes(self) -> Mapping[str, Any] | None: """Info about capabilities.""" return self._handle("capability_attributes") @property - def device_class(self): + def device_class(self) -> str | None: """Info how device should be classified.""" return self._handle("device_class") @property - def device_info(self): + def device_info(self) -> entity.DeviceInfo | None: """Info how it links to a device.""" return self._handle("device_info") @property - def entity_category(self): + def entity_category(self) -> entity.EntityCategory | None: """Return the entity category.""" return self._handle("entity_category") @property - def has_entity_name(self): + def has_entity_name(self) -> bool: """Return the has_entity_name name flag.""" return self._handle("has_entity_name") @property - def entity_registry_enabled_default(self): + def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" return self._handle("entity_registry_enabled_default") @property - def entity_registry_visible_default(self): + def entity_registry_visible_default(self) -> bool: """Return if the entity should be visible when first added to the entity registry.""" return self._handle("entity_registry_visible_default") @property - def icon(self): + def icon(self) -> str | None: """Return the suggested icon.""" return self._handle("icon") @property - def name(self): + def name(self) -> str | None: """Return the name of the entity.""" return self._handle("name") @property - def should_poll(self): + def should_poll(self) -> bool: """Return the ste of the polling.""" return self._handle("should_poll") @property - def state(self): + def state(self) -> StateType: """Return the state of the entity.""" return self._handle("state") @property - def supported_features(self): + def supported_features(self) -> int | None: """Info about supported features.""" return self._handle("supported_features") @property - def translation_key(self): + def translation_key(self) -> str | None: """Return the translation key.""" return self._handle("translation_key") @property - def unique_id(self): + def unique_id(self) -> str | None: """Return the unique ID of the entity.""" return self._handle("unique_id") @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Info on the units the entity state is in.""" return self._handle("unit_of_measurement") - def _handle(self, attr): + def _handle(self, attr: str) -> Any: """Return attribute value.""" if attr in self._values: return self._values[attr] @@ -1165,13 +1184,13 @@ def mock_storage(data=None): # Route through original load so that we trigger migration loaded = await orig_load(store) - _LOGGER.info("Loading data for %s: %s", store.key, loaded) + _LOGGER.debug("Loading data for %s: %s", store.key, loaded) return loaded - def mock_write_data(store, path, data_to_write): + async def mock_write_data(store, path, data_to_write): """Mock version of write data.""" # To ensure that the data can be serialized - _LOGGER.info("Writing data to %s: %s", store.key, data_to_write) + _LOGGER.debug("Writing data to %s: %s", store.key, data_to_write) raise_contains_mocks(data_to_write) data[store.key] = json.loads(json.dumps(data_to_write, cls=store._encoder)) @@ -1184,7 +1203,7 @@ def mock_storage(data=None): side_effect=mock_async_load, autospec=True, ), patch( - "homeassistant.helpers.storage.Store._write_data", + "homeassistant.helpers.storage.Store._async_write_data", side_effect=mock_write_data, autospec=True, ), patch( @@ -1195,7 +1214,7 @@ def mock_storage(data=None): yield data -async def flush_store(store): +async def flush_store(store: storage.Store) -> None: """Make sure all delayed writes of a store are written.""" if store._data is None: return @@ -1205,12 +1224,14 @@ async def flush_store(store): await store._async_handle_write_data() -async def get_system_health_info(hass, domain): +async def get_system_health_info(hass: HomeAssistant, domain: str) -> dict[str, Any]: """Get system health info.""" return await hass.data["system_health"][domain].info_callback(hass) -def mock_integration(hass, module, built_in=True): +def mock_integration( + hass: HomeAssistant, module: MockModule, built_in: bool = True +) -> loader.Integration: """Mock an integration.""" integration = loader.Integration( hass, @@ -1221,7 +1242,7 @@ def mock_integration(hass, module, built_in=True): module.mock_manifest(), ) - def mock_import_platform(platform_name): + def mock_import_platform(platform_name: str) -> NoReturn: raise ImportError( f"Mocked unable to import platform '{platform_name}'", name=f"{integration.pkg_path}.{platform_name}", @@ -1236,7 +1257,9 @@ def mock_integration(hass, module, built_in=True): return integration -def mock_entity_platform(hass, platform_path, module): +def mock_entity_platform( + hass: HomeAssistant, platform_path: str, module: MockPlatform | None +) -> None: """Mock a entity platform. platform_path is in form light.hue. Will create platform @@ -1246,7 +1269,9 @@ def mock_entity_platform(hass, platform_path, module): mock_platform(hass, f"{platform_name}.{domain}", module) -def mock_platform(hass, platform_path, module=None): +def mock_platform( + hass: HomeAssistant, platform_path: str, module: Mock | MockPlatform | None = None +) -> None: """Mock a platform. platform_path is in form hue.config_flow. @@ -1262,12 +1287,12 @@ def mock_platform(hass, platform_path, module=None): module_cache[platform_path] = module or Mock() -def async_capture_events(hass, event_name): +def async_capture_events(hass: HomeAssistant, event_name: str) -> list[Event]: """Create a helper that captures events.""" events = [] - @ha.callback - def capture_events(event): + @callback + def capture_events(event: Event) -> None: events.append(event) hass.bus.async_listen(event_name, capture_events) @@ -1275,13 +1300,13 @@ def async_capture_events(hass, event_name): return events -@ha.callback -def async_mock_signal(hass, signal): +@callback +def async_mock_signal(hass: HomeAssistant, signal: str) -> list[tuple[Any]]: """Catch all dispatches to a signal.""" calls = [] - @ha.callback - def mock_signal_handler(*args): + @callback + def mock_signal_handler(*args: Any) -> None: """Mock service call.""" calls.append(args) @@ -1290,7 +1315,7 @@ def async_mock_signal(hass, signal): return calls -def assert_lists_same(a, b): +def assert_lists_same(a: list[Any], b: list[Any]) -> None: """Compare two lists, ignoring order. Check both that all items in a are in b and that all items in b are in a, @@ -1315,17 +1340,17 @@ class _HA_ANY: _other = _SENTINEL - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: """Test equal.""" self._other = other return True - def __ne__(self, other): + def __ne__(self, other: Any) -> bool: """Test not equal.""" self._other = other return False - def __repr__(self): + def __repr__(self) -> str: """Return repr() other to not show up in pytest quality diffs.""" if self._other is _SENTINEL: return "" @@ -1335,7 +1360,7 @@ class _HA_ANY: ANY = _HA_ANY() -def raise_contains_mocks(val): +def raise_contains_mocks(val: Any) -> None: """Raise for mocks.""" if isinstance(val, Mock): raise ValueError diff --git a/tests/components/abode/common.py b/tests/components/abode/common.py index dd9b889fe27..f9ae52a2709 100644 --- a/tests/components/abode/common.py +++ b/tests/components/abode/common.py @@ -23,8 +23,8 @@ async def setup_platform(hass: HomeAssistant, platform: str) -> MockConfigEntry: mock_entry.add_to_hass(hass) with patch("homeassistant.components.abode.PLATFORMS", [platform]), patch( - "abodepy.event_controller.sio" - ), patch("abodepy.utils.save_cache"): + "jaraco.abode.event_controller.sio" + ): assert await async_setup_component(hass, ABODE_DOMAIN, {}) await hass.async_block_till_done() diff --git a/tests/components/abode/conftest.py b/tests/components/abode/conftest.py index e41cf3ec587..42b86f88e87 100644 --- a/tests/components/abode/conftest.py +++ b/tests/components/abode/conftest.py @@ -1,5 +1,5 @@ """Configuration for Abode tests.""" -import abodepy.helpers.constants as CONST +from jaraco.abode.helpers import urls as URL import pytest from tests.common import load_fixture @@ -10,18 +10,14 @@ from tests.components.light.conftest import mock_light_profiles # noqa: F401 def requests_mock_fixture(requests_mock) -> None: """Fixture to provide a requests mocker.""" # Mocks the login response for abodepy. - requests_mock.post(CONST.LOGIN_URL, text=load_fixture("login.json", "abode")) + requests_mock.post(URL.LOGIN, text=load_fixture("login.json", "abode")) # Mocks the logout response for abodepy. - requests_mock.post(CONST.LOGOUT_URL, text=load_fixture("logout.json", "abode")) + requests_mock.post(URL.LOGOUT, text=load_fixture("logout.json", "abode")) # Mocks the oauth claims response for abodepy. - requests_mock.get( - CONST.OAUTH_TOKEN_URL, text=load_fixture("oauth_claims.json", "abode") - ) + requests_mock.get(URL.OAUTH_TOKEN, text=load_fixture("oauth_claims.json", "abode")) # Mocks the panel response for abodepy. - requests_mock.get(CONST.PANEL_URL, text=load_fixture("panel.json", "abode")) + requests_mock.get(URL.PANEL, text=load_fixture("panel.json", "abode")) # Mocks the automations response for abodepy. - requests_mock.get( - CONST.AUTOMATION_URL, text=load_fixture("automation.json", "abode") - ) + requests_mock.get(URL.AUTOMATION, text=load_fixture("automation.json", "abode")) # Mocks the devices response for abodepy. - requests_mock.get(CONST.DEVICES_URL, text=load_fixture("devices.json", "abode")) + requests_mock.get(URL.DEVICES, text=load_fixture("devices.json", "abode")) diff --git a/tests/components/abode/test_alarm_control_panel.py b/tests/components/abode/test_alarm_control_panel.py index 74d64731128..6924c440bb4 100644 --- a/tests/components/abode/test_alarm_control_panel.py +++ b/tests/components/abode/test_alarm_control_panel.py @@ -1,7 +1,7 @@ """Tests for the Abode alarm control panel device.""" from unittest.mock import PropertyMock, patch -import abodepy.helpers.constants as CONST +from jaraco.abode.helpers import constants as CONST from homeassistant.components.abode import ATTR_DEVICE_ID from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN @@ -49,8 +49,10 @@ async def test_attributes(hass: HomeAssistant) -> None: async def test_set_alarm_away(hass: HomeAssistant) -> None: """Test the alarm control panel can be set to away.""" - with patch("abodepy.AbodeEventController.add_device_callback") as mock_callback: - with patch("abodepy.ALARM.AbodeAlarm.set_away") as mock_set_away: + with patch( + "jaraco.abode.event_controller.EventController.add_device_callback" + ) as mock_callback: + with patch("jaraco.abode.devices.alarm.Alarm.set_away") as mock_set_away: await setup_platform(hass, ALARM_DOMAIN) await hass.services.async_call( @@ -63,7 +65,7 @@ async def test_set_alarm_away(hass: HomeAssistant) -> None: mock_set_away.assert_called_once() with patch( - "abodepy.ALARM.AbodeAlarm.mode", + "jaraco.abode.devices.alarm.Alarm.mode", new_callable=PropertyMock, ) as mock_mode: mock_mode.return_value = CONST.MODE_AWAY @@ -78,8 +80,10 @@ async def test_set_alarm_away(hass: HomeAssistant) -> None: async def test_set_alarm_home(hass: HomeAssistant) -> None: """Test the alarm control panel can be set to home.""" - with patch("abodepy.AbodeEventController.add_device_callback") as mock_callback: - with patch("abodepy.ALARM.AbodeAlarm.set_home") as mock_set_home: + with patch( + "jaraco.abode.event_controller.EventController.add_device_callback" + ) as mock_callback: + with patch("jaraco.abode.devices.alarm.Alarm.set_home") as mock_set_home: await setup_platform(hass, ALARM_DOMAIN) await hass.services.async_call( @@ -92,7 +96,7 @@ async def test_set_alarm_home(hass: HomeAssistant) -> None: mock_set_home.assert_called_once() with patch( - "abodepy.ALARM.AbodeAlarm.mode", new_callable=PropertyMock + "jaraco.abode.devices.alarm.Alarm.mode", new_callable=PropertyMock ) as mock_mode: mock_mode.return_value = CONST.MODE_HOME @@ -106,8 +110,10 @@ async def test_set_alarm_home(hass: HomeAssistant) -> None: async def test_set_alarm_standby(hass: HomeAssistant) -> None: """Test the alarm control panel can be set to standby.""" - with patch("abodepy.AbodeEventController.add_device_callback") as mock_callback: - with patch("abodepy.ALARM.AbodeAlarm.set_standby") as mock_set_standby: + with patch( + "jaraco.abode.event_controller.EventController.add_device_callback" + ) as mock_callback: + with patch("jaraco.abode.devices.alarm.Alarm.set_standby") as mock_set_standby: await setup_platform(hass, ALARM_DOMAIN) await hass.services.async_call( ALARM_DOMAIN, @@ -119,7 +125,7 @@ async def test_set_alarm_standby(hass: HomeAssistant) -> None: mock_set_standby.assert_called_once() with patch( - "abodepy.ALARM.AbodeAlarm.mode", new_callable=PropertyMock + "jaraco.abode.devices.alarm.Alarm.mode", new_callable=PropertyMock ) as mock_mode: mock_mode.return_value = CONST.MODE_STANDBY @@ -133,7 +139,9 @@ async def test_set_alarm_standby(hass: HomeAssistant) -> None: async def test_state_unknown(hass: HomeAssistant) -> None: """Test an unknown alarm control panel state.""" - with patch("abodepy.ALARM.AbodeAlarm.mode", new_callable=PropertyMock) as mock_mode: + with patch( + "jaraco.abode.devices.alarm.Alarm.mode", new_callable=PropertyMock + ) as mock_mode: await setup_platform(hass, ALARM_DOMAIN) await hass.async_block_till_done() diff --git a/tests/components/abode/test_camera.py b/tests/components/abode/test_camera.py index fd490c4a1c2..4bfc16d9689 100644 --- a/tests/components/abode/test_camera.py +++ b/tests/components/abode/test_camera.py @@ -31,7 +31,7 @@ async def test_capture_image(hass: HomeAssistant) -> None: """Test the camera capture image service.""" await setup_platform(hass, CAMERA_DOMAIN) - with patch("abodepy.AbodeCamera.capture") as mock_capture: + with patch("jaraco.abode.devices.camera.Camera.capture") as mock_capture: await hass.services.async_call( ABODE_DOMAIN, "capture_image", @@ -46,7 +46,7 @@ async def test_camera_on(hass: HomeAssistant) -> None: """Test the camera turn on service.""" await setup_platform(hass, CAMERA_DOMAIN) - with patch("abodepy.AbodeCamera.privacy_mode") as mock_capture: + with patch("jaraco.abode.devices.camera.Camera.privacy_mode") as mock_capture: await hass.services.async_call( CAMERA_DOMAIN, "turn_on", @@ -61,7 +61,7 @@ async def test_camera_off(hass: HomeAssistant) -> None: """Test the camera turn off service.""" await setup_platform(hass, CAMERA_DOMAIN) - with patch("abodepy.AbodeCamera.privacy_mode") as mock_capture: + with patch("jaraco.abode.devices.camera.Camera.privacy_mode") as mock_capture: await hass.services.async_call( CAMERA_DOMAIN, "turn_off", diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py index 987a0b74996..c16fea4ef20 100644 --- a/tests/components/abode/test_config_flow.py +++ b/tests/components/abode/test_config_flow.py @@ -2,8 +2,10 @@ from http import HTTPStatus from unittest.mock import patch -from abodepy.exceptions import AbodeAuthenticationException -from abodepy.helpers.errors import MFA_CODE_REQUIRED +from jaraco.abode.exceptions import ( + AuthenticationException as AbodeAuthenticationException, +) +from jaraco.abode.helpers.errors import MFA_CODE_REQUIRED from requests.exceptions import ConnectTimeout from homeassistant import data_entry_flow @@ -96,9 +98,7 @@ async def test_step_user(hass: HomeAssistant) -> None: """Test that the user step works.""" conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} - with patch("homeassistant.components.abode.config_flow.Abode"), patch( - "abodepy.UTILS" - ): + with patch("homeassistant.components.abode.config_flow.Abode"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf @@ -140,9 +140,7 @@ async def test_step_mfa(hass: HomeAssistant) -> None: assert result["errors"] == {"base": "invalid_mfa_code"} - with patch("homeassistant.components.abode.config_flow.Abode"), patch( - "abodepy.UTILS" - ): + with patch("homeassistant.components.abode.config_flow.Abode"): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={"mfa_code": "123456"} ) @@ -166,9 +164,7 @@ async def test_step_reauth(hass: HomeAssistant) -> None: data=conf, ).add_to_hass(hass) - with patch("homeassistant.components.abode.config_flow.Abode"), patch( - "abodepy.UTILS" - ): + with patch("homeassistant.components.abode.config_flow.Abode"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, diff --git a/tests/components/abode/test_cover.py b/tests/components/abode/test_cover.py index bd7104bff3f..a187c0c447e 100644 --- a/tests/components/abode/test_cover.py +++ b/tests/components/abode/test_cover.py @@ -44,7 +44,7 @@ async def test_open(hass: HomeAssistant) -> None: """Test the cover can be opened.""" await setup_platform(hass, COVER_DOMAIN) - with patch("abodepy.AbodeCover.open_cover") as mock_open: + with patch("jaraco.abode.devices.cover.Cover.open_cover") as mock_open: await hass.services.async_call( COVER_DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True ) @@ -56,7 +56,7 @@ async def test_close(hass: HomeAssistant) -> None: """Test the cover can be closed.""" await setup_platform(hass, COVER_DOMAIN) - with patch("abodepy.AbodeCover.close_cover") as mock_close: + with patch("jaraco.abode.devices.cover.Cover.close_cover") as mock_close: await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER, diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py index 9ed2fc82595..17039235f37 100644 --- a/tests/components/abode/test_init.py +++ b/tests/components/abode/test_init.py @@ -2,7 +2,10 @@ from http import HTTPStatus from unittest.mock import patch -from abodepy.exceptions import AbodeAuthenticationException, AbodeException +from jaraco.abode.exceptions import ( + AuthenticationException as AbodeAuthenticationException, + Exception as AbodeException, +) from homeassistant import data_entry_flow from homeassistant.components.abode import ( @@ -23,7 +26,7 @@ async def test_change_settings(hass: HomeAssistant) -> None: """Test change_setting service.""" await setup_platform(hass, ALARM_DOMAIN) - with patch("abodepy.Abode.set_setting") as mock_set_setting: + with patch("jaraco.abode.client.Client.set_setting") as mock_set_setting: await hass.services.async_call( ABODE_DOMAIN, SERVICE_SETTINGS, @@ -43,9 +46,8 @@ async def test_add_unique_id(hass: HomeAssistant) -> None: assert mock_entry.unique_id is None - with patch("abodepy.UTILS"): - await hass.config_entries.async_reload(mock_entry.entry_id) - await hass.async_block_till_done() + await hass.config_entries.async_reload(mock_entry.entry_id) + await hass.async_block_till_done() assert mock_entry.unique_id == mock_entry.data[CONF_USERNAME] @@ -54,8 +56,8 @@ async def test_unload_entry(hass: HomeAssistant) -> None: """Test unloading the Abode entry.""" mock_entry = await setup_platform(hass, ALARM_DOMAIN) - with patch("abodepy.Abode.logout") as mock_logout, patch( - "abodepy.event_controller.AbodeEventController.stop" + with patch("jaraco.abode.client.Client.logout") as mock_logout, patch( + "jaraco.abode.event_controller.EventController.stop" ) as mock_events_stop: assert await hass.config_entries.async_unload(mock_entry.entry_id) mock_logout.assert_called_once() diff --git a/tests/components/abode/test_light.py b/tests/components/abode/test_light.py index 3514376d5a0..5716a18f195 100644 --- a/tests/components/abode/test_light.py +++ b/tests/components/abode/test_light.py @@ -62,7 +62,7 @@ async def test_switch_off(hass: HomeAssistant) -> None: """Test the light can be turned off.""" await setup_platform(hass, LIGHT_DOMAIN) - with patch("abodepy.AbodeLight.switch_off") as mock_switch_off: + with patch("jaraco.abode.devices.light.Light.switch_off") as mock_switch_off: assert await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True ) @@ -74,7 +74,7 @@ async def test_switch_on(hass: HomeAssistant) -> None: """Test the light can be turned on.""" await setup_platform(hass, LIGHT_DOMAIN) - with patch("abodepy.AbodeLight.switch_on") as mock_switch_on: + with patch("jaraco.abode.devices.light.Light.switch_on") as mock_switch_on: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True ) @@ -86,7 +86,7 @@ async def test_set_brightness(hass: HomeAssistant) -> None: """Test the brightness can be set.""" await setup_platform(hass, LIGHT_DOMAIN) - with patch("abodepy.AbodeLight.set_level") as mock_set_level: + with patch("jaraco.abode.devices.light.Light.set_level") as mock_set_level: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, @@ -102,7 +102,7 @@ async def test_set_color(hass: HomeAssistant) -> None: """Test the color can be set.""" await setup_platform(hass, LIGHT_DOMAIN) - with patch("abodepy.AbodeLight.set_color") as mock_set_color: + with patch("jaraco.abode.devices.light.Light.set_color") as mock_set_color: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, @@ -117,7 +117,9 @@ async def test_set_color_temp(hass: HomeAssistant) -> None: """Test the color temp can be set.""" await setup_platform(hass, LIGHT_DOMAIN) - with patch("abodepy.AbodeLight.set_color_temp") as mock_set_color_temp: + with patch( + "jaraco.abode.devices.light.Light.set_color_temp" + ) as mock_set_color_temp: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, diff --git a/tests/components/abode/test_lock.py b/tests/components/abode/test_lock.py index 837b62e06cd..ca1a4794bdb 100644 --- a/tests/components/abode/test_lock.py +++ b/tests/components/abode/test_lock.py @@ -44,7 +44,7 @@ async def test_lock(hass: HomeAssistant) -> None: """Test the lock can be locked.""" await setup_platform(hass, LOCK_DOMAIN) - with patch("abodepy.AbodeLock.lock") as mock_lock: + with patch("jaraco.abode.devices.lock.Lock.lock") as mock_lock: await hass.services.async_call( LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True ) @@ -56,7 +56,7 @@ async def test_unlock(hass: HomeAssistant) -> None: """Test the lock can be unlocked.""" await setup_platform(hass, LOCK_DOMAIN) - with patch("abodepy.AbodeLock.unlock") as mock_unlock: + with patch("jaraco.abode.devices.lock.Lock.unlock") as mock_unlock: await hass.services.async_call( LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True ) diff --git a/tests/components/abode/test_sensor.py b/tests/components/abode/test_sensor.py index ba163ba87d9..5d074de214f 100644 --- a/tests/components/abode/test_sensor.py +++ b/tests/components/abode/test_sensor.py @@ -6,7 +6,7 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -44,4 +44,4 @@ async def test_attributes(hass: HomeAssistant) -> None: state = hass.states.get("sensor.environment_sensor_temperature") # Abodepy device JSON reports 19.5, but Home Assistant shows 19.4 assert state.state == "19.4" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS diff --git a/tests/components/abode/test_switch.py b/tests/components/abode/test_switch.py index 74fa6491f66..bd9a5f8d72d 100644 --- a/tests/components/abode/test_switch.py +++ b/tests/components/abode/test_switch.py @@ -48,7 +48,7 @@ async def test_switch_on(hass: HomeAssistant) -> None: """Test the switch can be turned on.""" await setup_platform(hass, SWITCH_DOMAIN) - with patch("abodepy.AbodeSwitch.switch_on") as mock_switch_on: + with patch("jaraco.abode.devices.switch.Switch.switch_on") as mock_switch_on: assert await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True ) @@ -61,7 +61,7 @@ async def test_switch_off(hass: HomeAssistant) -> None: """Test the switch can be turned off.""" await setup_platform(hass, SWITCH_DOMAIN) - with patch("abodepy.AbodeSwitch.switch_off") as mock_switch_off: + with patch("jaraco.abode.devices.switch.Switch.switch_off") as mock_switch_off: assert await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True ) @@ -81,7 +81,7 @@ async def test_automation_attributes(hass: HomeAssistant) -> None: async def test_turn_automation_off(hass: HomeAssistant) -> None: """Test the automation can be turned off.""" - with patch("abodepy.AbodeAutomation.enable") as mock_trigger: + with patch("jaraco.abode.automation.Automation.enable") as mock_trigger: await setup_platform(hass, SWITCH_DOMAIN) await hass.services.async_call( @@ -97,7 +97,7 @@ async def test_turn_automation_off(hass: HomeAssistant) -> None: async def test_turn_automation_on(hass: HomeAssistant) -> None: """Test the automation can be turned on.""" - with patch("abodepy.AbodeAutomation.enable") as mock_trigger: + with patch("jaraco.abode.automation.Automation.enable") as mock_trigger: await setup_platform(hass, SWITCH_DOMAIN) await hass.services.async_call( @@ -115,7 +115,7 @@ async def test_trigger_automation(hass: HomeAssistant) -> None: """Test the trigger automation service.""" await setup_platform(hass, SWITCH_DOMAIN) - with patch("abodepy.AbodeAutomation.trigger") as mock: + with patch("jaraco.abode.automation.Automation.trigger") as mock: await hass.services.async_call( ABODE_DOMAIN, SERVICE_TRIGGER_AUTOMATION, diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index 251f5a16932..0182f7584b1 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -17,15 +17,14 @@ from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_PARTS_PER_CUBIC_METER, - LENGTH_FEET, - LENGTH_METERS, - LENGTH_MILLIMETERS, PERCENTAGE, - SPEED_KILOMETERS_PER_HOUR, STATE_UNAVAILABLE, - TEMP_CELSIUS, - TIME_HOURS, UV_INDEX, + UnitOfLength, + UnitOfSpeed, + UnitOfTemperature, + UnitOfTime, + UnitOfVolumetricFlux, ) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -47,7 +46,7 @@ async def test_sensor_without_forecast(hass): assert state.state == "3200" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_METERS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.METERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DISTANCE @@ -59,11 +58,17 @@ async def test_sensor_without_forecast(hass): assert state assert state.state == "0.0" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILLIMETERS + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR + ) assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get("type") is None assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRECIPITATION + assert ( + state.attributes.get(ATTR_DEVICE_CLASS) + == SensorDeviceClass.PRECIPITATION_INTENSITY + ) entry = registry.async_get("sensor.home_precipitation") assert entry @@ -86,7 +91,7 @@ async def test_sensor_without_forecast(hass): assert state assert state.state == "25.1" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -117,7 +122,7 @@ async def test_sensor_with_forecast(hass): assert state.state == "7.2" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ICON) == "mdi:weather-partly-cloudy" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_HOURS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.HOURS assert state.attributes.get(ATTR_STATE_CLASS) is None entry = registry.async_get("sensor.home_hours_of_sun_0d") @@ -128,7 +133,7 @@ async def test_sensor_with_forecast(hass): assert state assert state.state == "29.8" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is None @@ -139,7 +144,7 @@ async def test_sensor_with_forecast(hass): assert state assert state.state == "15.1" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is None @@ -363,7 +368,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "22.8" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -387,7 +392,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "16.2" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -399,7 +404,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "21.1" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -411,7 +416,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "18.6" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -423,7 +428,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "22.8" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -435,7 +440,10 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "20.3" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfSpeed.KILOMETERS_PER_HOUR + ) assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED @@ -448,7 +456,10 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "14.5" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfSpeed.KILOMETERS_PER_HOUR + ) assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED @@ -542,7 +553,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "28.0" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is None @@ -554,7 +565,7 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "15.1" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE entry = registry.async_get("sensor.home_realfeel_temperature_shade_min_0d") @@ -581,7 +592,10 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "13.0" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfSpeed.KILOMETERS_PER_HOUR + ) assert state.attributes.get("direction") == "SSE" assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WIND_SPEED @@ -594,7 +608,10 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "7.4" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfSpeed.KILOMETERS_PER_HOUR + ) assert state.attributes.get("direction") == "WNW" assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_STATE_CLASS) is None @@ -608,7 +625,10 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "29.6" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfSpeed.KILOMETERS_PER_HOUR + ) assert state.attributes.get("direction") == "S" assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_STATE_CLASS) is None @@ -622,7 +642,10 @@ async def test_sensor_enabled_without_forecast(hass): assert state assert state.state == "18.5" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfSpeed.KILOMETERS_PER_HOUR + ) assert state.attributes.get("direction") == "WSW" assert state.attributes.get(ATTR_ICON) is None assert state.attributes.get(ATTR_STATE_CLASS) is None @@ -714,7 +737,7 @@ async def test_sensor_imperial_units(hass): assert state.state == "10500" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_FEET + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.FEET async def test_state_update(hass): diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index 2fdae7b9d6b..54f1d2d86af 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -44,9 +44,11 @@ async def test_connection_error( ) -> None: """Test we show user form on AdGuard Home connection error.""" aioclient_mock.get( - f"{'https' if FIXTURE_USER_INPUT[CONF_SSL] else 'http'}" - f"://{FIXTURE_USER_INPUT[CONF_HOST]}" - f":{FIXTURE_USER_INPUT[CONF_PORT]}/control/status", + ( + f"{'https' if FIXTURE_USER_INPUT[CONF_SSL] else 'http'}" + f"://{FIXTURE_USER_INPUT[CONF_HOST]}" + f":{FIXTURE_USER_INPUT[CONF_PORT]}/control/status" + ), exc=aiohttp.ClientError, ) @@ -65,9 +67,11 @@ async def test_full_flow_implementation( ) -> None: """Test registering an integration and finishing flow works.""" aioclient_mock.get( - f"{'https' if FIXTURE_USER_INPUT[CONF_SSL] else 'http'}" - f"://{FIXTURE_USER_INPUT[CONF_HOST]}" - f":{FIXTURE_USER_INPUT[CONF_PORT]}/control/status", + ( + f"{'https' if FIXTURE_USER_INPUT[CONF_SSL] else 'http'}" + f"://{FIXTURE_USER_INPUT[CONF_HOST]}" + f":{FIXTURE_USER_INPUT[CONF_PORT]}/control/status" + ), json={"version": "v0.99.0"}, headers={"Content-Type": CONTENT_TYPE_JSON}, ) diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index b7c2a65812e..dc20888f532 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -62,6 +62,22 @@ async def test_invalid_location(hass, aioclient_mock): assert result["errors"] == {"base": "wrong_location"} +async def test_invalid_location_for_point_and_nearest(hass, aioclient_mock): + """Test an abort when the location is wrong for the point and nearest methods.""" + + aioclient_mock.get(API_POINT_URL, text=load_fixture("no_station.json", "airly")) + + aioclient_mock.get(API_NEAREST_URL, text=load_fixture("no_station.json", "airly")) + + with patch("homeassistant.components.airly.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "wrong_location" + + async def test_duplicate_error(hass, aioclient_mock): """Test that errors are shown when duplicates are added.""" aioclient_mock.get(API_POINT_URL, text=load_fixture("valid_station.json", "airly")) diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py index c95d2d895fd..bdc1e909f35 100644 --- a/tests/components/airly/test_sensor.py +++ b/tests/components/airly/test_sensor.py @@ -1,5 +1,8 @@ """Test sensor of Airly integration.""" from datetime import timedelta +from http import HTTPStatus + +from airly.exceptions import AirlyError from homeassistant.components.airly.sensor import ATTRIBUTION from homeassistant.components.sensor import ( @@ -15,9 +18,9 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, PERCENTAGE, - PRESSURE_HPA, STATE_UNAVAILABLE, - TEMP_CELSIUS, + UnitOfPressure, + UnitOfTemperature, ) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -164,7 +167,7 @@ async def test_sensor(hass, aioclient_mock): assert state assert state.state == "1020" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.HPA assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -176,7 +179,7 @@ async def test_sensor(hass, aioclient_mock): assert state assert state.state == "14.4" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -195,7 +198,9 @@ async def test_availability(hass, aioclient_mock): assert state.state == "68.3" aioclient_mock.clear_requests() - aioclient_mock.get(API_POINT_URL, exc=ConnectionError()) + aioclient_mock.get( + API_POINT_URL, exc=AirlyError(HTTPStatus.NOT_FOUND, {"message": "Not found"}) + ) future = utcnow() + timedelta(minutes=60) async_fire_time_changed(hass, future) await hass.async_block_till_done() diff --git a/tests/components/airvisual/conftest.py b/tests/components/airvisual/conftest.py index 8ef060c3116..c85d6e90c4b 100644 --- a/tests/components/airvisual/conftest.py +++ b/tests/components/airvisual/conftest.py @@ -4,29 +4,72 @@ from unittest.mock import AsyncMock, Mock, patch import pytest -from homeassistant.components.airvisual.const import ( +from homeassistant.components.airvisual import ( + CONF_CITY, CONF_INTEGRATION_TYPE, DOMAIN, INTEGRATION_TYPE_GEOGRAPHY_COORDS, ) +from homeassistant.components.airvisual.config_flow import async_get_geography_id from homeassistant.const import ( CONF_API_KEY, + CONF_COUNTRY, CONF_LATITUDE, CONF_LONGITUDE, CONF_SHOW_ON_MAP, + CONF_STATE, ) -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture +TEST_API_KEY = "abcde12345" +TEST_LATITUDE = 51.528308 +TEST_LONGITUDE = -0.3817765 +TEST_LATITUDE2 = 37.514626 +TEST_LONGITUDE2 = 127.057414 + +COORDS_CONFIG = { + CONF_API_KEY: TEST_API_KEY, + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, +} + +COORDS_CONFIG2 = { + CONF_API_KEY: TEST_API_KEY, + CONF_LATITUDE: TEST_LATITUDE2, + CONF_LONGITUDE: TEST_LONGITUDE2, +} + +TEST_CITY = "Beijing" +TEST_STATE = "Beijing" +TEST_COUNTRY = "China" + +NAME_CONFIG = { + CONF_API_KEY: TEST_API_KEY, + CONF_CITY: TEST_CITY, + CONF_STATE: TEST_STATE, + CONF_COUNTRY: TEST_COUNTRY, +} + + +@pytest.fixture(name="cloud_api") +def cloud_api_fixture(data_cloud): + """Define a mock CloudAPI object.""" + return Mock( + air_quality=Mock( + city=AsyncMock(return_value=data_cloud), + nearest_city=AsyncMock(return_value=data_cloud), + ) + ) + @pytest.fixture(name="config_entry") -def config_entry_fixture(hass, config, config_entry_version, unique_id): +def config_entry_fixture(hass, config, config_entry_version, integration_type): """Define a config entry fixture.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id=unique_id, - data={CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS, **config}, + unique_id=async_get_geography_id(config), + data={**config, CONF_INTEGRATION_TYPE: integration_type}, options={CONF_SHOW_ON_MAP: True}, version=config_entry_version, ) @@ -41,49 +84,61 @@ def config_entry_version_fixture(): @pytest.fixture(name="config") -def config_fixture(hass): +def config_fixture(): """Define a config entry data fixture.""" - return { - CONF_API_KEY: "abcde12345", - CONF_LATITUDE: 51.528308, - CONF_LONGITUDE: -0.3817765, - } + return COORDS_CONFIG -@pytest.fixture(name="data", scope="package") -def data_fixture(): +@pytest.fixture(name="data_cloud", scope="package") +def data_cloud_fixture(): """Define an update coordinator data example.""" return json.loads(load_fixture("data.json", "airvisual")) -@pytest.fixture(name="pro_data", scope="session") -def pro_data_fixture(): +@pytest.fixture(name="data_pro", scope="package") +def data_pro_fixture(): """Define an update coordinator data example for the Pro.""" return json.loads(load_fixture("data.json", "airvisual_pro")) -@pytest.fixture(name="pro") -def pro_fixture(pro_data): - """Define a mocked NodeSamba object.""" - return Mock( - async_connect=AsyncMock(), - async_disconnect=AsyncMock(), - async_get_latest_measurements=AsyncMock(return_value=pro_data), - ) +@pytest.fixture(name="integration_type") +def integration_type_fixture(): + """Define an integration type.""" + return INTEGRATION_TYPE_GEOGRAPHY_COORDS -@pytest.fixture(name="setup_airvisual") -async def setup_airvisual_fixture(hass, config, data): - """Define a fixture to set up AirVisual.""" - with patch("pyairvisual.air_quality.AirQuality.city"), patch( - "pyairvisual.air_quality.AirQuality.nearest_city", return_value=data +@pytest.fixture(name="mock_pyairvisual") +async def mock_pyairvisual_fixture(cloud_api, node_samba): + """Define a fixture to patch pyairvisual.""" + with patch( + "homeassistant.components.airvisual.CloudAPI", + return_value=cloud_api, + ), patch( + "homeassistant.components.airvisual.config_flow.CloudAPI", + return_value=cloud_api, + ), patch( + "homeassistant.components.airvisual_pro.NodeSamba", + return_value=node_samba, + ), patch( + "homeassistant.components.airvisual_pro.config_flow.NodeSamba", + return_value=node_samba, ): - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() yield -@pytest.fixture(name="unique_id") -def unique_id_fixture(hass): - """Define a config entry unique ID fixture.""" - return "51.528308, -0.3817765" +@pytest.fixture(name="node_samba") +def node_samba_fixture(data_pro): + """Define a mock NodeSamba object.""" + return Mock( + async_connect=AsyncMock(), + async_disconnect=AsyncMock(), + async_get_latest_measurements=AsyncMock(return_value=data_pro), + ) + + +@pytest.fixture(name="setup_config_entry") +async def setup_config_entry_fixture(hass, config_entry, mock_pyairvisual): + """Define a fixture to set up airvisual.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index 7bad9af1002..aa8b16ec194 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -1,5 +1,5 @@ """Define tests for the AirVisual config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from pyairvisual.cloud_api import ( InvalidKeyError, @@ -13,307 +13,101 @@ import pytest from homeassistant import data_entry_flow from homeassistant.components.airvisual import ( CONF_CITY, - CONF_COUNTRY, - CONF_GEOGRAPHIES, CONF_INTEGRATION_TYPE, DOMAIN, INTEGRATION_TYPE_GEOGRAPHY_COORDS, INTEGRATION_TYPE_GEOGRAPHY_NAME, - INTEGRATION_TYPE_NODE_PRO, ) -from homeassistant.components.airvisual_pro import DOMAIN as AIRVISUAL_PRO_DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import ( - CONF_API_KEY, - CONF_IP_ADDRESS, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_PASSWORD, - CONF_SHOW_ON_MAP, - CONF_STATE, +from homeassistant.const import CONF_API_KEY, CONF_SHOW_ON_MAP + +from .conftest import ( + COORDS_CONFIG, + NAME_CONFIG, + TEST_CITY, + TEST_COUNTRY, + TEST_LATITUDE, + TEST_LONGITUDE, + TEST_STATE, ) -from homeassistant.helpers import device_registry as dr, issue_registry as ir - -from tests.common import MockConfigEntry - - -async def test_duplicate_error(hass, config, config_entry, data, setup_airvisual): - """Test that errors are shown when duplicate entries are added.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={"type": INTEGRATION_TYPE_GEOGRAPHY_COORDS}, - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=config - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "already_configured" @pytest.mark.parametrize( - "data,exc,errors,integration_type", + "integration_type,input_form_step,patched_method,config,entry_title", [ ( - { - CONF_API_KEY: "abcde12345", - CONF_CITY: "Beijing", - CONF_STATE: "Beijing", - CONF_COUNTRY: "China", - }, - InvalidKeyError, - {CONF_API_KEY: "invalid_api_key"}, - INTEGRATION_TYPE_GEOGRAPHY_NAME, + INTEGRATION_TYPE_GEOGRAPHY_COORDS, + "geography_by_coords", + "nearest_city", + COORDS_CONFIG, + f"Cloud API ({TEST_LATITUDE}, {TEST_LONGITUDE})", ), ( - { - CONF_API_KEY: "abcde12345", - CONF_CITY: "Beijing", - CONF_STATE: "Beijing", - CONF_COUNTRY: "China", - }, - KeyExpiredError, - {CONF_API_KEY: "invalid_api_key"}, - INTEGRATION_TYPE_GEOGRAPHY_NAME, - ), - ( - { - CONF_API_KEY: "abcde12345", - CONF_CITY: "Beijing", - CONF_STATE: "Beijing", - CONF_COUNTRY: "China", - }, - UnauthorizedError, - {CONF_API_KEY: "invalid_api_key"}, - INTEGRATION_TYPE_GEOGRAPHY_NAME, - ), - ( - { - CONF_API_KEY: "abcde12345", - CONF_CITY: "Beijing", - CONF_STATE: "Beijing", - CONF_COUNTRY: "China", - }, - NotFoundError, - {CONF_CITY: "location_not_found"}, - INTEGRATION_TYPE_GEOGRAPHY_NAME, - ), - ( - { - CONF_API_KEY: "abcde12345", - CONF_CITY: "Beijing", - CONF_STATE: "Beijing", - CONF_COUNTRY: "China", - }, - AirVisualError, - {"base": "unknown"}, INTEGRATION_TYPE_GEOGRAPHY_NAME, + "geography_by_name", + "city", + NAME_CONFIG, + f"Cloud API ({TEST_CITY}, {TEST_STATE}, {TEST_COUNTRY})", ), ], ) -async def test_errors(hass, data, exc, errors, integration_type): - """Test that an exceptions show an error.""" - with patch("pyairvisual.air_quality.AirQuality.city", side_effect=exc): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={"type": integration_type} - ) +@pytest.mark.parametrize( + "response,errors", + [ + (AsyncMock(side_effect=AirVisualError), {"base": "unknown"}), + (AsyncMock(side_effect=InvalidKeyError), {CONF_API_KEY: "invalid_api_key"}), + (AsyncMock(side_effect=KeyExpiredError), {CONF_API_KEY: "invalid_api_key"}), + (AsyncMock(side_effect=NotFoundError), {CONF_CITY: "location_not_found"}), + (AsyncMock(side_effect=UnauthorizedError), {CONF_API_KEY: "invalid_api_key"}), + ], +) +async def test_create_entry( + hass, + cloud_api, + config, + entry_title, + errors, + input_form_step, + integration_type, + mock_pyairvisual, + patched_method, + response, +): + """Test creating a config entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={"type": integration_type} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == input_form_step + + # Test errors that can arise: + with patch.object(cloud_api.air_quality, patched_method, response): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=data + result["flow_id"], user_input=config ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == errors - -@pytest.mark.parametrize( - "config,config_entry_version,unique_id", - [ - ( - { - CONF_API_KEY: "abcde12345", - CONF_GEOGRAPHIES: [ - {CONF_LATITUDE: 51.528308, CONF_LONGITUDE: -0.3817765}, - { - CONF_CITY: "Beijing", - CONF_STATE: "Beijing", - CONF_COUNTRY: "China", - }, - ], - }, - 1, - "abcde12345", - ) - ], -) -async def test_migration_1_2(hass, config, config_entry, setup_airvisual, unique_id): - """Test migrating from version 1 to 2.""" - config_entries = hass.config_entries.async_entries(DOMAIN) - assert len(config_entries) == 2 - - assert config_entries[0].unique_id == "51.528308, -0.3817765" - assert config_entries[0].title == "Cloud API (51.528308, -0.3817765)" - assert config_entries[0].data == { - CONF_API_KEY: "abcde12345", - CONF_LATITUDE: 51.528308, - CONF_LONGITUDE: -0.3817765, - CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS, - } - - assert config_entries[1].unique_id == "Beijing, Beijing, China" - assert config_entries[1].title == "Cloud API (Beijing, Beijing, China)" - assert config_entries[1].data == { - CONF_API_KEY: "abcde12345", - CONF_CITY: "Beijing", - CONF_STATE: "Beijing", - CONF_COUNTRY: "China", - CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_NAME, - } - - -async def test_migration_2_3(hass, pro): - """Test migrating from version 2 to 3.""" - old_pro_entry = MockConfigEntry( - domain=DOMAIN, - unique_id="192.168.1.100", - data={ - CONF_IP_ADDRESS: "192.168.1.100", - CONF_PASSWORD: "abcde12345", - CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO, - }, - version=2, - ) - old_pro_entry.add_to_hass(hass) - - device_registry = dr.async_get(hass) - device_registry.async_get_or_create( - name="192.168.1.100", - config_entry_id=old_pro_entry.entry_id, - identifiers={(DOMAIN, "ABCDE12345")}, - ) - - with patch( - "homeassistant.components.airvisual.automation.automations_with_device", - return_value=["automation.test_automation"], - ), patch( - "homeassistant.components.airvisual_pro.NodeSamba", return_value=pro - ), patch( - "homeassistant.components.airvisual_pro.config_flow.NodeSamba", return_value=pro - ): - await hass.config_entries.async_setup(old_pro_entry.entry_id) - await hass.async_block_till_done() - - for domain, entry_count in ((DOMAIN, 0), (AIRVISUAL_PRO_DOMAIN, 1)): - entries = hass.config_entries.async_entries(domain) - assert len(entries) == entry_count - - issue_registry = ir.async_get(hass) - assert len(issue_registry.issues) == 1 - - -async def test_options_flow(hass, config_entry): - """Test config flow options.""" - with patch( - "homeassistant.components.airvisual.async_setup_entry", return_value=True - ): - await hass.config_entries.async_setup(config_entry.entry_id) - result = await hass.config_entries.options.async_init(config_entry.entry_id) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "init" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_SHOW_ON_MAP: False} - ) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert config_entry.options == {CONF_SHOW_ON_MAP: False} - - -async def test_step_geography_by_coords(hass, config, setup_airvisual): - """Test setting up a geography entry by latitude/longitude.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={"type": INTEGRATION_TYPE_GEOGRAPHY_COORDS}, - ) + # Test that we can recover and finish the flow after errors occur: result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=config ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "Cloud API (51.528308, -0.3817765)" - assert result["data"] == { - CONF_API_KEY: "abcde12345", - CONF_LATITUDE: 51.528308, - CONF_LONGITUDE: -0.3817765, - CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS, - } + assert result["title"] == entry_title + assert result["data"] == {**config, CONF_INTEGRATION_TYPE: integration_type} -@pytest.mark.parametrize( - "config", - [ - { - CONF_API_KEY: "abcde12345", - CONF_CITY: "Beijing", - CONF_STATE: "Beijing", - CONF_COUNTRY: "China", - } - ], -) -async def test_step_geography_by_name(hass, config, setup_airvisual): - """Test setting up a geography entry by city/state/country.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={"type": INTEGRATION_TYPE_GEOGRAPHY_NAME}, - ) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=config - ) - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "Cloud API (Beijing, Beijing, China)" - assert result["data"] == { - CONF_API_KEY: "abcde12345", - CONF_CITY: "Beijing", - CONF_STATE: "Beijing", - CONF_COUNTRY: "China", - CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_NAME, - } - - -async def test_step_reauth(hass, config_entry, setup_airvisual): - """Test that the reauth step works.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=config_entry.data - ) - assert result["step_id"] == "reauth_confirm" - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" - - new_api_key = "defgh67890" - - with patch( - "homeassistant.components.airvisual.async_setup_entry", return_value=True - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_API_KEY: new_api_key} - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "reauth_successful" - - assert len(hass.config_entries.async_entries()) == 1 - assert hass.config_entries.async_entries()[0].data[CONF_API_KEY] == new_api_key - - -async def test_step_user(hass): - """Test the user ("pick the integration type") step.""" +async def test_duplicate_error(hass, config, setup_config_entry): + """Test that errors are shown when duplicate entries are added.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" @@ -322,15 +116,48 @@ async def test_step_user(hass): context={"source": SOURCE_USER}, data={"type": INTEGRATION_TYPE_GEOGRAPHY_COORDS}, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "geography_by_coords" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={"type": INTEGRATION_TYPE_GEOGRAPHY_NAME}, + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + +async def test_options_flow(hass, config_entry, setup_config_entry): + """Test config flow options.""" + result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "geography_by_name" + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_SHOW_ON_MAP: False} + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert config_entry.options == {CONF_SHOW_ON_MAP: False} + + +async def test_step_reauth(hass, config_entry, setup_config_entry): + """Test that the reauth step works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_REAUTH}, data=config_entry.data + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + new_api_key = "defgh67890" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_API_KEY: new_api_key} + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + assert len(hass.config_entries.async_entries()) == 1 + assert hass.config_entries.async_entries()[0].data[CONF_API_KEY] == new_api_key diff --git a/tests/components/airvisual/test_diagnostics.py b/tests/components/airvisual/test_diagnostics.py index 37a8437dcdf..c76bfd8db92 100644 --- a/tests/components/airvisual/test_diagnostics.py +++ b/tests/components/airvisual/test_diagnostics.py @@ -4,7 +4,7 @@ from homeassistant.components.diagnostics import REDACTED from tests.components.diagnostics import get_diagnostics_for_config_entry -async def test_entry_diagnostics(hass, config_entry, hass_client, setup_airvisual): +async def test_entry_diagnostics(hass, config_entry, hass_client, setup_config_entry): """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { diff --git a/tests/components/airvisual/test_init.py b/tests/components/airvisual/test_init.py new file mode 100644 index 00000000000..b9459f5608b --- /dev/null +++ b/tests/components/airvisual/test_init.py @@ -0,0 +1,135 @@ +"""Define tests for AirVisual init.""" +from unittest.mock import patch + +from homeassistant.components.airvisual import ( + CONF_CITY, + CONF_GEOGRAPHIES, + CONF_INTEGRATION_TYPE, + DOMAIN, + INTEGRATION_TYPE_GEOGRAPHY_COORDS, + INTEGRATION_TYPE_GEOGRAPHY_NAME, + INTEGRATION_TYPE_NODE_PRO, +) +from homeassistant.components.airvisual_pro import DOMAIN as AIRVISUAL_PRO_DOMAIN +from homeassistant.const import ( + CONF_API_KEY, + CONF_COUNTRY, + CONF_IP_ADDRESS, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_PASSWORD, + CONF_STATE, +) +from homeassistant.helpers import device_registry as dr, issue_registry as ir + +from .conftest import ( + COORDS_CONFIG, + COORDS_CONFIG2, + NAME_CONFIG, + TEST_API_KEY, + TEST_CITY, + TEST_COUNTRY, + TEST_LATITUDE, + TEST_LATITUDE2, + TEST_LONGITUDE, + TEST_LONGITUDE2, + TEST_STATE, +) + +from tests.common import MockConfigEntry + + +async def test_migration_1_2(hass, mock_pyairvisual): + """Test migrating from version 1 to 2.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_API_KEY, + data={ + CONF_API_KEY: TEST_API_KEY, + CONF_GEOGRAPHIES: [ + { + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, + }, + { + CONF_CITY: TEST_CITY, + CONF_STATE: TEST_STATE, + CONF_COUNTRY: TEST_COUNTRY, + }, + { + CONF_LATITUDE: TEST_LATITUDE2, + CONF_LONGITUDE: TEST_LONGITUDE2, + }, + ], + }, + version=1, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + config_entries = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries) == 3 + + # Ensure that after migration, each configuration has its own config entry: + identifier1 = f"{TEST_LATITUDE}, {TEST_LONGITUDE}" + assert config_entries[0].unique_id == identifier1 + assert config_entries[0].title == f"Cloud API ({identifier1})" + assert config_entries[0].data == { + **COORDS_CONFIG, + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS, + } + + identifier2 = f"{TEST_CITY}, {TEST_STATE}, {TEST_COUNTRY}" + assert config_entries[1].unique_id == identifier2 + assert config_entries[1].title == f"Cloud API ({identifier2})" + assert config_entries[1].data == { + **NAME_CONFIG, + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_NAME, + } + + identifier3 = f"{TEST_LATITUDE2}, {TEST_LONGITUDE2}" + assert config_entries[2].unique_id == identifier3 + assert config_entries[2].title == f"Cloud API ({identifier3})" + assert config_entries[2].data == { + **COORDS_CONFIG2, + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_GEOGRAPHY_COORDS, + } + + +async def test_migration_2_3(hass, mock_pyairvisual): + """Test migrating from version 2 to 3.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="192.168.1.100", + data={ + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PASSWORD: "abcde12345", + CONF_INTEGRATION_TYPE: INTEGRATION_TYPE_NODE_PRO, + }, + version=2, + ) + entry.add_to_hass(hass) + + device_registry = dr.async_get(hass) + device_registry.async_get_or_create( + name="192.168.1.100", + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, "SERIAL_NUMBER")}, + ) + + with patch( + "homeassistant.components.airvisual.automation.automations_with_device", + return_value=["automation.test_automation"], + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Ensure that after migration, the AirVisual Pro device has been moved to the + # `airvisual_pro` domain and an issue has been created: + for domain, entry_count in ((DOMAIN, 0), (AIRVISUAL_PRO_DOMAIN, 1)): + assert len(hass.config_entries.async_entries(domain)) == entry_count + + issue_registry = ir.async_get(hass) + assert len(issue_registry.issues) == 1 diff --git a/tests/components/airzone/test_select.py b/tests/components/airzone/test_select.py new file mode 100644 index 00000000000..545a45508de --- /dev/null +++ b/tests/components/airzone/test_select.py @@ -0,0 +1,177 @@ +"""The select tests for the Airzone platform.""" + +from unittest.mock import patch + +from aioairzone.const import ( + API_COLD_ANGLE, + API_DATA, + API_HEAT_ANGLE, + API_SLEEP, + API_SYSTEM_ID, + API_ZONE_ID, +) +import pytest + +from homeassistant.components.select import DOMAIN as SELECT_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, ATTR_OPTION, SERVICE_SELECT_OPTION +from homeassistant.core import HomeAssistant + +from .util import async_init_integration + + +async def test_airzone_create_selects(hass: HomeAssistant) -> None: + """Test creation of selects.""" + + await async_init_integration(hass) + + state = hass.states.get("select.despacho_cold_angle") + assert state.state == "90º" + + state = hass.states.get("select.despacho_heat_angle") + assert state.state == "90º" + + state = hass.states.get("select.despacho_sleep") + assert state.state == "Off" + + state = hass.states.get("select.dorm_1_cold_angle") + assert state.state == "90º" + + state = hass.states.get("select.dorm_1_heat_angle") + assert state.state == "90º" + + state = hass.states.get("select.dorm_1_sleep") + assert state.state == "Off" + + state = hass.states.get("select.dorm_2_cold_angle") + assert state.state == "90º" + + state = hass.states.get("select.dorm_2_heat_angle") + assert state.state == "90º" + + state = hass.states.get("select.dorm_2_sleep") + assert state.state == "Off" + + state = hass.states.get("select.dorm_ppal_cold_angle") + assert state.state == "45º" + + state = hass.states.get("select.dorm_ppal_heat_angle") + assert state.state == "50º" + + state = hass.states.get("select.dorm_ppal_sleep") + assert state.state == "30m" + + state = hass.states.get("select.salon_cold_angle") + assert state.state == "90º" + + state = hass.states.get("select.salon_heat_angle") + assert state.state == "90º" + + state = hass.states.get("select.salon_sleep") + assert state.state == "Off" + + +async def test_airzone_select_sleep(hass: HomeAssistant) -> None: + """Test select sleep.""" + + await async_init_integration(hass) + + put_hvac_sleep = { + API_DATA: [ + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 3, + API_SLEEP: 30, + } + ] + } + + with pytest.raises(ValueError): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.dorm_1_sleep", + ATTR_OPTION: "Invalid", + }, + blocking=True, + ) + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + return_value=put_hvac_sleep, + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.dorm_1_sleep", + ATTR_OPTION: "30m", + }, + blocking=True, + ) + + state = hass.states.get("select.dorm_1_sleep") + assert state.state == "30m" + + +async def test_airzone_select_grille_angle(hass: HomeAssistant) -> None: + """Test select sleep.""" + + await async_init_integration(hass) + + # Cold Angle + + put_hvac_cold_angle = { + API_DATA: [ + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 3, + API_COLD_ANGLE: 1, + } + ] + } + + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + return_value=put_hvac_cold_angle, + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.dorm_1_cold_angle", + ATTR_OPTION: "50º", + }, + blocking=True, + ) + + state = hass.states.get("select.dorm_1_cold_angle") + assert state.state == "50º" + + # Heat Angle + + put_hvac_heat_angle = { + API_DATA: [ + { + API_SYSTEM_ID: 1, + API_ZONE_ID: 3, + API_HEAT_ANGLE: 2, + } + ] + } + with patch( + "homeassistant.components.airzone.AirzoneLocalApi.put_hvac", + return_value=put_hvac_heat_angle, + ): + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.dorm_1_heat_angle", + ATTR_OPTION: "45º", + }, + blocking=True, + ) + + state = hass.states.get("select.dorm_1_heat_angle") + assert state.state == "45º" diff --git a/tests/components/airzone/test_sensor.py b/tests/components/airzone/test_sensor.py index 248dc020732..151ee7c42fb 100644 --- a/tests/components/airzone/test_sensor.py +++ b/tests/components/airzone/test_sensor.py @@ -20,7 +20,7 @@ async def test_airzone_create_sensors( # Zones state = hass.states.get("sensor.despacho_temperature") - assert state.state == "21.2" + assert state.state == "21.20" state = hass.states.get("sensor.despacho_humidity") assert state.state == "36" diff --git a/tests/components/airzone/util.py b/tests/components/airzone/util.py index a29b035648b..6277c077c00 100644 --- a/tests/components/airzone/util.py +++ b/tests/components/airzone/util.py @@ -4,11 +4,13 @@ from unittest.mock import patch from aioairzone.const import ( API_AIR_DEMAND, + API_COLD_ANGLE, API_COLD_STAGE, API_COLD_STAGES, API_DATA, API_ERRORS, API_FLOOR_DEMAND, + API_HEAT_ANGLE, API_HEAT_STAGE, API_HEAT_STAGES, API_HUMIDITY, @@ -22,6 +24,7 @@ from aioairzone.const import ( API_POWER, API_ROOM_TEMP, API_SET_POINT, + API_SLEEP, API_SYSTEM_FIRMWARE, API_SYSTEM_ID, API_SYSTEM_TYPE, @@ -68,6 +71,7 @@ HVAC_MOCK = { API_MIN_TEMP: 15, API_SET_POINT: 19.1, API_ROOM_TEMP: 19.6, + API_SLEEP: 0, API_MODES: [1, 4, 2, 3, 5], API_MODE: 3, API_COLD_STAGES: 1, @@ -79,6 +83,8 @@ HVAC_MOCK = { API_ERRORS: [], API_AIR_DEMAND: 0, API_FLOOR_DEMAND: 0, + API_HEAT_ANGLE: 0, + API_COLD_ANGLE: 0, }, { API_SYSTEM_ID: 1, @@ -92,6 +98,7 @@ HVAC_MOCK = { API_MIN_TEMP: 15, API_SET_POINT: 19.2, API_ROOM_TEMP: 21.1, + API_SLEEP: 30, API_MODE: 3, API_COLD_STAGES: 1, API_COLD_STAGE: 1, @@ -102,6 +109,8 @@ HVAC_MOCK = { API_ERRORS: [], API_AIR_DEMAND: 1, API_FLOOR_DEMAND: 1, + API_HEAT_ANGLE: 1, + API_COLD_ANGLE: 2, }, { API_SYSTEM_ID: 1, @@ -115,6 +124,7 @@ HVAC_MOCK = { API_MIN_TEMP: 15, API_SET_POINT: 19.3, API_ROOM_TEMP: 20.8, + API_SLEEP: 0, API_MODE: 3, API_COLD_STAGES: 1, API_COLD_STAGE: 1, @@ -125,6 +135,8 @@ HVAC_MOCK = { API_ERRORS: [], API_AIR_DEMAND: 0, API_FLOOR_DEMAND: 0, + API_HEAT_ANGLE: 0, + API_COLD_ANGLE: 0, }, { API_SYSTEM_ID: 1, @@ -138,6 +150,7 @@ HVAC_MOCK = { API_MIN_TEMP: 59, API_SET_POINT: 66.92, API_ROOM_TEMP: 70.16, + API_SLEEP: 0, API_MODE: 3, API_COLD_STAGES: 1, API_COLD_STAGE: 1, @@ -152,6 +165,8 @@ HVAC_MOCK = { ], API_AIR_DEMAND: 0, API_FLOOR_DEMAND: 0, + API_HEAT_ANGLE: 0, + API_COLD_ANGLE: 0, }, { API_SYSTEM_ID: 1, @@ -165,6 +180,7 @@ HVAC_MOCK = { API_MIN_TEMP: 15, API_SET_POINT: 19.5, API_ROOM_TEMP: 20.5, + API_SLEEP: 0, API_MODE: 3, API_COLD_STAGES: 1, API_COLD_STAGE: 1, @@ -175,6 +191,8 @@ HVAC_MOCK = { API_ERRORS: [], API_AIR_DEMAND: 0, API_FLOOR_DEMAND: 0, + API_HEAT_ANGLE: 0, + API_COLD_ANGLE: 0, }, ] }, diff --git a/tests/components/aladdin_connect/test_cover.py b/tests/components/aladdin_connect/test_cover.py index 4e65607fa9d..2192017c26f 100644 --- a/tests/components/aladdin_connect/test_cover.py +++ b/tests/components/aladdin_connect/test_cover.py @@ -231,37 +231,3 @@ async def test_yaml_import( config_data = hass.config_entries.async_entries(DOMAIN)[0].data assert config_data[CONF_USERNAME] == "test-user" assert config_data[CONF_PASSWORD] == "test-password" - - -async def test_callback( - hass: HomeAssistant, - mock_aladdinconnect_api: MagicMock, -): - """Test callback from Aladdin Connect API.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - data=YAML_CONFIG, - unique_id="test-id", - ) - config_entry.add_to_hass(hass) - await hass.async_block_till_done() - - with patch( - "homeassistant.components.aladdin_connect.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - mock_aladdinconnect_api.async_get_door_status.return_value = STATE_CLOSING - mock_aladdinconnect_api.get_door_status.return_value = STATE_CLOSING - with patch( - "homeassistant.components.aladdin_connect.AladdinConnectClient", - return_value=mock_aladdinconnect_api, - ), patch( - "homeassistant.components.aladdin_connect.AladdinConnectClient._call_back", - AsyncMock(), - ): - callback = mock_aladdinconnect_api.register_callback.call_args[0][0] - await callback() - assert hass.states.get("cover.home").state == STATE_CLOSING diff --git a/tests/components/alarm_control_panel/test_device_condition.py b/tests/components/alarm_control_panel/test_device_condition.py index bcb94a63bc9..9c0f6077854 100644 --- a/tests/components/alarm_control_panel/test_device_condition.py +++ b/tests/components/alarm_control_panel/test_device_condition.py @@ -212,7 +212,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_triggered - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_triggered " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -230,7 +234,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_disarmed - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_disarmed " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -248,7 +256,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_armed_home - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_armed_home " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -266,7 +278,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_armed_away - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_armed_away " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -284,7 +300,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_armed_night - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_armed_night " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -302,7 +322,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_armed_vacation - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_armed_vacation " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -320,7 +344,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_armed_custom_bypass - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_armed_custom_bypass " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py index ca9c5a7ea69..a76c75e814d 100644 --- a/tests/components/alarm_control_panel/test_device_trigger.py +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -232,9 +232,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "triggered - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "triggered " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -251,9 +254,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "disarmed - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "disarmed " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -270,9 +276,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "armed_home - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "armed_home " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -289,9 +298,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "armed_away - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "armed_away " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -308,9 +320,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "armed_night - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "armed_night " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -327,9 +342,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "armed_vacation - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "armed_vacation " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -344,7 +362,8 @@ async def test_if_fires_on_state_change(hass, calls): assert len(calls) == 1 assert ( calls[0].data["some"] - == "triggered - device - alarm_control_panel.entity - pending - triggered - None" + == "triggered - device - alarm_control_panel.entity - pending - triggered -" + " None" ) # Fake that the entity is disarmed. @@ -353,7 +372,8 @@ async def test_if_fires_on_state_change(hass, calls): assert len(calls) == 2 assert ( calls[1].data["some"] - == "disarmed - device - alarm_control_panel.entity - triggered - disarmed - None" + == "disarmed - device - alarm_control_panel.entity - triggered - disarmed -" + " None" ) # Fake that the entity is armed home. @@ -362,7 +382,8 @@ async def test_if_fires_on_state_change(hass, calls): assert len(calls) == 3 assert ( calls[2].data["some"] - == "armed_home - device - alarm_control_panel.entity - disarmed - armed_home - None" + == "armed_home - device - alarm_control_panel.entity - disarmed - armed_home -" + " None" ) # Fake that the entity is armed away. @@ -371,7 +392,8 @@ async def test_if_fires_on_state_change(hass, calls): assert len(calls) == 4 assert ( calls[3].data["some"] - == "armed_away - device - alarm_control_panel.entity - armed_home - armed_away - None" + == "armed_away - device - alarm_control_panel.entity - armed_home - armed_away" + " - None" ) # Fake that the entity is armed night. @@ -380,7 +402,8 @@ async def test_if_fires_on_state_change(hass, calls): assert len(calls) == 5 assert ( calls[4].data["some"] - == "armed_night - device - alarm_control_panel.entity - armed_away - armed_night - None" + == "armed_night - device - alarm_control_panel.entity - armed_away -" + " armed_night - None" ) # Fake that the entity is armed vacation. @@ -389,7 +412,8 @@ async def test_if_fires_on_state_change(hass, calls): assert len(calls) == 6 assert ( calls[5].data["some"] - == "armed_vacation - device - alarm_control_panel.entity - armed_night - armed_vacation - None" + == "armed_vacation - device - alarm_control_panel.entity - armed_night -" + " armed_vacation - None" ) diff --git a/tests/components/alert/test_init.py b/tests/components/alert/test_init.py index f1543892b6b..d0ffe49b0f6 100644 --- a/tests/components/alert/test_init.py +++ b/tests/components/alert/test_init.py @@ -1,5 +1,5 @@ """The tests for the Alert component.""" -# pylint: disable=protected-access + from copy import deepcopy import pytest diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 13d095f1cc6..6b0ed360517 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -18,7 +18,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_UNLOCKED, - TEMP_CELSIUS, + UnitOfTemperature, ) from .test_common import ( @@ -412,7 +412,7 @@ async def test_report_fan_speed_state(hass): async def test_report_humidifier_humidity_state(hass): - """Test PercentageController, PowerLevelController reports humidifier humidity correctly.""" + """Test PercentageController, PowerLevelController humidifier humidity reporting.""" hass.states.async_set( "humidifier.dry", "on", @@ -639,7 +639,7 @@ async def test_report_climate_state(hass): "friendly_name": "Climate Downstairs", "supported_features": 91, ATTR_CURRENT_TEMPERATURE: 34, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ) properties = await reported_properties(hass, "climate.downstairs") @@ -658,7 +658,7 @@ async def test_report_climate_state(hass): "friendly_name": "Climate Downstairs", "supported_features": 91, ATTR_CURRENT_TEMPERATURE: 34, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ) properties = await reported_properties(hass, "climate.downstairs") @@ -677,7 +677,7 @@ async def test_report_climate_state(hass): "friendly_name": "Climate Downstairs", "supported_features": 91, ATTR_CURRENT_TEMPERATURE: 34, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ) properties = await reported_properties(hass, "climate.downstairs") @@ -694,7 +694,7 @@ async def test_report_climate_state(hass): "friendly_name": "Climate Downstairs", "supported_features": 91, ATTR_CURRENT_TEMPERATURE: 31, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ) properties = await reported_properties(hass, "climate.downstairs") @@ -710,7 +710,7 @@ async def test_report_climate_state(hass): "friendly_name": "Climate Heat", "supported_features": 91, ATTR_CURRENT_TEMPERATURE: 34, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ) properties = await reported_properties(hass, "climate.heat") @@ -726,7 +726,7 @@ async def test_report_climate_state(hass): "friendly_name": "Climate Cool", "supported_features": 91, ATTR_CURRENT_TEMPERATURE: 34, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ) properties = await reported_properties(hass, "climate.cool") @@ -753,7 +753,7 @@ async def test_report_climate_state(hass): "friendly_name": "Climate Unsupported", "supported_features": 91, ATTR_CURRENT_TEMPERATURE: 34, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ) msg = await reported_properties(hass, "climate.unsupported", True) @@ -767,14 +767,16 @@ async def test_temperature_sensor_sensor(hass): hass.states.async_set( "sensor.temp_living_room", bad_value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) properties = await reported_properties(hass, "sensor.temp_living_room") properties.assert_not_has_property("Alexa.TemperatureSensor", "temperature") hass.states.async_set( - "sensor.temp_living_room", "34", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "sensor.temp_living_room", + "34", + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) properties = await reported_properties(hass, "sensor.temp_living_room") properties.assert_equal( @@ -932,7 +934,10 @@ async def test_report_image_processing(hass): @pytest.mark.parametrize("domain", ["button", "input_button"]) async def test_report_button_pressed(hass, domain): - """Test button presses report human presence detection events to trigger routines.""" + """Test button presses report human presence detection events. + + For use to trigger routines. + """ hass.states.async_set( f"{domain}.test_button", "now", {"friendly_name": "Test button"} ) @@ -990,7 +995,7 @@ async def test_get_property_blowup(hass, caplog): "friendly_name": "Climate Downstairs", "supported_features": 91, ATTR_CURRENT_TEMPERATURE: 34, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ) with patch( diff --git a/tests/components/alexa/test_entities.py b/tests/components/alexa/test_entities.py index fb364dbf14e..e26a60cab39 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -114,6 +114,6 @@ async def test_serialize_discovery_recovers(hass, caplog): assert "Alexa.PowerController" not in interfaces assert ( - f"Error serializing Alexa.PowerController discovery for {hass.states.get('switch.bla')}" - in caplog.text - ) + "Error serializing Alexa.PowerController discovery" + f" for {hass.states.get('switch.bla')}" + ) in caplog.text diff --git a/tests/components/alexa/test_flash_briefings.py b/tests/components/alexa/test_flash_briefings.py index 499848f01db..62dce4fd13a 100644 --- a/tests/components/alexa/test_flash_briefings.py +++ b/tests/components/alexa/test_flash_briefings.py @@ -1,5 +1,5 @@ """The tests for the Alexa component.""" -# pylint: disable=protected-access + import datetime from http import HTTPStatus diff --git a/tests/components/alexa/test_intent.py b/tests/components/alexa/test_intent.py index 54708e9d0f0..eb8896cb71d 100644 --- a/tests/components/alexa/test_intent.py +++ b/tests/components/alexa/test_intent.py @@ -1,5 +1,5 @@ """The tests for the Alexa component.""" -# pylint: disable=protected-access + from http import HTTPStatus import json @@ -193,7 +193,9 @@ async def test_intent_launch_request_not_configured(alexa_client): "new": True, "sessionId": SESSION_ID, "application": { - "applicationId": "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00000" + "applicationId": ( + "amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00000" + ), }, "attributes": {}, "user": {"userId": "amzn1.account.AM3B00000000000000000000000"}, diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 3b76654f312..a84d7342490 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -26,7 +26,7 @@ from homeassistant.components.media_player import ( ) import homeassistant.components.vacuum as vacuum from homeassistant.config import async_process_ha_core_config -from homeassistant.const import STATE_UNKNOWN, TEMP_FAHRENHEIT +from homeassistant.const import STATE_UNKNOWN, UnitOfTemperature from homeassistant.core import Context from homeassistant.helpers import entityfilter from homeassistant.setup import async_setup_component @@ -410,7 +410,8 @@ async def test_fan(hass): assert appliance["endpointId"] == "fan#test_1" assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 1" - # Alexa.RangeController is added to make a fan controllable when no other controllers are available + # Alexa.RangeController is added to make a fan controllable when + # no other controllers are available. capabilities = assert_endpoint_capabilities( appliance, "Alexa.RangeController", @@ -466,7 +467,8 @@ async def test_fan2(hass): assert appliance["endpointId"] == "fan#test_2" assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 2" - # Alexa.RangeController is added to make a fan controllable when no other controllers are available + # Alexa.RangeController is added to make a fan controllable + # when no other controllers are available capabilities = assert_endpoint_capabilities( appliance, "Alexa.RangeController", @@ -597,7 +599,8 @@ async def test_variable_fan_no_current_speed(hass, caplog): assert appliance["endpointId"] == "fan#test_3" assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 3" - # Alexa.RangeController is added to make a van controllable when no other controllers are available + # Alexa.RangeController is added to make a van controllable + # when no other controllers are available capabilities = assert_endpoint_capabilities( appliance, "Alexa.RangeController", @@ -625,9 +628,9 @@ async def test_variable_fan_no_current_speed(hass, caplog): "fan.percentage", ) assert ( - "Request Alexa.RangeController/AdjustRangeValue error INVALID_VALUE: Unable to determine fan.test_3 current fan speed" - in caplog.text - ) + "Request Alexa.RangeController/AdjustRangeValue error " + "INVALID_VALUE: Unable to determine fan.test_3 current fan speed" + ) in caplog.text caplog.clear() @@ -1987,7 +1990,10 @@ async def test_temp_sensor(hass): device = ( "sensor.test_temp", "42", - {"friendly_name": "Test Temp Sensor", "unit_of_measurement": TEMP_FAHRENHEIT}, + { + "friendly_name": "Test Temp Sensor", + "unit_of_measurement": UnitOfTemperature.FAHRENHEIT, + }, ) appliance = await discovery_test(device, hass) @@ -3338,10 +3344,11 @@ async def test_cover_semantics_position_and_tilt(hass): } in tilt_state_mappings -async def test_input_number(hass): - """Test input_number discovery.""" +@pytest.mark.parametrize("domain", ["input_number", "number"]) +async def test_input_number(hass, domain: str): + """Test input_number and number discovery.""" device = ( - "input_number.test_slider", + f"{domain}.test_slider", 30, { "initial": 30, @@ -3354,7 +3361,7 @@ async def test_input_number(hass): ) appliance = await discovery_test(device, hass) - assert appliance["endpointId"] == "input_number#test_slider" + assert appliance["endpointId"] == f"{domain}#test_slider" assert appliance["displayCategories"][0] == "OTHER" assert appliance["friendlyName"] == "Test Slider" @@ -3363,7 +3370,7 @@ async def test_input_number(hass): ) range_capability = get_capability( - capabilities, "Alexa.RangeController", "input_number.value" + capabilities, "Alexa.RangeController", f"{domain}.value" ) capability_resources = range_capability["capabilityResources"] @@ -3403,11 +3410,11 @@ async def test_input_number(hass): call, _ = await assert_request_calls_service( "Alexa.RangeController", "SetRangeValue", - "input_number#test_slider", - "input_number.set_value", + f"{domain}#test_slider", + f"{domain}.set_value", hass, payload={"rangeValue": 10}, - instance="input_number.value", + instance=f"{domain}.value", ) assert call.data["value"] == 10 @@ -3416,17 +3423,18 @@ async def test_input_number(hass): [(25, -5, False), (35, 5, False), (-20, -100, False), (35, 100, False)], "Alexa.RangeController", "AdjustRangeValue", - "input_number#test_slider", - "input_number.set_value", + f"{domain}#test_slider", + f"{domain}.set_value", "value", - instance="input_number.value", + instance=f"{domain}.value", ) -async def test_input_number_float(hass): - """Test input_number discovery.""" +@pytest.mark.parametrize("domain", ["input_number", "number"]) +async def test_input_number_float(hass, domain: str): + """Test input_number and number discovery.""" device = ( - "input_number.test_slider_float", + f"{domain}.test_slider_float", 0.5, { "initial": 0.5, @@ -3439,7 +3447,7 @@ async def test_input_number_float(hass): ) appliance = await discovery_test(device, hass) - assert appliance["endpointId"] == "input_number#test_slider_float" + assert appliance["endpointId"] == f"{domain}#test_slider_float" assert appliance["displayCategories"][0] == "OTHER" assert appliance["friendlyName"] == "Test Slider Float" @@ -3448,7 +3456,7 @@ async def test_input_number_float(hass): ) range_capability = get_capability( - capabilities, "Alexa.RangeController", "input_number.value" + capabilities, "Alexa.RangeController", f"{domain}.value" ) capability_resources = range_capability["capabilityResources"] @@ -3488,11 +3496,11 @@ async def test_input_number_float(hass): call, _ = await assert_request_calls_service( "Alexa.RangeController", "SetRangeValue", - "input_number#test_slider_float", - "input_number.set_value", + f"{domain}#test_slider_float", + f"{domain}.set_value", hass, payload={"rangeValue": 0.333}, - instance="input_number.value", + instance=f"{domain}.value", ) assert call.data["value"] == 0.333 @@ -3507,10 +3515,10 @@ async def test_input_number_float(hass): ], "Alexa.RangeController", "AdjustRangeValue", - "input_number#test_slider_float", - "input_number.set_value", + f"{domain}#test_slider_float", + f"{domain}.set_value", "value", - instance="input_number.value", + instance=f"{domain}.value", ) diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index ed70afc02d6..4cb1e073d5a 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -7,6 +7,8 @@ import pytest from homeassistant import core from homeassistant.components.alexa import errors, state_report +from homeassistant.components.alexa.resources import AlexaGlobalCatalog +from homeassistant.const import PERCENTAGE, UnitOfLength, UnitOfTemperature from .test_common import TEST_URL, get_default_config @@ -333,6 +335,96 @@ async def test_report_state_humidifier(hass, aioclient_mock): assert call_json["event"]["endpoint"]["endpointId"] == "humidifier#test_humidifier" +@pytest.mark.parametrize( + "domain,value,unit,label", + [ + ( + "number", + 50, + None, + AlexaGlobalCatalog.SETTING_PRESET, + ), + ( + "input_number", + 40, + UnitOfLength.METERS, + AlexaGlobalCatalog.UNIT_DISTANCE_METERS, + ), + ( + "number", + 20.5, + UnitOfTemperature.CELSIUS, + AlexaGlobalCatalog.UNIT_TEMPERATURE_CELSIUS, + ), + ( + "input_number", + 40.5, + UnitOfLength.MILLIMETERS, + AlexaGlobalCatalog.SETTING_PRESET, + ), + ( + "number", + 20.5, + PERCENTAGE, + AlexaGlobalCatalog.UNIT_PERCENT, + ), + ], +) +async def test_report_state_number(hass, aioclient_mock, domain, value, unit, label): + """Test proactive state reports with number or input_number instance.""" + aioclient_mock.post(TEST_URL, text="", status=202) + state = { + "friendly_name": f"Test {domain}", + "min": 10, + "max": 100, + "step": 0.1, + } + + if unit: + state["unit_of_measurement"]: unit + + hass.states.async_set( + f"{domain}.test_{domain}", + None, + state, + ) + + await state_report.async_enable_proactive_mode(hass, get_default_config(hass)) + + hass.states.async_set( + f"{domain}.test_{domain}", + value, + state, + ) + + # To trigger event listener + await hass.async_block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + call = aioclient_mock.mock_calls + + call_json = call[0][2] + assert call_json["event"]["header"]["namespace"] == "Alexa" + assert call_json["event"]["header"]["name"] == "ChangeReport" + + change_reports = call_json["event"]["payload"]["change"]["properties"] + + checks = 0 + for report in change_reports: + if report["name"] == "connectivity": + assert report["value"] == {"value": "OK"} + assert report["namespace"] == "Alexa.EndpointHealth" + checks += 1 + if report["name"] == "rangeValue": + assert report["value"] == value + assert report["instance"] == f"{domain}.value" + assert report["namespace"] == "Alexa.RangeController" + checks += 1 + assert checks == 2 + + assert call_json["event"]["endpoint"]["endpointId"] == f"{domain}#test_{domain}" + + async def test_send_add_or_update_message(hass, aioclient_mock): """Test sending an AddOrUpdateReport message.""" aioclient_mock.post(TEST_URL, text="") @@ -527,8 +619,9 @@ async def test_doorbell_event_fail(hass, aioclient_mock, caplog): # Check we log the entity id of the failing entity assert ( - "Error when sending DoorbellPress event for binary_sensor.test_doorbell to Alexa: " - "THROTTLING_EXCEPTION: Request could not be processed due to throttling" + "Error when sending DoorbellPress event for binary_sensor.test_doorbell" + " to Alexa: THROTTLING_EXCEPTION: Request could not be processed" + " due to throttling" ) in caplog.text diff --git a/tests/components/almond/__init__.py b/tests/components/almond/__init__.py deleted file mode 100644 index 717271c3a6a..00000000000 --- a/tests/components/almond/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Almond integration.""" diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py deleted file mode 100644 index 511a5cf08dc..00000000000 --- a/tests/components/almond/test_config_flow.py +++ /dev/null @@ -1,163 +0,0 @@ -"""Test the Almond config flow.""" -import asyncio -from http import HTTPStatus -from unittest.mock import patch - -from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.almond import config_flow -from homeassistant.components.almond.const import DOMAIN -from homeassistant.components.hassio import HassioServiceInfo -from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET -from homeassistant.helpers import config_entry_oauth2_flow - -from tests.common import MockConfigEntry - -CLIENT_ID_VALUE = "1234" -CLIENT_SECRET_VALUE = "5678" - - -async def test_import(hass): - """Test that we can import a config entry.""" - with patch("pyalmond.WebAlmondAPI.async_list_apps"): - assert await setup.async_setup_component( - hass, - DOMAIN, - {DOMAIN: {"type": "local", "host": "http://localhost:3000"}}, - ) - 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["type"] == "local" - assert entry.data["host"] == "http://localhost:3000" - - -async def test_import_cannot_connect(hass): - """Test that we won't import a config entry if we cannot connect.""" - with patch( - "pyalmond.WebAlmondAPI.async_list_apps", side_effect=asyncio.TimeoutError - ): - assert await setup.async_setup_component( - hass, - DOMAIN, - {DOMAIN: {"type": "local", "host": "http://localhost:3000"}}, - ) - await hass.async_block_till_done() - - assert len(hass.config_entries.async_entries(DOMAIN)) == 0 - - -async def test_hassio(hass): - """Test that Hass.io can discover this integration.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_HASSIO}, - data=HassioServiceInfo( - config={"addon": "Almond add-on", "host": "almond-addon", "port": "1234"}, - name="Almond add-on", - slug="almond", - ), - ) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "hassio_confirm" - - with patch( - "homeassistant.components.almond.async_setup_entry", return_value=True - ) as mock_setup: - result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - - assert len(mock_setup.mock_calls) == 1 - - assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.data["type"] == "local" - assert entry.data["host"] == "http://almond-addon:1234" - - -async def test_abort_if_existing_entry(hass): - """Check flow abort when an entry already exist.""" - MockConfigEntry(domain=DOMAIN).add_to_hass(hass) - - flow = config_flow.AlmondFlowHandler() - flow.hass = hass - - result = await flow.async_step_user() - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "single_instance_allowed" - - result = await flow.async_step_import({}) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "single_instance_allowed" - - result = await flow.async_step_hassio( - HassioServiceInfo(config={}, name="Almond add-on", slug="almond") - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "single_instance_allowed" - - -async def test_full_flow( - hass, hass_client_no_auth, aioclient_mock, current_request_with_host -): - """Check full flow.""" - assert await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - "type": "oauth2", - CONF_CLIENT_ID: CLIENT_ID_VALUE, - CONF_CLIENT_SECRET: CLIENT_SECRET_VALUE, - }, - "http": {"base_url": "https://example.com"}, - }, - ) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - state = config_entry_oauth2_flow._encode_jwt( - hass, - { - "flow_id": result["flow_id"], - "redirect_uri": "https://example.com/auth/external/callback", - }, - ) - - assert result["type"] == data_entry_flow.FlowResultType.EXTERNAL_STEP - assert result["url"] == ( - "https://almond.stanford.edu/me/api/oauth2/authorize" - f"?response_type=code&client_id={CLIENT_ID_VALUE}" - "&redirect_uri=https://example.com/auth/external/callback" - f"&state={state}&scope=profile+user-read+user-read-results+user-exec-command" - ) - - client = await hass_client_no_auth() - resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") - assert resp.status == HTTPStatus.OK - assert resp.headers["content-type"] == "text/html; charset=utf-8" - - aioclient_mock.post( - "https://almond.stanford.edu/me/api/oauth2/token", - json={ - "refresh_token": "mock-refresh-token", - "access_token": "mock-access-token", - "type": "Bearer", - "expires_in": 60, - }, - ) - - with patch( - "homeassistant.components.almond.async_setup_entry", return_value=True - ) as mock_setup: - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - - assert len(mock_setup.mock_calls) == 1 - - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.data["type"] == "oauth2" - assert entry.data["host"] == "https://almond.stanford.edu/me" diff --git a/tests/components/almond/test_init.py b/tests/components/almond/test_init.py deleted file mode 100644 index 64537aa9465..00000000000 --- a/tests/components/almond/test_init.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Tests for Almond set up.""" -from time import time -from unittest.mock import patch - -import pytest - -from homeassistant import config_entries, core -from homeassistant.components.almond import const -from homeassistant.config import async_process_ha_core_config -from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.setup import async_setup_component -from homeassistant.util.dt import utcnow - -from tests.common import MockConfigEntry, async_fire_time_changed - - -@pytest.fixture(autouse=True) -def patch_hass_state(hass): - """Mock the hass.state to be not_running.""" - hass.state = core.CoreState.not_running - - -async def test_set_up_oauth_remote_url(hass, aioclient_mock): - """Test we set up Almond to connect to HA if we have external url.""" - entry = MockConfigEntry( - domain="almond", - data={ - "type": const.TYPE_OAUTH2, - "auth_implementation": "local", - "host": "http://localhost:9999", - "token": {"expires_at": time() + 1000, "access_token": "abcd"}, - }, - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ): - assert await async_setup_component(hass, "almond", {}) - - assert entry.state is config_entries.ConfigEntryState.LOADED - - hass.config.components.add("cloud") - with patch("homeassistant.components.almond.ALMOND_SETUP_DELAY", 0), patch( - "homeassistant.helpers.network.get_url", - return_value="https://example.nabu.casa", - ), patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - async_fire_time_changed(hass, utcnow()) - await hass.async_block_till_done() - - assert len(mock_create_device.mock_calls) == 1 - - -async def test_set_up_oauth_no_external_url(hass, aioclient_mock): - """Test we do not set up Almond to connect to HA if we have no external url.""" - entry = MockConfigEntry( - domain="almond", - data={ - "type": const.TYPE_OAUTH2, - "auth_implementation": "local", - "host": "http://localhost:9999", - "token": {"expires_at": time() + 1000, "access_token": "abcd"}, - }, - ) - entry.add_to_hass(hass) - - with patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: - assert await async_setup_component(hass, "almond", {}) - - assert entry.state is config_entries.ConfigEntryState.LOADED - assert len(mock_create_device.mock_calls) == 0 - - -async def test_set_up_hassio(hass, aioclient_mock): - """Test we do not set up Almond to connect to HA if we use Hass.io.""" - entry = MockConfigEntry( - domain="almond", - data={ - "is_hassio": True, - "type": const.TYPE_LOCAL, - "host": "http://localhost:9999", - }, - ) - entry.add_to_hass(hass) - - with patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: - assert await async_setup_component(hass, "almond", {}) - - assert entry.state is config_entries.ConfigEntryState.LOADED - assert len(mock_create_device.mock_calls) == 0 - - -async def test_set_up_local(hass, aioclient_mock): - """Test we do not set up Almond to connect to HA if we use local.""" - - # Set up an internal URL, as Almond won't be set up if there is no URL available - await async_process_ha_core_config( - hass, - {"internal_url": "https://192.168.0.1"}, - ) - - entry = MockConfigEntry( - domain="almond", - data={"type": const.TYPE_LOCAL, "host": "http://localhost:9999"}, - ) - entry.add_to_hass(hass) - - with patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: - assert await async_setup_component(hass, "almond", {}) - - assert entry.state is config_entries.ConfigEntryState.LOADED - assert len(mock_create_device.mock_calls) == 1 diff --git a/tests/components/ambient_station/conftest.py b/tests/components/ambient_station/conftest.py index 89dc4e88fb3..594458436e7 100644 --- a/tests/components/ambient_station/conftest.py +++ b/tests/components/ambient_station/conftest.py @@ -1,16 +1,21 @@ """Define test fixtures for Ambient PWS.""" import json -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant.components.ambient_station.const import CONF_APP_KEY, DOMAIN from homeassistant.const import CONF_API_KEY -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture +@pytest.fixture(name="api") +def api_fixture(hass, data_devices): + """Define a mock API object.""" + return Mock(get_devices=AsyncMock(return_value=data_devices)) + + @pytest.fixture(name="config") def config_fixture(hass): """Define a config entry data fixture.""" @@ -28,27 +33,31 @@ def config_entry_fixture(hass, config): return entry -@pytest.fixture(name="devices", scope="package") -def devices_fixture(): +@pytest.fixture(name="data_devices", scope="package") +def data_devices_fixture(): """Define devices data.""" return json.loads(load_fixture("devices.json", "ambient_station")) -@pytest.fixture(name="setup_ambient_station") -async def setup_ambient_station_fixture(hass, config, devices): - """Define a fixture to set up AirVisual.""" - with patch("homeassistant.components.ambient_station.PLATFORMS", []), patch( - "homeassistant.components.ambient_station.config_flow.API.get_devices", - side_effect=devices, - ), patch("aioambient.api.API.get_devices", side_effect=devices), patch( - "aioambient.websocket.Websocket.connect" - ): - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() +@pytest.fixture(name="data_station", scope="package") +def data_station_fixture(): + """Define station data.""" + return json.loads(load_fixture("station_data.json", "ambient_station")) + + +@pytest.fixture(name="mock_aioambient") +async def mock_aioambient_fixture(api): + """Define a fixture to patch aioambient.""" + with patch( + "homeassistant.components.ambient_station.config_flow.API", + return_value=api, + ), patch("aioambient.websocket.Websocket.connect"): yield -@pytest.fixture(name="station_data", scope="package") -def station_data_fixture(): - """Define devices data.""" - return json.loads(load_fixture("station_data.json", "ambient_station")) +@pytest.fixture(name="setup_config_entry") +async def setup_config_entry_fixture(hass, config_entry, mock_aioambient): + """Define a fixture to set up ambient_station.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield diff --git a/tests/components/ambient_station/test_config_flow.py b/tests/components/ambient_station/test_config_flow.py index 0e298c40c0e..876acb25126 100644 --- a/tests/components/ambient_station/test_config_flow.py +++ b/tests/components/ambient_station/test_config_flow.py @@ -1,5 +1,5 @@ """Define tests for the Ambient PWS config flow.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch from aioambient.errors import AmbientError import pytest @@ -10,44 +10,34 @@ from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY -async def test_duplicate_error(hass, config, config_entry, setup_ambient_station): - """Test that errors are shown when duplicates are added.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "already_configured" - - @pytest.mark.parametrize( - "devices,error", + "devices_response,errors", [ - (AmbientError, "invalid_key"), - (AsyncMock(return_value=[]), "no_devices"), + (AsyncMock(side_effect=AmbientError), {"base": "invalid_key"}), + (AsyncMock(return_value=[]), {"base": "no_devices"}), ], ) -async def test_errors(hass, config, devices, error, setup_ambient_station): - """Test that various issues show the correct error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {"base": error} - - -async def test_show_form(hass): - """Test that the form is served with no input.""" +async def test_create_entry( + hass, api, config, devices_response, errors, mock_aioambient +): + """Test creating an entry.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" + # Test errors that can arise: + with patch.object(api, "get_devices", devices_response): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == errors -async def test_step_user(hass, config, setup_ambient_station): - """Test that the user step works.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config + # Test that we can recover and finish the flow after errors occur: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "67890fghij67" @@ -55,3 +45,12 @@ async def test_step_user(hass, config, setup_ambient_station): CONF_API_KEY: "12345abcde12345abcde", CONF_APP_KEY: "67890fghij67890fghij", } + + +async def test_duplicate_error(hass, config, config_entry, setup_config_entry): + """Test that errors are shown when duplicates are added.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=config + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/ambient_station/test_diagnostics.py b/tests/components/ambient_station/test_diagnostics.py index e6285afa17a..7672193d264 100644 --- a/tests/components/ambient_station/test_diagnostics.py +++ b/tests/components/ambient_station/test_diagnostics.py @@ -6,11 +6,11 @@ from tests.components.diagnostics import get_diagnostics_for_config_entry async def test_entry_diagnostics( - hass, config_entry, hass_client, setup_ambient_station, station_data + hass, config_entry, hass_client, data_station, setup_config_entry ): """Test config entry diagnostics.""" ambient = hass.data[DOMAIN][config_entry.entry_id] - ambient.stations = station_data + ambient.stations = data_station assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { "entry_id": config_entry.entry_id, diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index cc580b16e08..bd16ce96ffe 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -233,9 +233,9 @@ async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock): ): await analytics.send_analytics() assert ( - "'addons': [{'slug': 'test_addon', 'protected': True, 'version': '1', 'auto_update': False}]" - in caplog.text - ) + "'addons': [{'slug': 'test_addon', 'protected': True, 'version': '1'," + " 'auto_update': False}]" + ) in caplog.text assert "'addon_count':" not in caplog.text @@ -251,9 +251,9 @@ async def test_send_statistics(hass, caplog, aioclient_mock): with patch("homeassistant.components.analytics.analytics.HA_VERSION", MOCK_VERSION): await analytics.send_analytics() assert ( - "'state_count': 0, 'automation_count': 0, 'integration_count': 1, 'user_count': 0" - in caplog.text - ) + "'state_count': 0, 'automation_count': 0, 'integration_count': 1," + " 'user_count': 0" + ) in caplog.text assert "'integrations':" not in caplog.text @@ -406,9 +406,9 @@ async def test_dev_url_error(hass, aioclient_mock, caplog): payload = aioclient_mock.mock_calls[0] assert str(payload[1]) == ANALYTICS_ENDPOINT_URL_DEV assert ( - f"Sending analytics failed with statuscode 400 from {ANALYTICS_ENDPOINT_URL_DEV}" - in caplog.text - ) + "Sending analytics failed with statuscode 400 from" + f" {ANALYTICS_ENDPOINT_URL_DEV}" + ) in caplog.text async def test_nightly_endpoint(hass, aioclient_mock): diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 31e9a9c82c3..5ebd95ccacd 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -111,7 +111,7 @@ def patch_connect(success): } -def patch_shell(response=None, error=False, mac_eth=False): +def patch_shell(response=None, error=False, mac_eth=False, exc=None): """Mock the `AdbDeviceTcpAsyncFake.shell` and `DeviceAsyncFake.shell` methods.""" async def shell_success(self, cmd, *args, **kwargs): @@ -128,7 +128,7 @@ def patch_shell(response=None, error=False, mac_eth=False): async def shell_fail_python(self, cmd, *args, **kwargs): """Mock the `AdbDeviceTcpAsyncFake.shell` method when it fails.""" self.shell_cmd = cmd - raise ValueError + raise exc or ValueError async def shell_fail_server(self, cmd): """Mock the `DeviceAsyncFake.shell` method when it fails.""" diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index f5487c78425..a0d6230ed6b 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -2,6 +2,7 @@ import logging from unittest.mock import Mock, patch +from adb_shell.exceptions import TcpTimeoutException as AdbShellTimeoutException from androidtv.constants import APPS as ANDROIDTV_APPS, KEYS from androidtv.exceptions import LockNotAcquiredException import pytest @@ -78,8 +79,14 @@ ADB_PATCH_KEY = "patch_key" TEST_ENTITY_NAME = "entity_name" MSG_RECONNECT = { - patchers.KEY_PYTHON: f"ADB connection to {HOST}:{DEFAULT_PORT} successfully established", - patchers.KEY_SERVER: f"ADB connection to {HOST}:{DEFAULT_PORT} via ADB server {patchers.ADB_SERVER_HOST}:{DEFAULT_ADB_SERVER_PORT} successfully established", + patchers.KEY_PYTHON: ( + f"ADB connection to {HOST}:{DEFAULT_PORT} successfully established" + ), + patchers.KEY_SERVER: ( + f"ADB connection to {HOST}:{DEFAULT_PORT} via ADB server" + f" {patchers.ADB_SERVER_HOST}:{DEFAULT_ADB_SERVER_PORT} successfully" + " established" + ), } SHELL_RESPONSE_OFF = "" @@ -532,25 +539,28 @@ async def test_select_source_firetv(hass, source, expected_arg, method_patch): @pytest.mark.parametrize( - "config", + ["config", "connect"], [ - CONFIG_ANDROIDTV_DEFAULT, - CONFIG_FIRETV_DEFAULT, + (CONFIG_ANDROIDTV_DEFAULT, False), + (CONFIG_FIRETV_DEFAULT, False), + (CONFIG_ANDROIDTV_DEFAULT, True), + (CONFIG_FIRETV_DEFAULT, True), ], ) -async def test_setup_fail(hass, config): +async def test_setup_fail(hass, config, connect): """Test that the entity is not created when the ADB connection is not established.""" patch_key, entity_id, config_entry = _setup(config) config_entry.add_to_hass(hass) - with patchers.patch_connect(False)[patch_key], patchers.patch_shell( - SHELL_RESPONSE_OFF + with patchers.patch_connect(connect)[patch_key], patchers.patch_shell( + SHELL_RESPONSE_OFF, error=True, exc=AdbShellTimeoutException )[patch_key]: assert await hass.config_entries.async_setup(config_entry.entry_id) is False await hass.async_block_till_done() await async_update_entity(hass, entity_id) state = hass.states.get(entity_id) + assert config_entry.state == ConfigEntryState.SETUP_RETRY assert state is None diff --git a/tests/components/anthemav/conftest.py b/tests/components/anthemav/conftest.py index 595c867304b..89dba9563d1 100644 --- a/tests/components/anthemav/conftest.py +++ b/tests/components/anthemav/conftest.py @@ -1,5 +1,5 @@ """Fixtures for anthemav integration tests.""" -from typing import Callable +from collections.abc import Callable from unittest.mock import AsyncMock, MagicMock, patch import pytest diff --git a/tests/components/anthemav/test_init.py b/tests/components/anthemav/test_init.py index 63bd8390958..019668769ed 100644 --- a/tests/components/anthemav/test_init.py +++ b/tests/components/anthemav/test_init.py @@ -1,5 +1,5 @@ """Test the Anthem A/V Receivers config flow.""" -from typing import Callable +from collections.abc import Callable from unittest.mock import ANY, AsyncMock, patch from anthemav.device_error import DeviceError diff --git a/tests/components/anthemav/test_media_player.py b/tests/components/anthemav/test_media_player.py index e6a4e60108a..2609ee46dd8 100644 --- a/tests/components/anthemav/test_media_player.py +++ b/tests/components/anthemav/test_media_player.py @@ -1,5 +1,5 @@ """Test the Anthem A/V Receivers config flow.""" -from typing import Callable +from collections.abc import Callable from unittest.mock import AsyncMock import pytest diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index d6bfd3de521..f1df9d1eded 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -1,5 +1,5 @@ """The tests for the Home Assistant API component.""" -# pylint: disable=protected-access + from http import HTTPStatus import json from unittest.mock import patch diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index 6a53b70d9e7..d16f7dd7d35 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -140,8 +140,8 @@ async def test_source_list(player, state): @pytest.mark.parametrize( "mode", [ - ("STEREO"), - ("DOLBY_PL"), + "STEREO", + "DOLBY_PL", ], ) async def test_select_sound_mode(player, state, mode): diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py index d062d30ba3f..a0f15b86bb4 100644 --- a/tests/components/august/test_binary_sensor.py +++ b/tests/components/august/test_binary_sensor.py @@ -191,7 +191,9 @@ async def test_doorbell_update_via_pubnub(hass): "data": { "result": { "created_at": "2021-03-16T01:07:08.817Z", - "secure_url": "https://dyu7azbnaoi74.cloudfront.net/zip/images/zip.jpeg", + "secure_url": ( + "https://dyu7azbnaoi74.cloudfront.net/zip/images/zip.jpeg" + ), }, }, }, @@ -220,7 +222,9 @@ async def test_doorbell_update_via_pubnub(hass): "format": "jpg", "created_at": "2021-03-16T02:36:26.886Z", "bytes": 14061, - "secure_url": "https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg", + "secure_url": ( + "https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg" + ), "url": "https://dyu7azbnaoi74.cloudfront.net/images/1f8.jpeg", "etag": "09e839331c4ea59eef28081f2caa0e90", }, diff --git a/tests/components/august/test_diagnostics.py b/tests/components/august/test_diagnostics.py index 0b65b6eea3e..2b193c84b98 100644 --- a/tests/components/august/test_diagnostics.py +++ b/tests/components/august/test_diagnostics.py @@ -75,7 +75,7 @@ async def test_diagnostics(hass, hass_client): "doorbell_low_battery": False, "ip_addr": "10.0.1.11", "link_quality": 54, - "load_average": "0.50 0.47 0.35 " "1/154 9345", + "load_average": "0.50 0.47 0.35 1/154 9345", "signal_level": -56, "steady_ac_in": 22.196405, "temperature": 28.25, diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index ef82efae177..839c62f2270 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -95,9 +95,9 @@ async def test_unlock_throws_august_api_http_error(hass): await hass.services.async_call(LOCK_DOMAIN, SERVICE_UNLOCK, data, blocking=True) except HomeAssistantError as err: last_err = err - assert ( - str(last_err) - == "A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user consumable" + assert str(last_err) == ( + "A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user" + " consumable" ) @@ -121,9 +121,9 @@ async def test_lock_throws_august_api_http_error(hass): await hass.services.async_call(LOCK_DOMAIN, SERVICE_LOCK, data, blocking=True) except HomeAssistantError as err: last_err = err - assert ( - str(last_err) - == "A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user consumable" + assert str(last_err) == ( + "A6697750D607098BAE8D6BAA11EF8063 Name: This should bubble up as its user" + " consumable" ) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 5ec77a43e8c..6f6b0df5814 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -121,8 +121,9 @@ async def test_service_specify_data(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "{{ trigger.platform }} - " - "{{ trigger.event.event_type }}" + "some": ( + "{{ trigger.platform }} - {{ trigger.event.event_type }}" + ) }, }, } @@ -1401,9 +1402,9 @@ async def test_automation_bad_config_validation( # Check we get the expected error message assert ( - f"Automation with alias 'bad_automation' {problem} and has been disabled: {details}" - in caplog.text - ) + f"Automation with alias 'bad_automation' {problem} and has been disabled:" + f" {details}" + ) in caplog.text # Make sure one bad automation does not prevent other automations from setting up assert hass.states.async_entity_ids("automation") == ["automation.good_automation"] @@ -1969,7 +1970,10 @@ async def test_blueprint_automation(hass, calls): "a_number": 5, }, "Blueprint 'Call service based on event' generated invalid automation", - "value should be a string for dictionary value @ data['action'][0]['service']", + ( + "value should be a string for dictionary value @" + " data['action'][0]['service']" + ), ), ), ) @@ -2016,10 +2020,10 @@ async def test_blueprint_automation_fails_substitution(hass, caplog): }, ) assert ( - "Blueprint 'Call service based on event' failed to generate automation with inputs " - "{'trigger_event': 'test_event', 'service_to_call': 'test.automation', 'a_number': 5}:" - " No substitution found for input blah" in caplog.text - ) + "Blueprint 'Call service based on event' failed to generate automation with" + " inputs {'trigger_event': 'test_event', 'service_to_call': 'test.automation'," + " 'a_number': 5}: No substitution found for input blah" + ) in caplog.text async def test_trigger_service(hass, calls): @@ -2204,7 +2208,10 @@ async def test_recursive_automation_starting_script( "sequence": [ {"event": "trigger_automation"}, { - "wait_template": f"{{{{ float(states('sensor.test'), 0) >= {automation_runs} }}}}" + "wait_template": ( + "{{ float(states('sensor.test'), 0) >=" + f" {automation_runs} }}}}" + ) }, {"service": "script.script1"}, {"service": "test.script_done"}, diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index 32a4578bebc..8122948bff9 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -23,7 +23,7 @@ from homeassistant.const import ( LIGHT_LUX, PERCENTAGE, STATE_UNAVAILABLE, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -84,7 +84,7 @@ async def test_awair_gen1_sensors(hass: HomeAssistant, user, cloud_devices, gen1 "sensor.living_room_temperature", f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_TEMP].unique_id_tag}", "21.8", - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, "awair_index": 1.0}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, "awair_index": 1.0}, ) assert_expected_properties( diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index c816277a3f4..38f20df7ff0 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -1,36 +1,222 @@ """Axis conftest.""" from __future__ import annotations +from copy import deepcopy from unittest.mock import patch -from axis.rtsp import ( - SIGNAL_DATA, - SIGNAL_FAILED, - SIGNAL_PLAYING, - STATE_PLAYING, - STATE_STOPPED, -) +from axis.rtsp import Signal, State import pytest +import respx +from homeassistant.components.axis.const import CONF_EVENTS, DOMAIN as AXIS_DOMAIN +from homeassistant.const import ( + CONF_HOST, + CONF_MODEL, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) + +from .const import ( + API_DISCOVERY_RESPONSE, + APPLICATIONS_LIST_RESPONSE, + BASIC_DEVICE_INFO_RESPONSE, + BRAND_RESPONSE, + DEFAULT_HOST, + FORMATTED_MAC, + IMAGE_RESPONSE, + MODEL, + MQTT_CLIENT_RESPONSE, + NAME, + PORT_MANAGEMENT_RESPONSE, + PORTS_RESPONSE, + PROPERTIES_RESPONSE, + PTZ_RESPONSE, + STREAM_PROFILES_RESPONSE, + VIEW_AREAS_RESPONSE, + VMD4_RESPONSE, +) + +from tests.common import MockConfigEntry from tests.components.light.conftest import mock_light_profiles # noqa: F401 +# Config entry fixtures + + +@pytest.fixture(name="config_entry") +def config_entry_fixture(hass, config, options, config_entry_version): + """Define a config entry fixture.""" + entry = MockConfigEntry( + domain=AXIS_DOMAIN, + unique_id=FORMATTED_MAC, + data=config, + options=options, + version=config_entry_version, + ) + entry.add_to_hass(hass) + return entry + + +@pytest.fixture(name="config_entry_version") +def config_entry_version_fixture(request): + """Define a config entry version fixture.""" + return 3 + + +@pytest.fixture(name="config") +def config_fixture(): + """Define a config entry data fixture.""" + return { + CONF_HOST: DEFAULT_HOST, + CONF_USERNAME: "root", + CONF_PASSWORD: "pass", + CONF_PORT: 80, + CONF_MODEL: MODEL, + CONF_NAME: NAME, + } + + +@pytest.fixture(name="options") +def options_fixture(request): + """Define a config entry options fixture.""" + return {CONF_EVENTS: True} + + +# Axis API fixtures + + +@pytest.fixture(name="mock_vapix_requests") +def default_request_fixture(respx_mock): + """Mock default Vapix requests responses.""" + + def __mock_default_requests(host): + path = f"http://{host}:80" + + if host != DEFAULT_HOST: + respx.post(f"{path}/axis-cgi/apidiscovery.cgi").respond( + json=API_DISCOVERY_RESPONSE, + ) + respx.post(f"{path}/axis-cgi/basicdeviceinfo.cgi").respond( + json=BASIC_DEVICE_INFO_RESPONSE, + ) + respx.post(f"{path}/axis-cgi/io/portmanagement.cgi").respond( + json=PORT_MANAGEMENT_RESPONSE, + ) + respx.post(f"{path}/axis-cgi/mqtt/client.cgi").respond( + json=MQTT_CLIENT_RESPONSE, + ) + respx.post(f"{path}/axis-cgi/streamprofile.cgi").respond( + json=STREAM_PROFILES_RESPONSE, + ) + respx.post(f"{path}/axis-cgi/viewarea/info.cgi").respond( + json=VIEW_AREAS_RESPONSE + ) + respx.get(f"{path}/axis-cgi/param.cgi?action=list&group=root.Brand").respond( + text=BRAND_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get(f"{path}/axis-cgi/param.cgi?action=list&group=root.Image").respond( + text=IMAGE_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get(f"{path}/axis-cgi/param.cgi?action=list&group=root.Input").respond( + text=PORTS_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get(f"{path}/axis-cgi/param.cgi?action=list&group=root.IOPort").respond( + text=PORTS_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get(f"{path}/axis-cgi/param.cgi?action=list&group=root.Output").respond( + text=PORTS_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get( + f"{path}/axis-cgi/param.cgi?action=list&group=root.Properties" + ).respond( + text=PROPERTIES_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get(f"{path}/axis-cgi/param.cgi?action=list&group=root.PTZ").respond( + text=PTZ_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get( + f"{path}/axis-cgi/param.cgi?action=list&group=root.StreamProfile" + ).respond( + text=STREAM_PROFILES_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.post(f"{path}/axis-cgi/applications/list.cgi").respond( + text=APPLICATIONS_LIST_RESPONSE, + headers={"Content-Type": "text/xml"}, + ) + respx.post(f"{path}/local/vmd/control.cgi").respond(json=VMD4_RESPONSE) + + yield __mock_default_requests + + +@pytest.fixture() +def api_discovery_items(): + """Additional Apidiscovery items.""" + return {} + + +@pytest.fixture(autouse=True) +def api_discovery_fixture(api_discovery_items): + """Apidiscovery mock response.""" + data = deepcopy(API_DISCOVERY_RESPONSE) + if api_discovery_items: + data["data"]["apiList"].append(api_discovery_items) + respx.post(f"http://{DEFAULT_HOST}:80/axis-cgi/apidiscovery.cgi").respond(json=data) + + +@pytest.fixture(name="setup_default_vapix_requests") +def default_vapix_requests_fixture(mock_vapix_requests): + """Mock default Vapix requests responses.""" + mock_vapix_requests(DEFAULT_HOST) + + +@pytest.fixture(name="prepare_config_entry") +async def prep_config_entry_fixture(hass, config_entry, setup_default_vapix_requests): + """Fixture factory to set up Axis network device.""" + + async def __mock_setup_config_entry(): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + return config_entry + + yield __mock_setup_config_entry + + +@pytest.fixture(name="setup_config_entry") +async def setup_config_entry_fixture(hass, config_entry, setup_default_vapix_requests): + """Define a fixture to set up Axis network device.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield config_entry + + +# RTSP fixtures + @pytest.fixture(autouse=True) def mock_axis_rtspclient(): """No real RTSP communication allowed.""" - with patch("axis.streammanager.RTSPClient") as rtsp_client_mock: + with patch("axis.stream_manager.RTSPClient") as rtsp_client_mock: - rtsp_client_mock.return_value.session.state = STATE_STOPPED + rtsp_client_mock.return_value.session.state = State.STOPPED async def start_stream(): """Set state to playing when calling RTSPClient.start.""" - rtsp_client_mock.return_value.session.state = STATE_PLAYING + rtsp_client_mock.return_value.session.state = State.PLAYING rtsp_client_mock.return_value.start = start_stream def stop_stream(): """Set state to stopped when calling RTSPClient.stop.""" - rtsp_client_mock.return_value.session.state = STATE_STOPPED + rtsp_client_mock.return_value.session.state = State.STOPPED rtsp_client_mock.return_value.stop = stop_stream @@ -40,7 +226,7 @@ def mock_axis_rtspclient(): if data: rtsp_client_mock.return_value.rtp.data = data - axis_streammanager_session_callback(signal=SIGNAL_DATA) + axis_streammanager_session_callback(signal=Signal.DATA) elif state: axis_streammanager_session_callback(signal=state) else: @@ -106,7 +292,7 @@ def mock_rtsp_signal_state(mock_axis_rtspclient): def send_signal(connected: bool) -> None: """Signal state change of RTSP connection.""" - signal = SIGNAL_PLAYING if connected else SIGNAL_FAILED + signal = Signal.PLAYING if connected else Signal.FAILED mock_axis_rtspclient(state=signal) yield send_signal diff --git a/tests/components/axis/const.py b/tests/components/axis/const.py new file mode 100644 index 00000000000..d90a788ae75 --- /dev/null +++ b/tests/components/axis/const.py @@ -0,0 +1,141 @@ +"""Constants for Axis integration tests.""" + + +MAC = "00408C123456" +FORMATTED_MAC = "00:40:8c:12:34:56" +MODEL = "model" +NAME = "name" + +DEFAULT_HOST = "1.2.3.4" + + +API_DISCOVERY_RESPONSE = { + "method": "getApiList", + "apiVersion": "1.0", + "data": { + "apiList": [ + {"id": "api-discovery", "version": "1.0", "name": "API Discovery Service"}, + {"id": "param-cgi", "version": "1.0", "name": "Legacy Parameter Handling"}, + ] + }, +} + +API_DISCOVERY_BASIC_DEVICE_INFO = { + "id": "basic-device-info", + "version": "1.1", + "name": "Basic Device Information", +} +API_DISCOVERY_MQTT = {"id": "mqtt-client", "version": "1.0", "name": "MQTT Client API"} +API_DISCOVERY_PORT_MANAGEMENT = { + "id": "io-port-management", + "version": "1.0", + "name": "IO Port Management", +} + +APPLICATIONS_LIST_RESPONSE = """ + +""" + +BASIC_DEVICE_INFO_RESPONSE = { + "apiVersion": "1.1", + "data": { + "propertyList": { + "ProdNbr": "M1065-LW", + "ProdType": "Network Camera", + "SerialNumber": MAC, + "Version": "9.80.1", + } + }, +} + + +MQTT_CLIENT_RESPONSE = { + "apiVersion": "1.0", + "context": "some context", + "method": "getClientStatus", + "data": {"status": {"state": "active", "connectionStatus": "Connected"}}, +} + +PORT_MANAGEMENT_RESPONSE = { + "apiVersion": "1.0", + "method": "getPorts", + "data": { + "numberOfPorts": 1, + "items": [ + { + "port": "0", + "configurable": False, + "usage": "", + "name": "PIR sensor", + "direction": "input", + "state": "open", + "normalState": "open", + } + ], + }, +} + +VMD4_RESPONSE = { + "apiVersion": "1.4", + "method": "getConfiguration", + "context": "Axis library", + "data": { + "cameras": [{"id": 1, "rotation": 0, "active": True}], + "profiles": [ + {"filters": [], "camera": 1, "triggers": [], "name": "Profile 1", "uid": 1} + ], + }, +} + +BRAND_RESPONSE = """root.Brand.Brand=AXIS +root.Brand.ProdFullName=AXIS M1065-LW Network Camera +root.Brand.ProdNbr=M1065-LW +root.Brand.ProdShortName=AXIS M1065-LW +root.Brand.ProdType=Network Camera +root.Brand.ProdVariant= +root.Brand.WebURL=http://www.axis.com +""" + +IMAGE_RESPONSE = """root.Image.I0.Enabled=yes +root.Image.I0.Name=View Area 1 +root.Image.I0.Source=0 +root.Image.I1.Enabled=no +root.Image.I1.Name=View Area 2 +root.Image.I1.Source=0 +""" + +PORTS_RESPONSE = """root.Input.NbrOfInputs=1 +root.IOPort.I0.Configurable=no +root.IOPort.I0.Direction=input +root.IOPort.I0.Input.Name=PIR sensor +root.IOPort.I0.Input.Trig=closed +root.Output.NbrOfOutputs=0 +""" + +PROPERTIES_RESPONSE = f"""root.Properties.API.HTTP.Version=3 +root.Properties.API.Metadata.Metadata=yes +root.Properties.API.Metadata.Version=1.0 +root.Properties.EmbeddedDevelopment.Version=2.16 +root.Properties.Firmware.BuildDate=Feb 15 2019 09:42 +root.Properties.Firmware.BuildNumber=26 +root.Properties.Firmware.Version=9.10.1 +root.Properties.Image.Format=jpeg,mjpeg,h264 +root.Properties.Image.NbrOfViews=2 +root.Properties.Image.Resolution=1920x1080,1280x960,1280x720,1024x768,1024x576,800x600,640x480,640x360,352x240,320x240 +root.Properties.Image.Rotation=0,180 +root.Properties.System.SerialNumber={MAC} +""" + +PTZ_RESPONSE = "" + + +STREAM_PROFILES_RESPONSE = """root.StreamProfile.MaxGroups=26 +root.StreamProfile.S0.Description=profile_1_description +root.StreamProfile.S0.Name=profile_1 +root.StreamProfile.S0.Parameters=videocodec=h264 +root.StreamProfile.S1.Description=profile_2_description +root.StreamProfile.S1.Name=profile_2 +root.StreamProfile.S1.Parameters=videocodec=h265 +""" + +VIEW_AREAS_RESPONSE = {"apiVersion": "1.0", "method": "list", "data": {"viewAreas": []}} diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index 0fe862263ff..de4bdd95943 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -8,7 +8,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component -from .test_device import NAME, setup_axis_integration +from .const import NAME async def test_platform_manually_configured(hass): @@ -25,17 +25,13 @@ async def test_platform_manually_configured(hass): assert AXIS_DOMAIN not in hass.data -async def test_no_binary_sensors(hass): +async def test_no_binary_sensors(hass, setup_config_entry): """Test that no sensors in Axis results in no sensor entities.""" - await setup_axis_integration(hass) - assert not hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN) -async def test_binary_sensors(hass, mock_rtsp_event): +async def test_binary_sensors(hass, setup_config_entry, mock_rtsp_event): """Test that sensors are loaded properly.""" - await setup_axis_integration(hass) - mock_rtsp_event( topic="tns1:Device/tnsaxis:Sensor/PIR", data_type="state", diff --git a/tests/components/axis/test_camera.py b/tests/components/axis/test_camera.py index 4961b4c40ca..57d54ba17bd 100644 --- a/tests/components/axis/test_camera.py +++ b/tests/components/axis/test_camera.py @@ -2,6 +2,8 @@ from unittest.mock import patch +import pytest + from homeassistant.components import camera from homeassistant.components.axis.const import ( CONF_STREAM_PROFILE, @@ -11,7 +13,7 @@ from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.const import STATE_IDLE from homeassistant.setup import async_setup_component -from .test_device import ENTRY_OPTIONS, NAME, setup_axis_integration +from .const import NAME async def test_platform_manually_configured(hass): @@ -26,10 +28,8 @@ async def test_platform_manually_configured(hass): assert AXIS_DOMAIN not in hass.data -async def test_camera(hass): +async def test_camera(hass, setup_config_entry): """Test that Axis camera platform is loaded properly.""" - await setup_axis_integration(hass) - assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 1 entity_id = f"{CAMERA_DOMAIN}.{NAME}" @@ -47,11 +47,9 @@ async def test_camera(hass): ) -async def test_camera_with_stream_profile(hass): +@pytest.mark.parametrize("options", [{CONF_STREAM_PROFILE: "profile_1"}]) +async def test_camera_with_stream_profile(hass, setup_config_entry): """Test that Axis camera entity is using the correct path with stream profike.""" - with patch.dict(ENTRY_OPTIONS, {CONF_STREAM_PROFILE: "profile_1"}): - await setup_axis_integration(hass) - assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 1 entity_id = f"{CAMERA_DOMAIN}.{NAME}" @@ -72,9 +70,9 @@ async def test_camera_with_stream_profile(hass): ) -async def test_camera_disabled(hass): +async def test_camera_disabled(hass, prepare_config_entry): """Test that Axis camera platform is loaded properly but does not create camera entity.""" - with patch("axis.vapix.Params.image_format", new=None): - await setup_axis_integration(hass) + with patch("axis.vapix.vapix.Params.image_format", new=None): + await prepare_config_entry() assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 0 diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 2daf350ac93..d66fb3881cb 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -2,7 +2,6 @@ from unittest.mock import patch import pytest -import respx from homeassistant.components import dhcp, ssdp, zeroconf from homeassistant.components.axis import config_flow @@ -32,19 +31,12 @@ from homeassistant.const import ( ) from homeassistant.data_entry_flow import FlowResultType -from .test_device import ( - DEFAULT_HOST, - MAC, - MODEL, - NAME, - mock_default_vapix_requests, - setup_axis_integration, -) +from .const import DEFAULT_HOST, MAC, MODEL, NAME from tests.common import MockConfigEntry -async def test_flow_manual_configuration(hass): +async def test_flow_manual_configuration(hass, setup_default_vapix_requests): """Test that config flow works.""" MockConfigEntry(domain=AXIS_DOMAIN, source=SOURCE_IGNORE).add_to_hass(hass) @@ -55,17 +47,15 @@ async def test_flow_manual_configuration(hass): assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER - with respx.mock: - mock_default_vapix_requests(respx) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", - CONF_PORT: 80, - }, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_PORT: 80, + }, + ) assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" @@ -79,10 +69,11 @@ async def test_flow_manual_configuration(hass): } -async def test_manual_configuration_update_configuration(hass): +async def test_manual_configuration_update_configuration( + hass, setup_config_entry, mock_vapix_requests +): """Test that config flow fails on already configured device.""" - config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, context={"source": SOURCE_USER} @@ -92,10 +83,9 @@ async def test_manual_configuration_update_configuration(hass): assert result["step_id"] == SOURCE_USER with patch( - "homeassistant.components.axis.async_setup_entry", - return_value=True, - ) as mock_setup_entry, respx.mock: - mock_default_vapix_requests(respx, "2.3.4.5") + "homeassistant.components.axis.async_setup_entry", return_value=True + ) as mock_setup_entry: + mock_vapix_requests("2.3.4.5") result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -165,7 +155,9 @@ async def test_flow_fails_cannot_connect(hass): assert result["errors"] == {"base": "cannot_connect"} -async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): +async def test_flow_create_entry_multiple_existing_entries_of_same_model( + hass, setup_default_vapix_requests +): """Test that create entry can generate a name with other entries.""" entry = MockConfigEntry( domain=AXIS_DOMAIN, @@ -185,17 +177,15 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER - with respx.mock: - mock_default_vapix_requests(respx) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", - CONF_PORT: 80, - }, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_PORT: 80, + }, + ) assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" @@ -211,32 +201,32 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): assert result["data"][CONF_NAME] == "M1065-LW 2" -async def test_reauth_flow_update_configuration(hass): +async def test_reauth_flow_update_configuration( + hass, setup_config_entry, mock_vapix_requests +): """Test that config flow fails on already configured device.""" - config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, context={"source": SOURCE_REAUTH}, - data=config_entry.data, + data=setup_config_entry.data, ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER - with respx.mock: - mock_default_vapix_requests(respx, "2.3.4.5") - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: "2.3.4.5", - CONF_USERNAME: "user2", - CONF_PASSWORD: "pass2", - CONF_PORT: 80, - }, - ) - await hass.async_block_till_done() + mock_vapix_requests("2.3.4.5") + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "2.3.4.5", + CONF_USERNAME: "user2", + CONF_PASSWORD: "pass2", + CONF_PORT: 80, + }, + ) + await hass.async_block_till_done() assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -265,7 +255,10 @@ async def test_reauth_flow_update_configuration(hass): "st": "urn:axis-com:service:BasicService:1", "usn": f"uuid:Upnp-BasicDevice-1_0-{MAC}::urn:axis-com:service:BasicService:1", "ext": "", - "server": "Linux/4.14.173-axis8, UPnP/1.0, Portable SDK for UPnP devices/1.8.7", + "server": ( + "Linux/4.14.173-axis8, UPnP/1.0, Portable SDK for UPnP" + " devices/1.8.7" + ), "deviceType": "urn:schemas-upnp-org:device:Basic:1", "friendlyName": f"AXIS M1065-LW - {MAC}", "manufacturer": "AXIS", @@ -306,7 +299,9 @@ async def test_reauth_flow_update_configuration(hass): ), ], ) -async def test_discovery_flow(hass, source: str, discovery_info: dict): +async def test_discovery_flow( + hass, setup_default_vapix_requests, source: str, discovery_info: dict +): """Test the different discovery flows for new devices work.""" result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, data=discovery_info, context={"source": source} @@ -319,17 +314,15 @@ async def test_discovery_flow(hass, source: str, discovery_info: dict): assert len(flows) == 1 assert flows[0].get("context", {}).get("configuration_url") == "http://1.2.3.4:80" - with respx.mock: - mock_default_vapix_requests(respx) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", - CONF_PORT: 80, - }, - ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "user", + CONF_PASSWORD: "pass", + CONF_PORT: 80, + }, + ) assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == f"M1065-LW - {MAC}" @@ -383,11 +376,10 @@ async def test_discovery_flow(hass, source: str, discovery_info: dict): ], ) async def test_discovered_device_already_configured( - hass, source: str, discovery_info: dict + hass, setup_config_entry, source: str, discovery_info: dict ): """Test that discovery doesn't setup already configured devices.""" - config_entry = await setup_axis_integration(hass) - assert config_entry.data[CONF_HOST] == DEFAULT_HOST + assert setup_config_entry.data[CONF_HOST] == DEFAULT_HOST result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, data=discovery_info, context={"source": source} @@ -395,7 +387,7 @@ async def test_discovered_device_already_configured( assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" - assert config_entry.data[CONF_HOST] == DEFAULT_HOST + assert setup_config_entry.data[CONF_HOST] == DEFAULT_HOST @pytest.mark.parametrize( @@ -439,11 +431,15 @@ async def test_discovered_device_already_configured( ], ) async def test_discovery_flow_updated_configuration( - hass, source: str, discovery_info: dict, expected_port: int + hass, + setup_config_entry, + mock_vapix_requests, + source: str, + discovery_info: dict, + expected_port: int, ): """Test that discovery flow update configuration with new parameters.""" - config_entry = await setup_axis_integration(hass) - assert config_entry.data == { + assert setup_config_entry.data == { CONF_HOST: DEFAULT_HOST, CONF_PORT: 80, CONF_USERNAME: "root", @@ -453,10 +449,9 @@ async def test_discovery_flow_updated_configuration( } with patch( - "homeassistant.components.axis.async_setup_entry", - return_value=True, - ) as mock_setup_entry, respx.mock: - mock_default_vapix_requests(respx, "2.3.4.5") + "homeassistant.components.axis.async_setup_entry", return_value=True + ) as mock_setup_entry: + mock_vapix_requests("2.3.4.5") result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, data=discovery_info, context={"source": source} ) @@ -464,7 +459,7 @@ async def test_discovery_flow_updated_configuration( assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" - assert config_entry.data == { + assert setup_config_entry.data == { CONF_HOST: "2.3.4.5", CONF_PORT: expected_port, CONF_USERNAME: "root", @@ -573,18 +568,13 @@ async def test_discovery_flow_ignore_link_local_address( assert result["reason"] == "link_local_address" -async def test_option_flow(hass): +async def test_option_flow(hass, setup_config_entry): """Test config flow options.""" - config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] assert device.option_stream_profile == DEFAULT_STREAM_PROFILE assert device.option_video_source == DEFAULT_VIDEO_SOURCE - with respx.mock: - mock_default_vapix_requests(respx) - result = await hass.config_entries.options.async_init( - device.config_entry.entry_id - ) + result = await hass.config_entries.options.async_init(setup_config_entry.entry_id) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "configure_stream" diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index ba6df6e2e2d..23b32e47cb5 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -1,324 +1,60 @@ """Test Axis device.""" -from copy import deepcopy from unittest import mock from unittest.mock import Mock, patch import axis as axislib -from axis.event_stream import OPERATION_INITIALIZED import pytest -import respx from homeassistant.components import axis, zeroconf -from homeassistant.components.axis.const import CONF_EVENTS, DOMAIN as AXIS_DOMAIN +from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.config_entries import SOURCE_ZEROCONF from homeassistant.const import ( CONF_HOST, CONF_MODEL, CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) from homeassistant.helpers import device_registry as dr -from tests.common import MockConfigEntry, async_fire_mqtt_message +from .const import ( + API_DISCOVERY_BASIC_DEVICE_INFO, + API_DISCOVERY_MQTT, + FORMATTED_MAC, + MAC, + NAME, +) -MAC = "00408C123456" -FORMATTED_MAC = "00:40:8c:12:34:56" -MODEL = "model" -NAME = "name" - -DEFAULT_HOST = "1.2.3.4" - -ENTRY_OPTIONS = {CONF_EVENTS: True} - -ENTRY_CONFIG = { - CONF_HOST: DEFAULT_HOST, - CONF_USERNAME: "root", - CONF_PASSWORD: "pass", - CONF_PORT: 80, - CONF_MODEL: MODEL, - CONF_NAME: NAME, -} - -API_DISCOVERY_RESPONSE = { - "method": "getApiList", - "apiVersion": "1.0", - "data": { - "apiList": [ - {"id": "api-discovery", "version": "1.0", "name": "API Discovery Service"}, - {"id": "param-cgi", "version": "1.0", "name": "Legacy Parameter Handling"}, - ] - }, -} - -API_DISCOVERY_BASIC_DEVICE_INFO = { - "id": "basic-device-info", - "version": "1.1", - "name": "Basic Device Information", -} -API_DISCOVERY_MQTT = {"id": "mqtt-client", "version": "1.0", "name": "MQTT Client API"} -API_DISCOVERY_PORT_MANAGEMENT = { - "id": "io-port-management", - "version": "1.0", - "name": "IO Port Management", -} - -APPLICATIONS_LIST_RESPONSE = """ - -""" - -BASIC_DEVICE_INFO_RESPONSE = { - "apiVersion": "1.1", - "data": { - "propertyList": { - "ProdNbr": "M1065-LW", - "ProdType": "Network Camera", - "SerialNumber": MAC, - "Version": "9.80.1", - } - }, -} - -LIGHT_CONTROL_RESPONSE = { - "apiVersion": "1.1", - "method": "getLightInformation", - "data": { - "items": [ - { - "lightID": "led0", - "lightType": "IR", - "enabled": True, - "synchronizeDayNightMode": True, - "lightState": False, - "automaticIntensityMode": False, - "automaticAngleOfIlluminationMode": False, - "nrOfLEDs": 1, - "error": False, - "errorInfo": "", - } - ] - }, -} - -MQTT_CLIENT_RESPONSE = { - "apiVersion": "1.0", - "context": "some context", - "method": "getClientStatus", - "data": {"status": {"state": "active", "connectionStatus": "Connected"}}, -} - -PORT_MANAGEMENT_RESPONSE = { - "apiVersion": "1.0", - "method": "getPorts", - "data": { - "numberOfPorts": 1, - "items": [ - { - "port": "0", - "configurable": False, - "usage": "", - "name": "PIR sensor", - "direction": "input", - "state": "open", - "normalState": "open", - } - ], - }, -} - -VMD4_RESPONSE = { - "apiVersion": "1.4", - "method": "getConfiguration", - "context": "Axis library", - "data": { - "cameras": [{"id": 1, "rotation": 0, "active": True}], - "profiles": [ - {"filters": [], "camera": 1, "triggers": [], "name": "Profile 1", "uid": 1} - ], - }, -} - -BRAND_RESPONSE = """root.Brand.Brand=AXIS -root.Brand.ProdFullName=AXIS M1065-LW Network Camera -root.Brand.ProdNbr=M1065-LW -root.Brand.ProdShortName=AXIS M1065-LW -root.Brand.ProdType=Network Camera -root.Brand.ProdVariant= -root.Brand.WebURL=http://www.axis.com -""" - -IMAGE_RESPONSE = """root.Image.I0.Enabled=yes -root.Image.I0.Name=View Area 1 -root.Image.I0.Source=0 -root.Image.I1.Enabled=no -root.Image.I1.Name=View Area 2 -root.Image.I1.Source=0 -""" - -PORTS_RESPONSE = """root.Input.NbrOfInputs=1 -root.IOPort.I0.Configurable=no -root.IOPort.I0.Direction=input -root.IOPort.I0.Input.Name=PIR sensor -root.IOPort.I0.Input.Trig=closed -root.Output.NbrOfOutputs=0 -""" - -PROPERTIES_RESPONSE = f"""root.Properties.API.HTTP.Version=3 -root.Properties.API.Metadata.Metadata=yes -root.Properties.API.Metadata.Version=1.0 -root.Properties.EmbeddedDevelopment.Version=2.16 -root.Properties.Firmware.BuildDate=Feb 15 2019 09:42 -root.Properties.Firmware.BuildNumber=26 -root.Properties.Firmware.Version=9.10.1 -root.Properties.Image.Format=jpeg,mjpeg,h264 -root.Properties.Image.NbrOfViews=2 -root.Properties.Image.Resolution=1920x1080,1280x960,1280x720,1024x768,1024x576,800x600,640x480,640x360,352x240,320x240 -root.Properties.Image.Rotation=0,180 -root.Properties.System.SerialNumber={MAC} -""" - -PTZ_RESPONSE = "" +from tests.common import async_fire_mqtt_message -STREAM_PROFILES_RESPONSE = """root.StreamProfile.MaxGroups=26 -root.StreamProfile.S0.Description=profile_1_description -root.StreamProfile.S0.Name=profile_1 -root.StreamProfile.S0.Parameters=videocodec=h264 -root.StreamProfile.S1.Description=profile_2_description -root.StreamProfile.S1.Name=profile_2 -root.StreamProfile.S1.Parameters=videocodec=h265 -""" - -VIEW_AREAS_RESPONSE = {"apiVersion": "1.0", "method": "list", "data": {"viewAreas": []}} +@pytest.fixture(name="forward_entry_setup") +def hass_mock_forward_entry_setup(hass): + """Mock async_forward_entry_setup.""" + with patch.object(hass.config_entries, "async_forward_entry_setup") as forward_mock: + yield forward_mock -def mock_default_vapix_requests(respx: respx, host: str = DEFAULT_HOST) -> None: - """Mock default Vapix requests responses.""" - respx.post(f"http://{host}:80/axis-cgi/apidiscovery.cgi").respond( - json=API_DISCOVERY_RESPONSE, - ) - respx.post(f"http://{host}:80/axis-cgi/basicdeviceinfo.cgi").respond( - json=BASIC_DEVICE_INFO_RESPONSE, - ) - respx.post(f"http://{host}:80/axis-cgi/io/portmanagement.cgi").respond( - json=PORT_MANAGEMENT_RESPONSE, - ) - respx.post(f"http://{host}:80/axis-cgi/lightcontrol.cgi").respond( - json=LIGHT_CONTROL_RESPONSE, - ) - respx.post(f"http://{host}:80/axis-cgi/mqtt/client.cgi").respond( - json=MQTT_CLIENT_RESPONSE, - ) - respx.post(f"http://{host}:80/axis-cgi/streamprofile.cgi").respond( - json=STREAM_PROFILES_RESPONSE, - ) - respx.post(f"http://{host}:80/axis-cgi/viewarea/info.cgi").respond( - json=VIEW_AREAS_RESPONSE - ) - respx.get( - f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Brand" - ).respond( - text=BRAND_RESPONSE, - headers={"Content-Type": "text/plain"}, - ) - respx.get( - f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Image" - ).respond( - text=IMAGE_RESPONSE, - headers={"Content-Type": "text/plain"}, - ) - respx.get( - f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Input" - ).respond( - text=PORTS_RESPONSE, - headers={"Content-Type": "text/plain"}, - ) - respx.get( - f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.IOPort" - ).respond( - text=PORTS_RESPONSE, - headers={"Content-Type": "text/plain"}, - ) - respx.get( - f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Output" - ).respond( - text=PORTS_RESPONSE, - headers={"Content-Type": "text/plain"}, - ) - respx.get( - f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Properties" - ).respond( - text=PROPERTIES_RESPONSE, - headers={"Content-Type": "text/plain"}, - ) - respx.get( - f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.PTZ" - ).respond( - text=PTZ_RESPONSE, - headers={"Content-Type": "text/plain"}, - ) - respx.get( - f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.StreamProfile" - ).respond( - text=STREAM_PROFILES_RESPONSE, - headers={"Content-Type": "text/plain"}, - ) - respx.post(f"http://{host}:80/axis-cgi/applications/list.cgi").respond( - text=APPLICATIONS_LIST_RESPONSE, - headers={"Content-Type": "text/xml"}, - ) - respx.post(f"http://{host}:80/local/vmd/control.cgi").respond(json=VMD4_RESPONSE) - - -async def setup_axis_integration(hass, config=ENTRY_CONFIG, options=ENTRY_OPTIONS): - """Create the Axis device.""" - config_entry = MockConfigEntry( - domain=AXIS_DOMAIN, - data=deepcopy(config), - options=deepcopy(options), - version=3, - unique_id=FORMATTED_MAC, - ) - config_entry.add_to_hass(hass) - - with respx.mock: - mock_default_vapix_requests(respx) - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - return config_entry - - -async def test_device_setup(hass): +async def test_device_setup(hass, forward_entry_setup, config, setup_config_entry): """Successful setup.""" - with patch( - "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", - return_value=True, - ) as forward_entry_setup: - config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] assert device.api.vapix.firmware_version == "9.10.1" assert device.api.vapix.product_number == "M1065-LW" assert device.api.vapix.product_type == "Network Camera" assert device.api.vapix.serial_number == "00408C123456" - entry = device.config_entry - assert len(forward_entry_setup.mock_calls) == 4 - assert forward_entry_setup.mock_calls[0][1] == (entry, "binary_sensor") - assert forward_entry_setup.mock_calls[1][1] == (entry, "camera") - assert forward_entry_setup.mock_calls[2][1] == (entry, "light") - assert forward_entry_setup.mock_calls[3][1] == (entry, "switch") + assert forward_entry_setup.mock_calls[0][1][1] == "binary_sensor" + assert forward_entry_setup.mock_calls[1][1][1] == "camera" + assert forward_entry_setup.mock_calls[2][1][1] == "light" + assert forward_entry_setup.mock_calls[3][1][1] == "switch" - assert device.host == ENTRY_CONFIG[CONF_HOST] - assert device.model == ENTRY_CONFIG[CONF_MODEL] - assert device.name == ENTRY_CONFIG[CONF_NAME] + assert device.host == config[CONF_HOST] + assert device.model == config[CONF_MODEL] + assert device.name == config[CONF_NAME] assert device.unique_id == FORMATTED_MAC device_registry = dr.async_get(hass) @@ -329,14 +65,10 @@ async def test_device_setup(hass): assert device_entry.configuration_url == device.api.config.url -async def test_device_info(hass): +@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_BASIC_DEVICE_INFO]) +async def test_device_info(hass, setup_config_entry): """Verify other path of device information works.""" - api_discovery = deepcopy(API_DISCOVERY_RESPONSE) - api_discovery["data"]["apiList"].append(API_DISCOVERY_BASIC_DEVICE_INFO) - - with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): - config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] assert device.api.vapix.firmware_version == "9.80.1" assert device.api.vapix.product_number == "M1065-LW" @@ -344,18 +76,16 @@ async def test_device_info(hass): assert device.api.vapix.serial_number == "00408C123456" -async def test_device_support_mqtt(hass, mqtt_mock): +@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_MQTT]) +async def test_device_support_mqtt(hass, mqtt_mock, setup_config_entry): """Successful setup.""" - api_discovery = deepcopy(API_DISCOVERY_RESPONSE) - api_discovery["data"]["apiList"].append(API_DISCOVERY_MQTT) - - with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): - await setup_axis_integration(hass) - mqtt_mock.async_subscribe.assert_called_with(f"{MAC}/#", mock.ANY, 0, "utf-8") topic = f"{MAC}/event/tns:onvif/Device/tns:axis/Sensor/PIR/$source/sensor/0" - message = b'{"timestamp": 1590258472044, "topic": "onvif:Device/axis:Sensor/PIR", "message": {"source": {"sensor": "0"}, "key": {}, "data": {"state": "1"}}}' + message = ( + b'{"timestamp": 1590258472044, "topic": "onvif:Device/axis:Sensor/PIR",' + b' "message": {"source": {"sensor": "0"}, "key": {}, "data": {"state": "1"}}}' + ) assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 0 async_fire_mqtt_message(hass, topic, message) @@ -367,17 +97,15 @@ async def test_device_support_mqtt(hass, mqtt_mock): assert pir.name == f"{NAME} PIR 0" -async def test_update_address(hass): +async def test_update_address(hass, setup_config_entry, mock_vapix_requests): """Test update address works.""" - config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] assert device.api.config.host == "1.2.3.4" with patch( - "homeassistant.components.axis.async_setup_entry", - return_value=True, - ) as mock_setup_entry, respx.mock: - mock_default_vapix_requests(respx, "2.3.4.5") + "homeassistant.components.axis.async_setup_entry", return_value=True + ) as mock_setup_entry: + mock_vapix_requests("2.3.4.5") await hass.config_entries.flow.async_init( AXIS_DOMAIN, data=zeroconf.ZeroconfServiceInfo( @@ -397,10 +125,10 @@ async def test_update_address(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_device_unavailable(hass, mock_rtsp_event, mock_rtsp_signal_state): +async def test_device_unavailable( + hass, setup_config_entry, mock_rtsp_event, mock_rtsp_signal_state +): """Successful setup.""" - await setup_axis_integration(hass) - # Provide an entity that can be used to verify connection state on mock_rtsp_event( topic="tns1:AudioSource/tnsaxis:TriggerLevel", @@ -431,58 +159,47 @@ async def test_device_unavailable(hass, mock_rtsp_event, mock_rtsp_signal_state) assert hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_sound_1").state == STATE_OFF -async def test_device_reset(hass): +async def test_device_reset(hass, setup_config_entry): """Successfully reset device.""" - config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] result = await device.async_reset() assert result is True -async def test_device_not_accessible(hass): +async def test_device_not_accessible(hass, config_entry, setup_default_vapix_requests): """Failed setup schedules a retry of setup.""" with patch.object(axis, "get_axis_device", side_effect=axis.errors.CannotConnect): - await setup_axis_integration(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() assert hass.data[AXIS_DOMAIN] == {} -async def test_device_trigger_reauth_flow(hass): +async def test_device_trigger_reauth_flow( + hass, config_entry, setup_default_vapix_requests +): """Failed authentication trigger a reauthentication flow.""" with patch.object( axis, "get_axis_device", side_effect=axis.errors.AuthenticationRequired ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: - await setup_axis_integration(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() mock_flow_init.assert_called_once() assert hass.data[AXIS_DOMAIN] == {} -async def test_device_unknown_error(hass): +async def test_device_unknown_error(hass, config_entry, setup_default_vapix_requests): """Unknown errors are handled.""" with patch.object(axis, "get_axis_device", side_effect=Exception): - await setup_axis_integration(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() assert hass.data[AXIS_DOMAIN] == {} -async def test_new_event_sends_signal(hass): - """Make sure that new event send signal.""" - entry = Mock() - entry.data = ENTRY_CONFIG - - axis_device = axis.device.AxisNetworkDevice(hass, entry, Mock()) - - with patch.object(axis.device, "async_dispatcher_send") as mock_dispatch_send: - axis_device.async_event_callback(action=OPERATION_INITIALIZED, event_id="event") - await hass.async_block_till_done() - - assert len(mock_dispatch_send.mock_calls) == 1 - assert len(mock_dispatch_send.mock_calls[0]) == 3 - - -async def test_shutdown(): +async def test_shutdown(config): """Successful shutdown.""" hass = Mock() entry = Mock() - entry.data = ENTRY_CONFIG + entry.data = config axis_device = axis.device.AxisNetworkDevice(hass, entry, Mock()) @@ -491,25 +208,25 @@ async def test_shutdown(): assert len(axis_device.api.stream.stop.mock_calls) == 1 -async def test_get_device_fails(hass): +async def test_get_device_fails(hass, config): """Device unauthorized yields authentication required error.""" with patch( - "axis.vapix.Vapix.request", side_effect=axislib.Unauthorized + "axis.vapix.vapix.Vapix.request", side_effect=axislib.Unauthorized ), pytest.raises(axis.errors.AuthenticationRequired): - await axis.device.get_axis_device(hass, ENTRY_CONFIG) + await axis.device.get_axis_device(hass, config) -async def test_get_device_device_unavailable(hass): +async def test_get_device_device_unavailable(hass, config): """Device unavailable yields cannot connect error.""" with patch( - "axis.vapix.Vapix.request", side_effect=axislib.RequestError + "axis.vapix.vapix.Vapix.request", side_effect=axislib.RequestError ), pytest.raises(axis.errors.CannotConnect): - await axis.device.get_axis_device(hass, ENTRY_CONFIG) + await axis.device.get_axis_device(hass, config) -async def test_get_device_unknown_error(hass): +async def test_get_device_unknown_error(hass, config): """Device yield unknown error.""" with patch( - "axis.vapix.Vapix.request", side_effect=axislib.AxisException + "axis.vapix.vapix.Vapix.request", side_effect=axislib.AxisException ), pytest.raises(axis.errors.AuthenticationRequired): - await axis.device.get_axis_device(hass, ENTRY_CONFIG) + await axis.device.get_axis_device(hass, config) diff --git a/tests/components/axis/test_diagnostics.py b/tests/components/axis/test_diagnostics.py index 4f43f1d42ff..7529f50bd6c 100644 --- a/tests/components/axis/test_diagnostics.py +++ b/tests/components/axis/test_diagnostics.py @@ -1,30 +1,22 @@ """Test Axis diagnostics.""" -from copy import deepcopy -from unittest.mock import patch +import pytest from homeassistant.components.diagnostics import REDACTED -from .test_device import ( - API_DISCOVERY_BASIC_DEVICE_INFO, - API_DISCOVERY_RESPONSE, - setup_axis_integration, -) +from .const import API_DISCOVERY_BASIC_DEVICE_INFO from tests.components.diagnostics import get_diagnostics_for_config_entry -async def test_entry_diagnostics(hass, hass_client): +@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_BASIC_DEVICE_INFO]) +async def test_entry_diagnostics(hass, hass_client, setup_config_entry): """Test config entry diagnostics.""" - api_discovery = deepcopy(API_DISCOVERY_RESPONSE) - api_discovery["data"]["apiList"].append(API_DISCOVERY_BASIC_DEVICE_INFO) - - with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): - config_entry = await setup_axis_integration(hass) - - assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { + assert await get_diagnostics_for_config_entry( + hass, hass_client, setup_config_entry + ) == { "config": { - "entry_id": config_entry.entry_id, + "entry_id": setup_config_entry.entry_id, "version": 3, "domain": "axis", "title": "Mock Title", diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index 94f8bf88a8d..fe504f7d15b 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -1,27 +1,12 @@ """Test Axis component setup process.""" from unittest.mock import AsyncMock, Mock, patch +import pytest + from homeassistant.components import axis from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.const import ( - CONF_DEVICE, - CONF_HOST, - CONF_MAC, - CONF_MODEL, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, -) -from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.device_registry import format_mac from homeassistant.setup import async_setup_component -from .test_device import MAC, setup_axis_integration - -from tests.common import MockConfigEntry - async def test_setup_no_config(hass): """Test setup without configuration.""" @@ -29,20 +14,14 @@ async def test_setup_no_config(hass): assert AXIS_DOMAIN not in hass.data -async def test_setup_entry(hass): +async def test_setup_entry(hass, setup_config_entry): """Test successful setup of entry.""" - await setup_axis_integration(hass) assert len(hass.data[AXIS_DOMAIN]) == 1 - assert format_mac(MAC) in hass.data[AXIS_DOMAIN] + assert setup_config_entry.entry_id in hass.data[AXIS_DOMAIN] -async def test_setup_entry_fails(hass): +async def test_setup_entry_fails(hass, config_entry): """Test successful setup of entry.""" - config_entry = MockConfigEntry( - domain=AXIS_DOMAIN, data={CONF_MAC: "0123"}, version=3 - ) - config_entry.add_to_hass(hass) - mock_device = Mock() mock_device.async_setup = AsyncMock(return_value=False) @@ -54,64 +33,31 @@ async def test_setup_entry_fails(hass): assert not hass.data[AXIS_DOMAIN] -async def test_unload_entry(hass): +async def test_unload_entry(hass, setup_config_entry): """Test successful unload of entry.""" - config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] assert hass.data[AXIS_DOMAIN] - assert await hass.config_entries.async_unload(device.config_entry.entry_id) + assert await hass.config_entries.async_unload(setup_config_entry.entry_id) assert not hass.data[AXIS_DOMAIN] -async def test_migrate_entry(hass): +@pytest.mark.parametrize("config_entry_version", [1]) +async def test_migrate_entry(hass, config_entry): """Test successful migration of entry data.""" - legacy_config = { - CONF_DEVICE: { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_PORT: 80, - }, - CONF_MAC: "00408C123456", - CONF_MODEL: "model", - CONF_NAME: "name", - } - entry = MockConfigEntry(domain=AXIS_DOMAIN, data=legacy_config) + assert config_entry.version == 1 - assert entry.data == legacy_config - assert entry.version == 1 - assert not entry.unique_id + mock_device = Mock() + mock_device.async_setup = AsyncMock() + mock_device.async_update_device_registry = AsyncMock() + mock_device.api.vapix.light_control = None + mock_device.api.vapix.params.image_format = None - # Create entity entry to migrate to new unique ID - registry = er.async_get(hass) - registry.async_get_or_create( - BINARY_SENSOR_DOMAIN, - AXIS_DOMAIN, - "00408C123456-vmd4-0", - suggested_object_id="vmd4", - config_entry=entry, - ) + with patch.object(axis, "get_axis_device"), patch.object( + axis, "AxisNetworkDevice" + ) as mock_device_class: + mock_device_class.return_value = mock_device - await entry.async_migrate(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) - assert entry.data == { - CONF_DEVICE: { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_PORT: 80, - }, - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_PORT: 80, - CONF_MAC: "00408C123456", - CONF_MODEL: "model", - CONF_NAME: "name", - } - assert entry.version == 2 # Keep version to support rollbacking - assert entry.unique_id == "00:40:8c:12:34:56" - - vmd4_entity = registry.async_get("binary_sensor.vmd4") - assert vmd4_entity.unique_id == "00:40:8c:12:34:56-vmd4-0" + assert hass.data[AXIS_DOMAIN] + assert config_entry.version == 3 diff --git a/tests/components/axis/test_light.py b/tests/components/axis/test_light.py index db7ca6921fb..7550e19e884 100644 --- a/tests/components/axis/test_light.py +++ b/tests/components/axis/test_light.py @@ -1,8 +1,10 @@ """Axis light platform tests.""" -from copy import deepcopy from unittest.mock import patch +import pytest +import respx + from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN from homeassistant.const import ( @@ -14,12 +16,7 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from .test_device import ( - API_DISCOVERY_RESPONSE, - LIGHT_CONTROL_RESPONSE, - NAME, - setup_axis_integration, -) +from .const import DEFAULT_HOST, NAME API_DISCOVERY_LIGHT_CONTROL = { "id": "light-control", @@ -28,6 +25,38 @@ API_DISCOVERY_LIGHT_CONTROL = { } +@pytest.fixture() +def light_control_items(): + """Available lights.""" + return [ + { + "lightID": "led0", + "lightType": "IR", + "enabled": True, + "synchronizeDayNightMode": True, + "lightState": False, + "automaticIntensityMode": False, + "automaticAngleOfIlluminationMode": False, + "nrOfLEDs": 1, + "error": False, + "errorInfo": "", + } + ] + + +@pytest.fixture(autouse=True) +def light_control_fixture(light_control_items): + """Light control mock response.""" + data = { + "apiVersion": "1.1", + "method": "getLightInformation", + "data": {"items": light_control_items}, + } + respx.post(f"http://{DEFAULT_HOST}:80/axis-cgi/lightcontrol.cgi").respond( + json=data, + ) + + async def test_platform_manually_configured(hass): """Test that nothing happens when platform is manually configured.""" assert await async_setup_component( @@ -37,28 +66,17 @@ async def test_platform_manually_configured(hass): assert AXIS_DOMAIN not in hass.data -async def test_no_lights(hass): +async def test_no_lights(hass, setup_config_entry): """Test that no light events in Axis results in no light entities.""" - await setup_axis_integration(hass) - assert not hass.states.async_entity_ids(LIGHT_DOMAIN) +@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_LIGHT_CONTROL]) +@pytest.mark.parametrize("light_control_items", [[]]) async def test_no_light_entity_without_light_control_representation( - hass, mock_rtsp_event + hass, setup_config_entry, mock_rtsp_event ): """Verify no lights entities get created without light control representation.""" - api_discovery = deepcopy(API_DISCOVERY_RESPONSE) - api_discovery["data"]["apiList"].append(API_DISCOVERY_LIGHT_CONTROL) - - light_control = deepcopy(LIGHT_CONTROL_RESPONSE) - light_control["data"]["items"] = [] - - with patch.dict(API_DISCOVERY_RESPONSE, api_discovery), patch.dict( - LIGHT_CONTROL_RESPONSE, light_control - ): - await setup_axis_integration(hass) - mock_rtsp_event( topic="tns1:Device/tnsaxis:Light/Status", data_type="state", @@ -71,20 +89,15 @@ async def test_no_light_entity_without_light_control_representation( assert not hass.states.async_entity_ids(LIGHT_DOMAIN) -async def test_lights(hass, mock_rtsp_event): +@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_LIGHT_CONTROL]) +async def test_lights(hass, setup_config_entry, mock_rtsp_event): """Test that lights are loaded properly.""" - api_discovery = deepcopy(API_DISCOVERY_RESPONSE) - api_discovery["data"]["apiList"].append(API_DISCOVERY_LIGHT_CONTROL) - - with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): - await setup_axis_integration(hass) - # Add light with patch( - "axis.light_control.LightControl.get_current_intensity", + "axis.vapix.interfaces.light_control.LightControl.get_current_intensity", return_value={"data": {"intensity": 100}}, ), patch( - "axis.light_control.LightControl.get_valid_intensity", + "axis.vapix.interfaces.light_control.LightControl.get_valid_intensity", return_value={"data": {"ranges": [{"high": 150}]}}, ): mock_rtsp_event( @@ -106,11 +119,11 @@ async def test_lights(hass, mock_rtsp_event): # Turn on, set brightness, light already on with patch( - "axis.light_control.LightControl.activate_light" + "axis.vapix.interfaces.light_control.LightControl.activate_light" ) as mock_activate, patch( - "axis.light_control.LightControl.set_manual_intensity" + "axis.vapix.interfaces.light_control.LightControl.set_manual_intensity" ) as mock_set_intensity, patch( - "axis.light_control.LightControl.get_current_intensity", + "axis.vapix.interfaces.light_control.LightControl.get_current_intensity", return_value={"data": {"intensity": 100}}, ): await hass.services.async_call( @@ -124,9 +137,9 @@ async def test_lights(hass, mock_rtsp_event): # Turn off with patch( - "axis.light_control.LightControl.deactivate_light" + "axis.vapix.interfaces.light_control.LightControl.deactivate_light" ) as mock_deactivate, patch( - "axis.light_control.LightControl.get_current_intensity", + "axis.vapix.interfaces.light_control.LightControl.get_current_intensity", return_value={"data": {"intensity": 100}}, ): await hass.services.async_call( @@ -152,11 +165,11 @@ async def test_lights(hass, mock_rtsp_event): # Turn on, set brightness with patch( - "axis.light_control.LightControl.activate_light" + "axis.vapix.interfaces.light_control.LightControl.activate_light" ) as mock_activate, patch( - "axis.light_control.LightControl.set_manual_intensity" + "axis.vapix.interfaces.light_control.LightControl.set_manual_intensity" ) as mock_set_intensity, patch( - "axis.light_control.LightControl.get_current_intensity", + "axis.vapix.interfaces.light_control.LightControl.get_current_intensity", return_value={"data": {"intensity": 100}}, ): await hass.services.async_call( @@ -170,9 +183,9 @@ async def test_lights(hass, mock_rtsp_event): # Turn off, light already off with patch( - "axis.light_control.LightControl.deactivate_light" + "axis.vapix.interfaces.light_control.LightControl.deactivate_light" ) as mock_deactivate, patch( - "axis.light_control.LightControl.get_current_intensity", + "axis.vapix.interfaces.light_control.LightControl.get_current_intensity", return_value={"data": {"intensity": 100}}, ): await hass.services.async_call( diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index 541c377d3ff..741aa6f1b1a 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -1,7 +1,8 @@ """Axis switch platform tests.""" -from copy import deepcopy -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock + +import pytest from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -14,12 +15,7 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from .test_device import ( - API_DISCOVERY_PORT_MANAGEMENT, - API_DISCOVERY_RESPONSE, - NAME, - setup_axis_integration, -) +from .const import API_DISCOVERY_PORT_MANAGEMENT, NAME async def test_platform_manually_configured(hass): @@ -31,17 +27,14 @@ async def test_platform_manually_configured(hass): assert AXIS_DOMAIN not in hass.data -async def test_no_switches(hass): +async def test_no_switches(hass, setup_config_entry): """Test that no output events in Axis results in no switch entities.""" - await setup_axis_integration(hass) - assert not hass.states.async_entity_ids(SWITCH_DOMAIN) -async def test_switches_with_port_cgi(hass, mock_rtsp_event): +async def test_switches_with_port_cgi(hass, setup_config_entry, mock_rtsp_event): """Test that switches are loaded properly using port.cgi.""" - config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] device.api.vapix.ports = {"0": AsyncMock(), "1": AsyncMock()} device.api.vapix.ports["0"].name = "Doorbell" @@ -94,16 +87,10 @@ async def test_switches_with_port_cgi(hass, mock_rtsp_event): device.api.vapix.ports["0"].open.assert_called_once() -async def test_switches_with_port_management( - hass, mock_axis_rtspclient, mock_rtsp_event -): +@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_PORT_MANAGEMENT]) +async def test_switches_with_port_management(hass, setup_config_entry, mock_rtsp_event): """Test that switches are loaded properly using port management.""" - api_discovery = deepcopy(API_DISCOVERY_RESPONSE) - api_discovery["data"]["apiList"].append(API_DISCOVERY_PORT_MANAGEMENT) - - with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): - config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + device = hass.data[AXIS_DOMAIN][setup_config_entry.entry_id] device.api.vapix.ports = {"0": AsyncMock(), "1": AsyncMock()} device.api.vapix.ports["0"].name = "Doorbell" @@ -139,6 +126,19 @@ async def test_switches_with_port_management( assert relay_0.state == STATE_OFF assert relay_0.name == f"{NAME} Doorbell" + # State update + + mock_rtsp_event( + topic="tns1:Device/Trigger/Relay", + data_type="LogicalState", + data_value="active", + source_name="RelayToken", + source_idx="0", + ) + await hass.async_block_till_done() + + assert hass.states.get(f"{SWITCH_DOMAIN}.{NAME}_relay_1").state == STATE_ON + await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, diff --git a/tests/components/backup/test_init.py b/tests/components/backup/test_init.py index 0087b35e2bc..86055889da5 100644 --- a/tests/components/backup/test_init.py +++ b/tests/components/backup/test_init.py @@ -16,9 +16,9 @@ async def test_setup_with_hassio( """Test the setup of the integration with hassio enabled.""" assert not await setup_backup_integration(hass=hass, with_hassio=True) assert ( - "The backup integration is not supported on this installation method, please remove it from your configuration" - in caplog.text - ) + "The backup integration is not supported on this installation method, please" + " remove it from your configuration" + ) in caplog.text async def test_create_service( diff --git a/tests/components/backup/test_manager.py b/tests/components/backup/test_manager.py index 7edd64e0cb0..9379be1e22d 100644 --- a/tests/components/backup/test_manager.py +++ b/tests/components/backup/test_manager.py @@ -154,8 +154,8 @@ async def test_getting_backup_that_does_not_exist( assert ( f"Removing tracked backup ({TEST_BACKUP.slug}) that " - f"does not exists on the expected path {TEST_BACKUP.path}" in caplog.text - ) + f"does not exists on the expected path {TEST_BACKUP.path}" + ) in caplog.text async def test_generate_backup_when_backing_up(hass: HomeAssistant) -> None: diff --git a/tests/components/balboa/test_climate.py b/tests/components/balboa/test_climate.py index 571ec1f432e..50ca33c5209 100644 --- a/tests/components/balboa/test_climate.py +++ b/tests/components/balboa/test_climate.py @@ -21,7 +21,7 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) -from homeassistant.const import ATTR_TEMPERATURE, TEMP_FAHRENHEIT +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from . import init_integration @@ -130,7 +130,9 @@ async def test_spa_temperature(hass: HomeAssistant, client: MagicMock) -> None: async def test_spa_temperature_unit(hass: HomeAssistant, client: MagicMock) -> None: """Test temperature unit conversions.""" - with patch.object(hass.config.units, "temperature_unit", TEMP_FAHRENHEIT): + with patch.object( + hass.config.units, "temperature_unit", UnitOfTemperature.FAHRENHEIT + ): config_entry = await init_integration(hass) state = await _patch_spa_settemp(hass, config_entry, 0, 15.4, client) diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 4dd42705026..8a29ba1036f 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -678,7 +678,10 @@ async def test_observed_entities(hass): }, { "platform": "template", - "value_template": "{{is_state('sensor.test_monitored1','on') and is_state('sensor.test_monitored','off')}}", + "value_template": ( + "{{is_state('sensor.test_monitored1','on') and" + " is_state('sensor.test_monitored','off')}}" + ), "prob_given_true": 0.9, "prob_given_false": 0.1, }, @@ -734,7 +737,10 @@ async def test_state_attributes_are_serializable(hass): }, { "platform": "template", - "value_template": "{{is_state('sensor.test_monitored1','on') and is_state('sensor.test_monitored','off')}}", + "value_template": ( + "{{is_state('sensor.test_monitored1','on') and" + " is_state('sensor.test_monitored','off')}}" + ), "prob_given_true": 0.9, "prob_given_false": 0.1, }, diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index dfb2576d262..01c300c9ad4 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -557,9 +557,9 @@ async def test_wlightbox_on_effect(wlightbox, hass): ) assert ( - f"Turning on with effect '{feature_mock.full_name}' failed: NOT IN LIST not in effect list." - in str(info.value) - ) + f"Turning on with effect '{feature_mock.full_name}' failed: " + "NOT IN LIST not in effect list." + ) in str(info.value) await hass.services.async_call( "light", diff --git a/tests/components/blebox/test_sensor.py b/tests/components/blebox/test_sensor.py index dd54e5272e2..4645a853071 100644 --- a/tests/components/blebox/test_sensor.py +++ b/tests/components/blebox/test_sensor.py @@ -11,7 +11,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.helpers import device_registry as dr @@ -66,7 +66,7 @@ async def test_init(tempsensor, hass): assert state.name == "tempSensor-0.temperature" assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert state.state == STATE_UNKNOWN device_registry = dr.async_get(hass) @@ -91,7 +91,7 @@ async def test_update(tempsensor, hass): await async_setup_entity(hass, entity_id) state = hass.states.get(entity_id) - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert state.state == "25.18" diff --git a/tests/components/blueprint/test_importer.py b/tests/components/blueprint/test_importer.py index 46a98840a80..eaee132ea00 100644 --- a/tests/components/blueprint/test_importer.py +++ b/tests/components/blueprint/test_importer.py @@ -36,7 +36,10 @@ COMMUNITY_POST_INPUTS = { }, "force_brightness": { "name": "Force turn on brightness", - "description": 'Force the brightness to the set level below, when the "on" button on the remote is pushed and lights turn on.\n', + "description": ( + 'Force the brightness to the set level below, when the "on" button on the' + " remote is pushed and lights turn on.\n" + ), "default": False, "selector": {"boolean": {}}, }, diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index 589025a08ba..56c2880fc75 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -256,10 +256,10 @@ async def test_domain_blueprints_add_blueprint(domain_bps, blueprint_1): with patch.object(domain_bps, "_create_file") as create_file_mock: # Should add extension when not present. await domain_bps.async_add_blueprint(blueprint_1, "something") - assert create_file_mock.call_args[0][1] == ("something.yaml") + assert create_file_mock.call_args[0][1] == "something.yaml" await domain_bps.async_add_blueprint(blueprint_1, "something2.yaml") - assert create_file_mock.call_args[0][1] == ("something2.yaml") + assert create_file_mock.call_args[0][1] == "something2.yaml" # Should be in cache. with patch.object(domain_bps, "_load_blueprint") as mock_load: diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index 05c0e4adc4c..4b65cafc950 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -150,9 +150,21 @@ async def test_save_blueprint(hass, aioclient_mock, hass_ws_client): output_yaml = write_mock.call_args[0][0] assert output_yaml in ( # pure python dumper will quote the value after !input - "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n selector:\n text: {}\n service_to_call:\n a_number:\n selector:\n number:\n mode: box\n step: 1.0\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input 'trigger_event'\naction:\n service: !input 'service_to_call'\n entity_id: light.kitchen\n" + "blueprint:\n name: Call service based on event\n domain: automation\n " + " input:\n trigger_event:\n selector:\n text: {}\n " + " service_to_call:\n a_number:\n selector:\n number:\n " + " mode: box\n step: 1.0\n source_url:" + " https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n" + " platform: event\n event_type: !input 'trigger_event'\naction:\n " + " service: !input 'service_to_call'\n entity_id: light.kitchen\n" # c dumper will not quote the value after !input - "blueprint:\n name: Call service based on event\n domain: automation\n input:\n trigger_event:\n selector:\n text: {}\n service_to_call:\n a_number:\n selector:\n number:\n mode: box\n step: 1.0\n source_url: https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n platform: event\n event_type: !input trigger_event\naction:\n service: !input service_to_call\n entity_id: light.kitchen\n" + "blueprint:\n name: Call service based on event\n domain: automation\n " + " input:\n trigger_event:\n selector:\n text: {}\n " + " service_to_call:\n a_number:\n selector:\n number:\n " + " mode: box\n step: 1.0\n source_url:" + " https://github.com/balloob/home-assistant-config/blob/main/blueprints/automation/motion_light.yaml\ntrigger:\n" + " platform: event\n event_type: !input trigger_event\naction:\n service:" + " !input service_to_call\n entity_id: light.kitchen\n" ) # Make sure ita parsable and does not raise assert len(parse_yaml(output_yaml)) > 1 diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index ffa353fd0c4..59c5cc822df 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -186,3 +186,18 @@ def one_adapter_old_bluez(): }, ): yield + + +@pytest.fixture(name="disable_new_discovery_flows") +def disable_new_discovery_flows_fixture(): + """Fixture that disables new discovery flows. + + We want to disable new discovery flows as we are testing the + BluetoothManager and not the discovery flows. This fixture + will patch the discovery_flow.async_create_flow method to + ensure we do not load other integrations. + """ + with patch( + "homeassistant.components.bluetooth.manager.discovery_flow.async_create_flow" + ) as mock_create_flow: + yield mock_create_flow diff --git a/tests/components/bluetooth/test_advertisement_tracker.py b/tests/components/bluetooth/test_advertisement_tracker.py index d4255a8cc91..6e94e58cf1c 100644 --- a/tests/components/bluetooth/test_advertisement_tracker.py +++ b/tests/components/bluetooth/test_advertisement_tracker.py @@ -14,6 +14,7 @@ from homeassistant.components.bluetooth.advertisement_tracker import ( ADVERTISING_TIMES_NEEDED, ) from homeassistant.components.bluetooth.const import ( + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, ) @@ -370,7 +371,21 @@ async def test_advertisment_interval_longer_than_adapter_stack_timeout_adapter_c ) await hass.async_block_till_done() - assert switchbot_device_went_unavailable is True + assert switchbot_device_went_unavailable is False + + # Now that the scanner is gone we should go back to the stack default timeout + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now + UNAVAILABLE_TRACK_SECONDS, + ): + async_fire_time_changed( + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS), + ) + await hass.async_block_till_done() + + assert switchbot_device_went_unavailable is False switchbot_device_unavailable_cancel() diff --git a/tests/components/bluetooth/test_api.py b/tests/components/bluetooth/test_api.py index acb09c22ba7..c875710d8e5 100644 --- a/tests/components/bluetooth/test_api.py +++ b/tests/components/bluetooth/test_api.py @@ -1,10 +1,18 @@ """Tests for the Bluetooth integration API.""" -from homeassistant.components import bluetooth -from homeassistant.components.bluetooth import async_scanner_by_source +from bleak.backends.scanner import AdvertisementData, BLEDevice -from . import FakeScanner +from homeassistant.components import bluetooth +from homeassistant.components.bluetooth import ( + BaseHaRemoteScanner, + BaseHaScanner, + HaBluetoothConnector, + async_scanner_by_source, + async_scanner_devices_by_address, +) + +from . import FakeScanner, MockBleakClient, _get_manager, generate_advertisement_data async def test_scanner_by_source(hass, enable_bluetooth): @@ -16,3 +24,116 @@ async def test_scanner_by_source(hass, enable_bluetooth): assert async_scanner_by_source(hass, "hci2") is hci2_scanner cancel_hci2() assert async_scanner_by_source(hass, "hci2") is None + + +async def test_async_scanner_devices_by_address_connectable(hass, enable_bluetooth): + """Test getting scanner devices by address with connectable devices.""" + manager = _get_manager() + + class FakeInjectableScanner(BaseHaRemoteScanner): + def inject_advertisement( + self, device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Inject an advertisement.""" + self._async_on_advertisement( + device.address, + advertisement_data.rssi, + device.name, + advertisement_data.service_uuids, + advertisement_data.service_data, + advertisement_data.manufacturer_data, + advertisement_data.tx_power, + {"scanner_specific_data": "test"}, + ) + + new_info_callback = manager.scanner_adv_received + connector = ( + HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), + ) + scanner = FakeInjectableScanner( + hass, "esp32", "esp32", new_info_callback, connector, False + ) + unsetup = scanner.async_setup() + cancel = manager.async_register_scanner(scanner, True) + switchbot_device = BLEDevice( + "44:44:33:11:23:45", + "wohand", + {}, + rssi=-100, + ) + switchbot_device_adv = generate_advertisement_data( + local_name="wohand", + service_uuids=["050a021a-0000-1000-8000-00805f9b34fb"], + service_data={"050a021a-0000-1000-8000-00805f9b34fb": b"\n\xff"}, + manufacturer_data={1: b"\x01"}, + rssi=-100, + ) + scanner.inject_advertisement(switchbot_device, switchbot_device_adv) + assert async_scanner_devices_by_address( + hass, switchbot_device.address, connectable=True + ) == async_scanner_devices_by_address(hass, "44:44:33:11:23:45", connectable=False) + devices = async_scanner_devices_by_address( + hass, switchbot_device.address, connectable=False + ) + assert len(devices) == 1 + assert devices[0].scanner == scanner + assert devices[0].ble_device.name == switchbot_device.name + assert devices[0].advertisement.local_name == switchbot_device_adv.local_name + unsetup() + cancel() + + +async def test_async_scanner_devices_by_address_non_connectable(hass, enable_bluetooth): + """Test getting scanner devices by address with non-connectable devices.""" + manager = _get_manager() + switchbot_device = BLEDevice( + "44:44:33:11:23:45", + "wohand", + {}, + rssi=-100, + ) + switchbot_device_adv = generate_advertisement_data( + local_name="wohand", + service_uuids=["050a021a-0000-1000-8000-00805f9b34fb"], + service_data={"050a021a-0000-1000-8000-00805f9b34fb": b"\n\xff"}, + manufacturer_data={1: b"\x01"}, + rssi=-100, + ) + + class FakeStaticScanner(BaseHaScanner): + @property + def discovered_devices(self) -> list[BLEDevice]: + """Return a list of discovered devices.""" + return [switchbot_device] + + @property + def discovered_devices_and_advertisement_data( + self, + ) -> dict[str, tuple[BLEDevice, AdvertisementData]]: + """Return a list of discovered devices and their advertisement data.""" + return {switchbot_device.address: (switchbot_device, switchbot_device_adv)} + + connector = ( + HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), + ) + scanner = FakeStaticScanner(hass, "esp32", "esp32", connector) + cancel = manager.async_register_scanner(scanner, False) + + assert scanner.discovered_devices_and_advertisement_data == { + switchbot_device.address: (switchbot_device, switchbot_device_adv) + } + + assert ( + async_scanner_devices_by_address( + hass, switchbot_device.address, connectable=True + ) + == [] + ) + devices = async_scanner_devices_by_address( + hass, switchbot_device.address, connectable=False + ) + assert len(devices) == 1 + assert devices[0].scanner == scanner + assert devices[0].ble_device.name == switchbot_device.name + assert devices[0].advertisement.local_name == switchbot_device_adv.local_name + cancel() diff --git a/tests/components/bluetooth/test_base_scanner.py b/tests/components/bluetooth/test_base_scanner.py index 53c2716b0bd..5c6bbccd443 100644 --- a/tests/components/bluetooth/test_base_scanner.py +++ b/tests/components/bluetooth/test_base_scanner.py @@ -14,10 +14,15 @@ from homeassistant.components.bluetooth import ( HaBluetoothConnector, storage, ) +from homeassistant.components.bluetooth.advertisement_tracker import ( + TRACKER_BUFFERING_WOBBLE_SECONDS, +) from homeassistant.components.bluetooth.const import ( CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, + UNAVAILABLE_TRACK_SECONDS, ) +from homeassistant.core import callback from homeassistant.helpers.json import json_loads from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -239,7 +244,9 @@ async def test_remote_scanner_expires_non_connectable(hass, enable_bluetooth): > CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS ) - # The connectable timeout is not used for non connectable devices + # The connectable timeout is used for all devices + # as the manager takes care of availability and the scanner + # if only concerned about making a connection expire_monotonic = ( start_time_monotonic + CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS @@ -255,11 +262,9 @@ async def test_remote_scanner_expires_non_connectable(hass, enable_bluetooth): async_fire_time_changed(hass, expire_utc) await hass.async_block_till_done() - assert len(scanner.discovered_devices) == 1 - assert len(scanner.discovered_devices_and_advertisement_data) == 1 + assert len(scanner.discovered_devices) == 0 + assert len(scanner.discovered_devices_and_advertisement_data) == 0 - # The non connectable timeout is used for non connectable devices - # which is always longer than the connectable timeout expire_monotonic = ( start_time_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 ) @@ -340,7 +345,9 @@ async def test_base_scanner_connecting_behavior(hass, enable_bluetooth): unsetup() -async def test_restore_history_remote_adapter(hass, hass_storage): +async def test_restore_history_remote_adapter( + hass, hass_storage, disable_new_discovery_flows +): """Test we can restore history for a remote adapter.""" data = hass_storage[storage.REMOTE_SCANNER_STORAGE_KEY] = json_loads( @@ -394,3 +401,119 @@ async def test_restore_history_remote_adapter(hass, hass_storage): cancel() unsetup() + + +async def test_device_with_ten_minute_advertising_interval( + hass, caplog, enable_bluetooth +): + """Test a device with a 10 minute advertising interval.""" + manager = _get_manager() + + bparasite_device = BLEDevice( + "44:44:33:11:23:45", + "bparasite", + {}, + rssi=-100, + ) + bparasite_device_adv = generate_advertisement_data( + local_name="bparasite", + service_uuids=[], + manufacturer_data={1: b"\x01"}, + rssi=-100, + ) + + class FakeScanner(BaseHaRemoteScanner): + def inject_advertisement( + self, device: BLEDevice, advertisement_data: AdvertisementData + ) -> None: + """Inject an advertisement.""" + self._async_on_advertisement( + device.address, + advertisement_data.rssi, + device.name, + advertisement_data.service_uuids, + advertisement_data.service_data, + advertisement_data.manufacturer_data, + advertisement_data.tx_power, + {"scanner_specific_data": "test"}, + ) + + new_info_callback = manager.scanner_adv_received + connector = ( + HaBluetoothConnector(MockBleakClient, "mock_bleak_client", lambda: False), + ) + scanner = FakeScanner(hass, "esp32", "esp32", new_info_callback, connector, False) + unsetup = scanner.async_setup() + cancel = manager.async_register_scanner(scanner, True) + + monotonic_now = time.monotonic() + new_time = monotonic_now + bparasite_device_went_unavailable = False + + @callback + def _bparasite_device_unavailable_callback(_address: str) -> None: + """Barasite device unavailable callback.""" + nonlocal bparasite_device_went_unavailable + bparasite_device_went_unavailable = True + + advertising_interval = 60 * 10 + + bparasite_device_unavailable_cancel = bluetooth.async_track_unavailable( + hass, + _bparasite_device_unavailable_callback, + bparasite_device.address, + connectable=False, + ) + + for _ in range(0, 20): + new_time += advertising_interval + with patch( + "homeassistant.components.bluetooth.base_scanner.MONOTONIC_TIME", + return_value=new_time, + ): + scanner.inject_advertisement(bparasite_device, bparasite_device_adv) + + future_time = new_time + assert ( + bluetooth.async_address_present(hass, bparasite_device.address, False) is True + ) + assert bparasite_device_went_unavailable is False + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=new_time, + ): + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=future_time)) + await hass.async_block_till_done() + + assert bparasite_device_went_unavailable is False + + missed_advertisement_future_time = ( + future_time + advertising_interval + TRACKER_BUFFERING_WOBBLE_SECONDS + 1 + ) + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=missed_advertisement_future_time, + ), patch( + "homeassistant.components.bluetooth.base_scanner.MONOTONIC_TIME", + return_value=missed_advertisement_future_time, + ): + # Fire once for the scanner to expire the device + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + ) + await hass.async_block_till_done() + # Fire again for the manager to expire the device + async_fire_time_changed( + hass, dt_util.utcnow() + timedelta(seconds=missed_advertisement_future_time) + ) + await hass.async_block_till_done() + + assert ( + bluetooth.async_address_present(hass, bparasite_device.address, False) is False + ) + assert bparasite_device_went_unavailable is True + bparasite_device_unavailable_cancel() + + cancel() + unsetup() diff --git a/tests/components/bluetooth/test_diagnostics.py b/tests/components/bluetooth/test_diagnostics.py index a40f4b5c024..f4c9beafd4a 100644 --- a/tests/components/bluetooth/test_diagnostics.py +++ b/tests/components/bluetooth/test_diagnostics.py @@ -104,10 +104,10 @@ async def test_diagnostics( "org.bluez": { "/org/bluez/hci0": { "org.bluez.Adapter1": { - "Alias": "BlueZ " "5.63", + "Alias": "BlueZ 5.63", "Discovering": False, "Modalias": "usb:v1D6Bp0246d0540", - "Name": "BlueZ " "5.63", + "Name": "BlueZ 5.63", }, "org.bluez.AdvertisementMonitorManager1": { "SupportedFeatures": [], @@ -323,7 +323,7 @@ async def test_diagnostics_macos( "address": "44:44:33:11:23:45", "advertisement": [ "wohand", - {"1": {"__type": "", "repr": "b'\\x01'"}}, + {"1": {"__type": "", "repr": "b'\\x01'"}}, {}, [], -127, @@ -331,12 +331,12 @@ async def test_diagnostics_macos( [[]], ], "device": { - "__type": "", - "repr": "BLEDevice(44:44:33:11:23:45, " "wohand)", + "__type": "", + "repr": "BLEDevice(44:44:33:11:23:45, wohand)", }, "connectable": True, "manufacturer_data": { - "1": {"__type": "", "repr": "b'\\x01'"} + "1": {"__type": "", "repr": "b'\\x01'"} }, "name": "wohand", "rssi": -127, @@ -351,7 +351,7 @@ async def test_diagnostics_macos( "address": "44:44:33:11:23:45", "advertisement": [ "wohand", - {"1": {"__type": "", "repr": "b'\\x01'"}}, + {"1": {"__type": "", "repr": "b'\\x01'"}}, {}, [], -127, @@ -359,12 +359,12 @@ async def test_diagnostics_macos( [[]], ], "device": { - "__type": "", - "repr": "BLEDevice(44:44:33:11:23:45, " "wohand)", + "__type": "", + "repr": "BLEDevice(44:44:33:11:23:45, wohand)", }, "connectable": True, "manufacturer_data": { - "1": {"__type": "", "repr": "b'\\x01'"} + "1": {"__type": "", "repr": "b'\\x01'"} }, "name": "wohand", "rssi": -127, @@ -384,7 +384,7 @@ async def test_diagnostics_macos( "wohand", { "1": { - "__type": "", + "__type": "", "repr": "b'\\x01'", } }, @@ -515,7 +515,7 @@ async def test_diagnostics_remote_adapter( "address": "44:44:33:11:23:45", "advertisement": [ "wohand", - {"1": {"__type": "", "repr": "b'\\x01'"}}, + {"1": {"__type": "", "repr": "b'\\x01'"}}, {}, [], -127, @@ -524,11 +524,11 @@ async def test_diagnostics_remote_adapter( ], "connectable": False, "device": { - "__type": "", - "repr": "BLEDevice(44:44:33:11:23:45, " "wohand)", + "__type": "", + "repr": "BLEDevice(44:44:33:11:23:45, wohand)", }, "manufacturer_data": { - "1": {"__type": "", "repr": "b'\\x01'"} + "1": {"__type": "", "repr": "b'\\x01'"} }, "name": "wohand", "rssi": -127, @@ -543,7 +543,7 @@ async def test_diagnostics_remote_adapter( "address": "44:44:33:11:23:45", "advertisement": [ "wohand", - {"1": {"__type": "", "repr": "b'\\x01'"}}, + {"1": {"__type": "", "repr": "b'\\x01'"}}, {}, [], -127, @@ -552,11 +552,11 @@ async def test_diagnostics_remote_adapter( ], "connectable": True, "device": { - "__type": "", - "repr": "BLEDevice(44:44:33:11:23:45, " "wohand)", + "__type": "", + "repr": "BLEDevice(44:44:33:11:23:45, wohand)", }, "manufacturer_data": { - "1": {"__type": "", "repr": "b'\\x01'"} + "1": {"__type": "", "repr": "b'\\x01'"} }, "name": "wohand", "rssi": -127, @@ -600,7 +600,7 @@ async def test_diagnostics_remote_adapter( "wohand", { "1": { - "__type": "", + "__type": "", "repr": "b'\\x01'", } }, diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index 332e211571b..7a0a7de8442 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -492,7 +492,9 @@ async def test_discovery_match_by_name_connectable_false( qingping_adv = generate_advertisement_data( local_name="Qingping Motion & Light", service_data={ - "0000fdcd-0000-1000-8000-00805f9b34fb": b"H\x12\xcd\xd5`4-X\x08\x04\x01\xe8\x00\x00\x0f\x01{" + "0000fdcd-0000-1000-8000-00805f9b34fb": ( + b"H\x12\xcd\xd5`4-X\x08\x04\x01\xe8\x00\x00\x0f\x01{" + ) }, ) @@ -508,7 +510,9 @@ async def test_discovery_match_by_name_connectable_false( qingping_adv_with_better_rssi = generate_advertisement_data( local_name="Qingping Motion & Light", service_data={ - "0000fdcd-0000-1000-8000-00805f9b34fb": b"H\x12\xcd\xd5`4-X\x08\x04\x01\xe8\x00\x00\x0f\x02{" + "0000fdcd-0000-1000-8000-00805f9b34fb": ( + b"H\x12\xcd\xd5`4-X\x08\x04\x01\xe8\x00\x00\x0f\x02{" + ) }, rssi=-30, ) @@ -832,7 +836,9 @@ async def test_discovery_match_by_service_data_uuid_when_format_changes( qingping_format_adv = generate_advertisement_data( local_name="Qingping Temp RH M", service_data={ - "0000fdcd-0000-1000-8000-00805f9b34fb": b"\x08\x16\xa7%\x144-X\x01\x04\xdb\x00\xa6\x01\x02\x01d" + "0000fdcd-0000-1000-8000-00805f9b34fb": ( + b"\x08\x16\xa7%\x144-X\x01\x04\xdb\x00\xa6\x01\x02\x01d" + ) }, ) # 1st discovery should not generate a flow because the diff --git a/tests/components/bluetooth/test_manager.py b/tests/components/bluetooth/test_manager.py index d0ae23a5226..12e48313332 100644 --- a/tests/components/bluetooth/test_manager.py +++ b/tests/components/bluetooth/test_manager.py @@ -282,7 +282,9 @@ async def test_switching_adapters_based_on_stale( ) -async def test_restore_history_from_dbus(hass, one_adapter): +async def test_restore_history_from_dbus( + hass, one_adapter, disable_new_discovery_flows +): """Test we can restore history from dbus.""" address = "AA:BB:CC:CC:CC:FF" @@ -304,7 +306,7 @@ async def test_restore_history_from_dbus(hass, one_adapter): async def test_restore_history_from_dbus_and_remote_adapters( - hass, one_adapter, hass_storage + hass, one_adapter, hass_storage, disable_new_discovery_flows ): """Test we can restore history from dbus along with remote adapters.""" address = "AA:BB:CC:CC:CC:FF" @@ -337,10 +339,11 @@ async def test_restore_history_from_dbus_and_remote_adapters( assert ( bluetooth.async_ble_device_from_address(hass, "EB:0B:36:35:6F:A4") is not None ) + assert disable_new_discovery_flows.call_count > 1 async def test_restore_history_from_dbus_and_corrupted_remote_adapters( - hass, one_adapter, hass_storage + hass, one_adapter, hass_storage, disable_new_discovery_flows ): """Test we can restore history from dbus when the remote adapters data is corrupted.""" address = "AA:BB:CC:CC:CC:FF" @@ -371,6 +374,7 @@ async def test_restore_history_from_dbus_and_corrupted_remote_adapters( assert bluetooth.async_ble_device_from_address(hass, address) is not None assert bluetooth.async_ble_device_from_address(hass, "EB:0B:36:35:6F:A4") is None + assert disable_new_discovery_flows.call_count >= 1 async def test_switching_adapters_based_on_rssi_connectable_to_non_connectable( diff --git a/tests/components/bluetooth/test_passive_update_coordinator.py b/tests/components/bluetooth/test_passive_update_coordinator.py index fb80bb7cec4..e88d60a669f 100644 --- a/tests/components/bluetooth/test_passive_update_coordinator.py +++ b/tests/components/bluetooth/test_passive_update_coordinator.py @@ -3,15 +3,16 @@ from __future__ import annotations from datetime import timedelta import logging +import time from typing import Any from unittest.mock import MagicMock, patch from homeassistant.components.bluetooth import ( DOMAIN, + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, BluetoothChange, BluetoothScanningMode, ) -from homeassistant.components.bluetooth.const import UNAVAILABLE_TRACK_SECONDS from homeassistant.components.bluetooth.passive_update_coordinator import ( PassiveBluetoothCoordinatorEntity, PassiveBluetoothDataUpdateCoordinator, @@ -126,6 +127,7 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( hass, mock_bleak_scanner_start, mock_bluetooth_adapters ): """Test that the coordinator goes unavailable when the bluetooth stack no longer sees the device.""" + start_monotonic = time.monotonic() with patch( "bleak.BleakScanner.discovered_devices_and_advertisement_data", # Must patch before we setup {"44:44:33:11:23:45": (MagicMock(address="44:44:33:11:23:45"), MagicMock())}, @@ -146,9 +148,16 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) assert coordinator.available is True - with patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1), ) await hass.async_block_till_done() assert coordinator.available is False @@ -156,9 +165,16 @@ async def test_unavailable_callbacks_mark_the_coordinator_unavailable( inject_bluetooth_service_info(hass, GENERIC_BLUETOOTH_SERVICE_INFO) assert coordinator.available is True - with patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 2 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): async_fire_time_changed( - hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) + hass, + dt_util.utcnow() + + timedelta(seconds=FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 2), ) await hass.async_block_till_done() assert coordinator.available is False diff --git a/tests/components/bluetooth/test_passive_update_processor.py b/tests/components/bluetooth/test_passive_update_processor.py index e72efd565de..9f72146f364 100644 --- a/tests/components/bluetooth/test_passive_update_processor.py +++ b/tests/components/bluetooth/test_passive_update_processor.py @@ -15,6 +15,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.components.bluetooth import ( DOMAIN, + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS, BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak, @@ -28,7 +29,7 @@ from homeassistant.components.bluetooth.passive_update_processor import ( PassiveBluetoothProcessorEntity, ) from homeassistant.components.sensor import SensorDeviceClass, SensorEntityDescription -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import UnitOfTemperature from homeassistant.core import CoreState, callback from homeassistant.helpers.entity import DeviceInfo from homeassistant.setup import async_setup_component @@ -86,7 +87,7 @@ GENERIC_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( entity_descriptions={ PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( key="temperature", - native_unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( @@ -200,6 +201,8 @@ async def test_unavailable_after_no_data( hass, mock_bleak_scanner_start, mock_bluetooth_adapters ): """Test that the coordinator is unavailable after no data for a while.""" + start_monotonic = time.monotonic() + with patch( "bleak.BleakScanner.discovered_devices_and_advertisement_data", # Must patch before we setup {"44:44:33:11:23:45": (MagicMock(address="44:44:33:11:23:45"), MagicMock())}, @@ -265,7 +268,12 @@ async def test_unavailable_after_no_data( assert len(mock_add_entities.mock_calls) == 1 assert coordinator.available is True assert processor.available is True - with patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 1 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) ) @@ -279,7 +287,12 @@ async def test_unavailable_after_no_data( assert coordinator.available is True assert processor.available is True - with patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): + monotonic_now = start_monotonic + FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS + 2 + + with patch( + "homeassistant.components.bluetooth.manager.MONOTONIC_TIME", + return_value=monotonic_now, + ), patch_all_discovered_devices([MagicMock(address="44:44:33:11:23:45")]): async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=UNAVAILABLE_TRACK_SECONDS) ) @@ -885,7 +898,7 @@ NO_DEVICES_PASSIVE_BLUETOOTH_DATA_UPDATE = PassiveBluetoothDataUpdate( PassiveBluetoothEntityKey("temperature", None): SensorEntityDescription( key="temperature", name="Temperature", - native_unit_of_measurement=TEMP_CELSIUS, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, ), PassiveBluetoothEntityKey("pressure", None): SensorEntityDescription( diff --git a/tests/components/bluetooth/test_wrappers.py b/tests/components/bluetooth/test_wrappers.py index dd051ebb1fe..b2be6bf7280 100644 --- a/tests/components/bluetooth/test_wrappers.py +++ b/tests/components/bluetooth/test_wrappers.py @@ -1,8 +1,7 @@ """Tests for the Bluetooth integration.""" - +from __future__ import annotations from collections.abc import Callable -from typing import Union from unittest.mock import patch import bleak @@ -66,14 +65,14 @@ class FakeScanner(BaseHaRemoteScanner): class BaseFakeBleakClient: """Base class for fake bleak clients.""" - def __init__(self, address_or_ble_device: Union[BLEDevice, str], **kwargs): + def __init__(self, address_or_ble_device: BLEDevice | str, **kwargs): """Initialize the fake bleak client.""" self._device_path = "/dev/test" self._device = address_or_ble_device self._address = address_or_ble_device.address async def disconnect(self, *args, **kwargs): - """Disconnect.""" "" + """Disconnect.""" async def get_services(self, *args, **kwargs): """Get services.""" diff --git a/tests/components/bluetooth_adapters/__init__.py b/tests/components/bluetooth_adapters/__init__.py new file mode 100644 index 00000000000..0681ccff51e --- /dev/null +++ b/tests/components/bluetooth_adapters/__init__.py @@ -0,0 +1 @@ +"""Tests for the Bluetooth Adapters integration.""" diff --git a/tests/components/bluetooth_adapters/conftest.py b/tests/components/bluetooth_adapters/conftest.py new file mode 100644 index 00000000000..9e56959209e --- /dev/null +++ b/tests/components/bluetooth_adapters/conftest.py @@ -0,0 +1,8 @@ +"""bluetooth_adapters session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/bluetooth_adapters/test_init.py b/tests/components/bluetooth_adapters/test_init.py new file mode 100644 index 00000000000..e9874922065 --- /dev/null +++ b/tests/components/bluetooth_adapters/test_init.py @@ -0,0 +1,10 @@ +"""Test the Bluetooth Adapters setup.""" + +from homeassistant.components.bluetooth_adapters import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def test_setup(hass: HomeAssistant) -> None: + """Ensure we can setup.""" + assert await async_setup_component(hass, DOMAIN, {}) diff --git a/tests/components/bmw_connected_drive/__init__.py b/tests/components/bmw_connected_drive/__init__.py index 81b3bb9fff3..ed31b4308b8 100644 --- a/tests/components/bmw_connected_drive/__init__.py +++ b/tests/components/bmw_connected_drive/__init__.py @@ -3,7 +3,10 @@ import json from pathlib import Path -from bimmer_connected.account import MyBMWAccount +from bimmer_connected.api.authentication import MyBMWAuthentication +from bimmer_connected.const import VEHICLE_STATE_URL, VEHICLES_URL +import httpx +import respx from homeassistant import config_entries from homeassistant.components.bmw_connected_drive.const import ( @@ -13,7 +16,6 @@ from homeassistant.components.bmw_connected_drive.const import ( ) from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from homeassistant.core import HomeAssistant -from homeassistant.util.dt import utcnow from tests.common import MockConfigEntry, get_fixture_path, load_fixture @@ -39,49 +41,46 @@ FIXTURE_CONFIG_ENTRY = { "unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}", } +FIXTURE_PATH = Path(get_fixture_path("", integration=BMW_DOMAIN)) -async def mock_vehicles_from_fixture(account: MyBMWAccount) -> None: - """Load MyBMWVehicle from fixtures and add them to the account.""" - fixture_path = Path(get_fixture_path("", integration=BMW_DOMAIN)) +def vehicles_sideeffect(request: httpx.Request) -> httpx.Response: + """Return /vehicles response based on x-user-agent.""" + x_user_agent = request.headers.get("x-user-agent", "").split(";") + brand = x_user_agent[1] + vehicles = [] + for vehicle_file in FIXTURE_PATH.rglob(f"vehicles_v2_{brand}_*.json"): + vehicles.extend(json.loads(load_fixture(vehicle_file, integration=BMW_DOMAIN))) + return httpx.Response(200, json=vehicles) - fixture_vehicles_bmw = list(fixture_path.rglob("vehicles_v2_bmw_*.json")) - fixture_vehicles_mini = list(fixture_path.rglob("vehicles_v2_mini_*.json")) - # Load vehicle base lists as provided by vehicles/v2 API - vehicles = { - "bmw": [ - vehicle - for bmw_file in fixture_vehicles_bmw - for vehicle in json.loads(load_fixture(bmw_file, integration=BMW_DOMAIN)) - ], - "mini": [ - vehicle - for mini_file in fixture_vehicles_mini - for vehicle in json.loads(load_fixture(mini_file, integration=BMW_DOMAIN)) - ], - } - fetched_at = utcnow() - - # Create a vehicle with base + specific state as provided by state/VIN API - for vehicle_base in [vehicle for brand in vehicles.values() for vehicle in brand]: - vehicle_state_path = ( - Path("vehicles") - / vehicle_base["attributes"]["bodyType"] - / f"state_{vehicle_base['vin']}_0.json" - ) - vehicle_state = json.loads( - load_fixture( - vehicle_state_path, - integration=BMW_DOMAIN, - ) +def vehicle_state_sideeffect(request: httpx.Request) -> httpx.Response: + """Return /vehicles/state response.""" + state_file = next(FIXTURE_PATH.rglob(f"state_{request.headers['bmw-vin']}_*.json")) + try: + return httpx.Response( + 200, json=json.loads(load_fixture(state_file, integration=BMW_DOMAIN)) ) + except KeyError: + return httpx.Response(404) - account.add_vehicle( - vehicle_base, - vehicle_state, - fetched_at, - ) + +def mock_vehicles() -> respx.Router: + """Return mocked adapter for vehicles.""" + router = respx.mock(assert_all_called=False) + + # Get vehicle list + router.get(VEHICLES_URL).mock(side_effect=vehicles_sideeffect) + + # Get vehicle state + router.get(VEHICLE_STATE_URL).mock(side_effect=vehicle_state_sideeffect) + + return router + + +async def mock_login(auth: MyBMWAuthentication) -> None: + """Mock a successful login.""" + auth.access_token = "SOME_ACCESS_TOKEN" async def setup_mocked_integration(hass: HomeAssistant) -> MockConfigEntry: diff --git a/tests/components/bmw_connected_drive/conftest.py b/tests/components/bmw_connected_drive/conftest.py index bf9d32ed9fa..887df4da603 100644 --- a/tests/components/bmw_connected_drive/conftest.py +++ b/tests/components/bmw_connected_drive/conftest.py @@ -1,12 +1,15 @@ """Fixtures for BMW tests.""" -from bimmer_connected.account import MyBMWAccount +from bimmer_connected.api.authentication import MyBMWAuthentication import pytest -from . import mock_vehicles_from_fixture +from . import mock_login, mock_vehicles @pytest.fixture async def bmw_fixture(monkeypatch): - """Patch the vehicle fixtures into a MyBMWAccount.""" - monkeypatch.setattr(MyBMWAccount, "get_vehicles", mock_vehicles_from_fixture) + """Patch the MyBMW Login and mock HTTP calls.""" + monkeypatch.setattr(MyBMWAuthentication, "login", mock_login) + + with mock_vehicles(): + yield mock_vehicles diff --git a/tests/components/bmw_connected_drive/fixtures/diagnostics/diagnostics_config_entry.json b/tests/components/bmw_connected_drive/fixtures/diagnostics/diagnostics_config_entry.json new file mode 100644 index 00000000000..9c56e0595b6 --- /dev/null +++ b/tests/components/bmw_connected_drive/fixtures/diagnostics/diagnostics_config_entry.json @@ -0,0 +1,657 @@ +{ + "info": { + "username": "**REDACTED**", + "password": "**REDACTED**", + "region": "rest_of_world", + "refresh_token": "**REDACTED**" + }, + "data": [ + { + "data": { + "appVehicleType": "CONNECTED", + "attributes": { + "a4aType": "USB_ONLY", + "bodyType": "I01", + "brand": "BMW_I", + "color": 4284110934, + "countryOfOrigin": "CZ", + "driveTrain": "ELECTRIC_WITH_RANGE_EXTENDER", + "driverGuideInfo": { + "androidAppScheme": "com.bmwgroup.driversguide.row", + "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", + "iosAppScheme": "bmwdriversguide:///open", + "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" + }, + "headUnitType": "NBT", + "hmiVersion": "ID4", + "lastFetched": "2022-07-10T09:25:53.104Z", + "model": "i3 (+ REX)", + "softwareVersionCurrent": { + "iStep": 510, + "puStep": { "month": 11, "year": 21 }, + "seriesCluster": "I001" + }, + "softwareVersionExFactory": { + "iStep": 502, + "puStep": { "month": 3, "year": 15 }, + "seriesCluster": "I001" + }, + "year": 2015 + }, + "mappingInfo": { + "isAssociated": false, + "isLmmEnabled": false, + "isPrimaryUser": true, + "mappingStatus": "CONFIRMED" + }, + "vin": "**REDACTED**", + "is_metric": true, + "fetched_at": "2022-07-10T11:00:00+00:00", + "capabilities": { + "climateFunction": "AIR_CONDITIONING", + "climateNow": true, + "climateTimerTrigger": "DEPARTURE_TIMER", + "horn": true, + "isBmwChargingSupported": true, + "isCarSharingSupported": false, + "isChargeNowForBusinessSupported": false, + "isChargingHistorySupported": true, + "isChargingHospitalityEnabled": false, + "isChargingLoudnessEnabled": false, + "isChargingPlanSupported": true, + "isChargingPowerLimitEnabled": false, + "isChargingSettingsEnabled": false, + "isChargingTargetSocEnabled": false, + "isClimateTimerSupported": true, + "isCustomerEsimSupported": false, + "isDCSContractManagementSupported": true, + "isDataPrivacyEnabled": false, + "isEasyChargeEnabled": false, + "isEvGoChargingSupported": false, + "isMiniChargingSupported": false, + "isNonLscFeatureEnabled": false, + "isRemoteEngineStartSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteHistorySupported": true, + "isRemoteParkingSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": false, + "isSustainabilitySupported": false, + "isWifiHotspotServiceSupported": false, + "lastStateCallState": "ACTIVATED", + "lights": true, + "lock": true, + "remoteChargingCommands": {}, + "sendPoi": true, + "specialThemeSupport": [], + "unlock": true, + "vehicleFinder": false, + "vehicleStateSource": "LAST_STATE_CALL" + }, + "state": { + "chargingProfile": { + "chargingControlType": "WEEKLY_PLANNER", + "chargingMode": "DELAYED_CHARGING", + "chargingPreference": "CHARGING_WINDOW", + "chargingSettings": { + "hospitality": "NO_ACTION", + "idcc": "NO_ACTION", + "targetSoc": 100 + }, + "climatisationOn": false, + "departureTimes": [ + { + "action": "DEACTIVATE", + "id": 1, + "timeStamp": { "hour": 7, "minute": 35 }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY" + ] + }, + { + "action": "DEACTIVATE", + "id": 2, + "timeStamp": { "hour": 18, "minute": 0 }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY" + ] + }, + { + "action": "DEACTIVATE", + "id": 3, + "timeStamp": { "hour": 7, "minute": 0 }, + "timerWeekDays": [] + }, + { "action": "DEACTIVATE", "id": 4, "timerWeekDays": [] } + ], + "reductionOfChargeCurrent": { + "end": { "hour": 1, "minute": 30 }, + "start": { "hour": 18, "minute": 1 } + } + }, + "checkControlMessages": [], + "climateTimers": [ + { + "departureTime": { "hour": 6, "minute": 40 }, + "isWeeklyTimer": true, + "timerAction": "ACTIVATE", + "timerWeekDays": ["THURSDAY", "SUNDAY"] + }, + { + "departureTime": { "hour": 12, "minute": 50 }, + "isWeeklyTimer": false, + "timerAction": "ACTIVATE", + "timerWeekDays": ["MONDAY"] + }, + { + "departureTime": { "hour": 18, "minute": 59 }, + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": ["WEDNESDAY"] + } + ], + "combustionFuelLevel": { + "range": 105, + "remainingFuelLiters": 6, + "remainingFuelPercent": 65 + }, + "currentMileage": 137009, + "doorsState": { + "combinedSecurityState": "UNLOCKED", + "combinedState": "CLOSED", + "hood": "CLOSED", + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "trunk": "CLOSED" + }, + "driverPreferences": { "lscPrivacyMode": "OFF" }, + "electricChargingState": { + "chargingConnectionType": "CONDUCTIVE", + "chargingLevelPercent": 82, + "chargingStatus": "WAITING_FOR_CHARGING", + "chargingTarget": 100, + "isChargerConnected": true, + "range": 174 + }, + "isLeftSteering": true, + "isLscSupported": true, + "lastFetched": "2022-06-22T14:24:23.982Z", + "lastUpdatedAt": "2022-06-22T13:58:52Z", + "range": 174, + "requiredServices": [ + { + "dateTime": "2022-10-01T00:00:00.000Z", + "description": "Next service due by the specified date.", + "status": "OK", + "type": "BRAKE_FLUID" + }, + { + "dateTime": "2023-05-01T00:00:00.000Z", + "description": "Next vehicle check due after the specified distance or date.", + "status": "OK", + "type": "VEHICLE_CHECK" + }, + { + "dateTime": "2023-05-01T00:00:00.000Z", + "description": "Next state inspection due by the specified date.", + "status": "OK", + "type": "VEHICLE_TUV" + } + ], + "roofState": { "roofState": "CLOSED", "roofStateType": "SUN_ROOF" }, + "windowsState": { + "combinedState": "CLOSED", + "leftFront": "CLOSED", + "rightFront": "CLOSED" + } + } + }, + "fuel_and_battery": { + "remaining_range_fuel": [105, "km"], + "remaining_range_electric": [174, "km"], + "remaining_range_total": [279, "km"], + "remaining_fuel": [6, "L"], + "remaining_fuel_percent": 65, + "remaining_battery_percent": 82, + "charging_status": "WAITING_FOR_CHARGING", + "charging_start_time_no_tz": "2022-07-10T18:01:00", + "charging_end_time": null, + "is_charger_connected": true, + "account_timezone": { + "_std_offset": "0:00:00", + "_dst_offset": "0:00:00", + "_dst_saved": "0:00:00", + "_hasdst": false, + "_tznames": ["UTC", "UTC"] + }, + "charging_start_time": "2022-07-10T18:01:00+00:00" + }, + "vehicle_location": { + "location": null, + "heading": null, + "vehicle_update_timestamp": "2022-07-10T09:25:53+00:00", + "account_region": "row", + "remote_service_position": null + }, + "doors_and_windows": { + "door_lock_state": "UNLOCKED", + "lids": [ + { "name": "hood", "state": "CLOSED", "is_closed": true }, + { "name": "leftFront", "state": "CLOSED", "is_closed": true }, + { "name": "leftRear", "state": "CLOSED", "is_closed": true }, + { "name": "rightFront", "state": "CLOSED", "is_closed": true }, + { "name": "rightRear", "state": "CLOSED", "is_closed": true }, + { "name": "trunk", "state": "CLOSED", "is_closed": true }, + { "name": "sunRoof", "state": "CLOSED", "is_closed": true } + ], + "windows": [ + { "name": "leftFront", "state": "CLOSED", "is_closed": true }, + { "name": "rightFront", "state": "CLOSED", "is_closed": true } + ], + "all_lids_closed": true, + "all_windows_closed": true, + "open_lids": [], + "open_windows": [] + }, + "condition_based_services": { + "messages": [ + { + "service_type": "BRAKE_FLUID", + "state": "OK", + "due_date": "2022-10-01T00:00:00+00:00", + "due_distance": [null, null] + }, + { + "service_type": "VEHICLE_CHECK", + "state": "OK", + "due_date": "2023-05-01T00:00:00+00:00", + "due_distance": [null, null] + }, + { + "service_type": "VEHICLE_TUV", + "state": "OK", + "due_date": "2023-05-01T00:00:00+00:00", + "due_distance": [null, null] + } + ], + "is_service_required": false + }, + "check_control_messages": { + "messages": [], + "has_check_control_messages": false + }, + "charging_profile": { + "is_pre_entry_climatization_enabled": false, + "timer_type": "WEEKLY_PLANNER", + "departure_times": [ + { + "_timer_dict": { + "action": "DEACTIVATE", + "id": 1, + "timeStamp": { "hour": 7, "minute": 35 }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY" + ] + }, + "action": "DEACTIVATE", + "start_time": "07:35:00", + "timer_id": 1, + "weekdays": ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"] + }, + { + "_timer_dict": { + "action": "DEACTIVATE", + "id": 2, + "timeStamp": { "hour": 18, "minute": 0 }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY" + ] + }, + "action": "DEACTIVATE", + "start_time": "18:00:00", + "timer_id": 2, + "weekdays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY" + ] + }, + { + "_timer_dict": { + "action": "DEACTIVATE", + "id": 3, + "timeStamp": { "hour": 7, "minute": 0 }, + "timerWeekDays": [] + }, + "action": "DEACTIVATE", + "start_time": "07:00:00", + "timer_id": 3, + "weekdays": [] + }, + { + "_timer_dict": { + "action": "DEACTIVATE", + "id": 4, + "timerWeekDays": [] + }, + "action": "DEACTIVATE", + "start_time": null, + "timer_id": 4, + "weekdays": [] + } + ], + "preferred_charging_window": { + "_window_dict": { + "end": { "hour": 1, "minute": 30 }, + "start": { "hour": 18, "minute": 1 } + }, + "end_time": "01:30:00", + "start_time": "18:01:00" + }, + "charging_preferences": "CHARGING_WINDOW", + "charging_mode": "DELAYED_CHARGING" + }, + "available_attributes": [ + "gps_position", + "vin", + "remaining_range_total", + "mileage", + "charging_time_remaining", + "charging_end_time", + "charging_time_label", + "charging_status", + "connection_status", + "remaining_battery_percent", + "remaining_range_electric", + "last_charging_end_result", + "remaining_fuel", + "remaining_range_fuel", + "remaining_fuel_percent", + "condition_based_services", + "check_control_messages", + "door_lock_state", + "timestamp", + "lids", + "windows" + ], + "brand": "bmw", + "drive_train": "ELECTRIC_WITH_RANGE_EXTENDER", + "drive_train_attributes": [ + "remaining_range_total", + "mileage", + "charging_time_remaining", + "charging_end_time", + "charging_time_label", + "charging_status", + "connection_status", + "remaining_battery_percent", + "remaining_range_electric", + "last_charging_end_result", + "remaining_fuel", + "remaining_range_fuel", + "remaining_fuel_percent" + ], + "has_combustion_drivetrain": true, + "has_electric_drivetrain": true, + "is_charging_plan_supported": true, + "is_lsc_enabled": true, + "is_vehicle_active": false, + "is_vehicle_tracking_enabled": false, + "lsc_type": "ACTIVATED", + "mileage": [137009, "km"], + "name": "i3 (+ REX)", + "timestamp": "2022-07-10T09:25:53+00:00", + "vin": "**REDACTED**" + } + ], + "fingerprint": [ + { + "filename": "bmw-vehicles.json", + "content": [ + { + "appVehicleType": "CONNECTED", + "attributes": { + "a4aType": "USB_ONLY", + "bodyType": "I01", + "brand": "BMW_I", + "color": 4284110934, + "countryOfOrigin": "CZ", + "driveTrain": "ELECTRIC_WITH_RANGE_EXTENDER", + "driverGuideInfo": { + "androidAppScheme": "com.bmwgroup.driversguide.row", + "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", + "iosAppScheme": "bmwdriversguide:///open", + "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" + }, + "headUnitType": "NBT", + "hmiVersion": "ID4", + "lastFetched": "2022-07-10T09:25:53.104Z", + "model": "i3 (+ REX)", + "softwareVersionCurrent": { + "iStep": 510, + "puStep": { "month": 11, "year": 21 }, + "seriesCluster": "I001" + }, + "softwareVersionExFactory": { + "iStep": 502, + "puStep": { "month": 3, "year": 15 }, + "seriesCluster": "I001" + }, + "year": 2015 + }, + "mappingInfo": { + "isAssociated": false, + "isLmmEnabled": false, + "isPrimaryUser": true, + "mappingStatus": "CONFIRMED" + }, + "vin": "**REDACTED**" + } + ] + }, + { "filename": "mini-vehicles.json", "content": [] }, + { + "filename": "bmw-vehicles_state_WBY0FINGERPRINT01.json", + "content": { + "capabilities": { + "climateFunction": "AIR_CONDITIONING", + "climateNow": true, + "climateTimerTrigger": "DEPARTURE_TIMER", + "horn": true, + "isBmwChargingSupported": true, + "isCarSharingSupported": false, + "isChargeNowForBusinessSupported": false, + "isChargingHistorySupported": true, + "isChargingHospitalityEnabled": false, + "isChargingLoudnessEnabled": false, + "isChargingPlanSupported": true, + "isChargingPowerLimitEnabled": false, + "isChargingSettingsEnabled": false, + "isChargingTargetSocEnabled": false, + "isClimateTimerSupported": true, + "isCustomerEsimSupported": false, + "isDCSContractManagementSupported": true, + "isDataPrivacyEnabled": false, + "isEasyChargeEnabled": false, + "isEvGoChargingSupported": false, + "isMiniChargingSupported": false, + "isNonLscFeatureEnabled": false, + "isRemoteEngineStartSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteHistorySupported": true, + "isRemoteParkingSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": false, + "isSustainabilitySupported": false, + "isWifiHotspotServiceSupported": false, + "lastStateCallState": "ACTIVATED", + "lights": true, + "lock": true, + "remoteChargingCommands": {}, + "sendPoi": true, + "specialThemeSupport": [], + "unlock": true, + "vehicleFinder": false, + "vehicleStateSource": "LAST_STATE_CALL" + }, + "state": { + "chargingProfile": { + "chargingControlType": "WEEKLY_PLANNER", + "chargingMode": "DELAYED_CHARGING", + "chargingPreference": "CHARGING_WINDOW", + "chargingSettings": { + "hospitality": "NO_ACTION", + "idcc": "NO_ACTION", + "targetSoc": 100 + }, + "climatisationOn": false, + "departureTimes": [ + { + "action": "DEACTIVATE", + "id": 1, + "timeStamp": { "hour": 7, "minute": 35 }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY" + ] + }, + { + "action": "DEACTIVATE", + "id": 2, + "timeStamp": { "hour": 18, "minute": 0 }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY" + ] + }, + { + "action": "DEACTIVATE", + "id": 3, + "timeStamp": { "hour": 7, "minute": 0 }, + "timerWeekDays": [] + }, + { "action": "DEACTIVATE", "id": 4, "timerWeekDays": [] } + ], + "reductionOfChargeCurrent": { + "end": { "hour": 1, "minute": 30 }, + "start": { "hour": 18, "minute": 1 } + } + }, + "checkControlMessages": [], + "climateTimers": [ + { + "departureTime": { "hour": 6, "minute": 40 }, + "isWeeklyTimer": true, + "timerAction": "ACTIVATE", + "timerWeekDays": ["THURSDAY", "SUNDAY"] + }, + { + "departureTime": { "hour": 12, "minute": 50 }, + "isWeeklyTimer": false, + "timerAction": "ACTIVATE", + "timerWeekDays": ["MONDAY"] + }, + { + "departureTime": { "hour": 18, "minute": 59 }, + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": ["WEDNESDAY"] + } + ], + "combustionFuelLevel": { + "range": 105, + "remainingFuelLiters": 6, + "remainingFuelPercent": 65 + }, + "currentMileage": 137009, + "doorsState": { + "combinedSecurityState": "UNLOCKED", + "combinedState": "CLOSED", + "hood": "CLOSED", + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "trunk": "CLOSED" + }, + "driverPreferences": { "lscPrivacyMode": "OFF" }, + "electricChargingState": { + "chargingConnectionType": "CONDUCTIVE", + "chargingLevelPercent": 82, + "chargingStatus": "WAITING_FOR_CHARGING", + "chargingTarget": 100, + "isChargerConnected": true, + "range": 174 + }, + "isLeftSteering": true, + "isLscSupported": true, + "lastFetched": "2022-06-22T14:24:23.982Z", + "lastUpdatedAt": "2022-06-22T13:58:52Z", + "range": 174, + "requiredServices": [ + { + "dateTime": "2022-10-01T00:00:00.000Z", + "description": "Next service due by the specified date.", + "status": "OK", + "type": "BRAKE_FLUID" + }, + { + "dateTime": "2023-05-01T00:00:00.000Z", + "description": "Next vehicle check due after the specified distance or date.", + "status": "OK", + "type": "VEHICLE_CHECK" + }, + { + "dateTime": "2023-05-01T00:00:00.000Z", + "description": "Next state inspection due by the specified date.", + "status": "OK", + "type": "VEHICLE_TUV" + } + ], + "roofState": { "roofState": "CLOSED", "roofStateType": "SUN_ROOF" }, + "windowsState": { + "combinedState": "CLOSED", + "leftFront": "CLOSED", + "rightFront": "CLOSED" + } + } + } + } + ] +} diff --git a/tests/components/bmw_connected_drive/fixtures/diagnostics/diagnostics_device.json b/tests/components/bmw_connected_drive/fixtures/diagnostics/diagnostics_device.json new file mode 100644 index 00000000000..d76f2c80712 --- /dev/null +++ b/tests/components/bmw_connected_drive/fixtures/diagnostics/diagnostics_device.json @@ -0,0 +1,655 @@ +{ + "info": { + "username": "**REDACTED**", + "password": "**REDACTED**", + "region": "rest_of_world", + "refresh_token": "**REDACTED**" + }, + "data": { + "data": { + "appVehicleType": "CONNECTED", + "attributes": { + "a4aType": "USB_ONLY", + "bodyType": "I01", + "brand": "BMW_I", + "color": 4284110934, + "countryOfOrigin": "CZ", + "driveTrain": "ELECTRIC_WITH_RANGE_EXTENDER", + "driverGuideInfo": { + "androidAppScheme": "com.bmwgroup.driversguide.row", + "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", + "iosAppScheme": "bmwdriversguide:///open", + "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" + }, + "headUnitType": "NBT", + "hmiVersion": "ID4", + "lastFetched": "2022-07-10T09:25:53.104Z", + "model": "i3 (+ REX)", + "softwareVersionCurrent": { + "iStep": 510, + "puStep": { "month": 11, "year": 21 }, + "seriesCluster": "I001" + }, + "softwareVersionExFactory": { + "iStep": 502, + "puStep": { "month": 3, "year": 15 }, + "seriesCluster": "I001" + }, + "year": 2015 + }, + "mappingInfo": { + "isAssociated": false, + "isLmmEnabled": false, + "isPrimaryUser": true, + "mappingStatus": "CONFIRMED" + }, + "vin": "**REDACTED**", + "is_metric": true, + "fetched_at": "2022-07-10T11:00:00+00:00", + "capabilities": { + "climateFunction": "AIR_CONDITIONING", + "climateNow": true, + "climateTimerTrigger": "DEPARTURE_TIMER", + "horn": true, + "isBmwChargingSupported": true, + "isCarSharingSupported": false, + "isChargeNowForBusinessSupported": false, + "isChargingHistorySupported": true, + "isChargingHospitalityEnabled": false, + "isChargingLoudnessEnabled": false, + "isChargingPlanSupported": true, + "isChargingPowerLimitEnabled": false, + "isChargingSettingsEnabled": false, + "isChargingTargetSocEnabled": false, + "isClimateTimerSupported": true, + "isCustomerEsimSupported": false, + "isDCSContractManagementSupported": true, + "isDataPrivacyEnabled": false, + "isEasyChargeEnabled": false, + "isEvGoChargingSupported": false, + "isMiniChargingSupported": false, + "isNonLscFeatureEnabled": false, + "isRemoteEngineStartSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteHistorySupported": true, + "isRemoteParkingSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": false, + "isSustainabilitySupported": false, + "isWifiHotspotServiceSupported": false, + "lastStateCallState": "ACTIVATED", + "lights": true, + "lock": true, + "remoteChargingCommands": {}, + "sendPoi": true, + "specialThemeSupport": [], + "unlock": true, + "vehicleFinder": false, + "vehicleStateSource": "LAST_STATE_CALL" + }, + "state": { + "chargingProfile": { + "chargingControlType": "WEEKLY_PLANNER", + "chargingMode": "DELAYED_CHARGING", + "chargingPreference": "CHARGING_WINDOW", + "chargingSettings": { + "hospitality": "NO_ACTION", + "idcc": "NO_ACTION", + "targetSoc": 100 + }, + "climatisationOn": false, + "departureTimes": [ + { + "action": "DEACTIVATE", + "id": 1, + "timeStamp": { "hour": 7, "minute": 35 }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY" + ] + }, + { + "action": "DEACTIVATE", + "id": 2, + "timeStamp": { "hour": 18, "minute": 0 }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY" + ] + }, + { + "action": "DEACTIVATE", + "id": 3, + "timeStamp": { "hour": 7, "minute": 0 }, + "timerWeekDays": [] + }, + { "action": "DEACTIVATE", "id": 4, "timerWeekDays": [] } + ], + "reductionOfChargeCurrent": { + "end": { "hour": 1, "minute": 30 }, + "start": { "hour": 18, "minute": 1 } + } + }, + "checkControlMessages": [], + "climateTimers": [ + { + "departureTime": { "hour": 6, "minute": 40 }, + "isWeeklyTimer": true, + "timerAction": "ACTIVATE", + "timerWeekDays": ["THURSDAY", "SUNDAY"] + }, + { + "departureTime": { "hour": 12, "minute": 50 }, + "isWeeklyTimer": false, + "timerAction": "ACTIVATE", + "timerWeekDays": ["MONDAY"] + }, + { + "departureTime": { "hour": 18, "minute": 59 }, + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": ["WEDNESDAY"] + } + ], + "combustionFuelLevel": { + "range": 105, + "remainingFuelLiters": 6, + "remainingFuelPercent": 65 + }, + "currentMileage": 137009, + "doorsState": { + "combinedSecurityState": "UNLOCKED", + "combinedState": "CLOSED", + "hood": "CLOSED", + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "trunk": "CLOSED" + }, + "driverPreferences": { "lscPrivacyMode": "OFF" }, + "electricChargingState": { + "chargingConnectionType": "CONDUCTIVE", + "chargingLevelPercent": 82, + "chargingStatus": "WAITING_FOR_CHARGING", + "chargingTarget": 100, + "isChargerConnected": true, + "range": 174 + }, + "isLeftSteering": true, + "isLscSupported": true, + "lastFetched": "2022-06-22T14:24:23.982Z", + "lastUpdatedAt": "2022-06-22T13:58:52Z", + "range": 174, + "requiredServices": [ + { + "dateTime": "2022-10-01T00:00:00.000Z", + "description": "Next service due by the specified date.", + "status": "OK", + "type": "BRAKE_FLUID" + }, + { + "dateTime": "2023-05-01T00:00:00.000Z", + "description": "Next vehicle check due after the specified distance or date.", + "status": "OK", + "type": "VEHICLE_CHECK" + }, + { + "dateTime": "2023-05-01T00:00:00.000Z", + "description": "Next state inspection due by the specified date.", + "status": "OK", + "type": "VEHICLE_TUV" + } + ], + "roofState": { "roofState": "CLOSED", "roofStateType": "SUN_ROOF" }, + "windowsState": { + "combinedState": "CLOSED", + "leftFront": "CLOSED", + "rightFront": "CLOSED" + } + } + }, + "fuel_and_battery": { + "remaining_range_fuel": [105, "km"], + "remaining_range_electric": [174, "km"], + "remaining_range_total": [279, "km"], + "remaining_fuel": [6, "L"], + "remaining_fuel_percent": 65, + "remaining_battery_percent": 82, + "charging_status": "WAITING_FOR_CHARGING", + "charging_start_time_no_tz": "2022-07-10T18:01:00", + "charging_end_time": null, + "is_charger_connected": true, + "account_timezone": { + "_std_offset": "0:00:00", + "_dst_offset": "0:00:00", + "_dst_saved": "0:00:00", + "_hasdst": false, + "_tznames": ["UTC", "UTC"] + }, + "charging_start_time": "2022-07-10T18:01:00+00:00" + }, + "vehicle_location": { + "location": null, + "heading": null, + "vehicle_update_timestamp": "2022-07-10T09:25:53+00:00", + "account_region": "row", + "remote_service_position": null + }, + "doors_and_windows": { + "door_lock_state": "UNLOCKED", + "lids": [ + { "name": "hood", "state": "CLOSED", "is_closed": true }, + { "name": "leftFront", "state": "CLOSED", "is_closed": true }, + { "name": "leftRear", "state": "CLOSED", "is_closed": true }, + { "name": "rightFront", "state": "CLOSED", "is_closed": true }, + { "name": "rightRear", "state": "CLOSED", "is_closed": true }, + { "name": "trunk", "state": "CLOSED", "is_closed": true }, + { "name": "sunRoof", "state": "CLOSED", "is_closed": true } + ], + "windows": [ + { "name": "leftFront", "state": "CLOSED", "is_closed": true }, + { "name": "rightFront", "state": "CLOSED", "is_closed": true } + ], + "all_lids_closed": true, + "all_windows_closed": true, + "open_lids": [], + "open_windows": [] + }, + "condition_based_services": { + "messages": [ + { + "service_type": "BRAKE_FLUID", + "state": "OK", + "due_date": "2022-10-01T00:00:00+00:00", + "due_distance": [null, null] + }, + { + "service_type": "VEHICLE_CHECK", + "state": "OK", + "due_date": "2023-05-01T00:00:00+00:00", + "due_distance": [null, null] + }, + { + "service_type": "VEHICLE_TUV", + "state": "OK", + "due_date": "2023-05-01T00:00:00+00:00", + "due_distance": [null, null] + } + ], + "is_service_required": false + }, + "check_control_messages": { + "messages": [], + "has_check_control_messages": false + }, + "charging_profile": { + "is_pre_entry_climatization_enabled": false, + "timer_type": "WEEKLY_PLANNER", + "departure_times": [ + { + "_timer_dict": { + "action": "DEACTIVATE", + "id": 1, + "timeStamp": { "hour": 7, "minute": 35 }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY" + ] + }, + "action": "DEACTIVATE", + "start_time": "07:35:00", + "timer_id": 1, + "weekdays": ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"] + }, + { + "_timer_dict": { + "action": "DEACTIVATE", + "id": 2, + "timeStamp": { "hour": 18, "minute": 0 }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY" + ] + }, + "action": "DEACTIVATE", + "start_time": "18:00:00", + "timer_id": 2, + "weekdays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY" + ] + }, + { + "_timer_dict": { + "action": "DEACTIVATE", + "id": 3, + "timeStamp": { "hour": 7, "minute": 0 }, + "timerWeekDays": [] + }, + "action": "DEACTIVATE", + "start_time": "07:00:00", + "timer_id": 3, + "weekdays": [] + }, + { + "_timer_dict": { + "action": "DEACTIVATE", + "id": 4, + "timerWeekDays": [] + }, + "action": "DEACTIVATE", + "start_time": null, + "timer_id": 4, + "weekdays": [] + } + ], + "preferred_charging_window": { + "_window_dict": { + "end": { "hour": 1, "minute": 30 }, + "start": { "hour": 18, "minute": 1 } + }, + "end_time": "01:30:00", + "start_time": "18:01:00" + }, + "charging_preferences": "CHARGING_WINDOW", + "charging_mode": "DELAYED_CHARGING" + }, + "available_attributes": [ + "gps_position", + "vin", + "remaining_range_total", + "mileage", + "charging_time_remaining", + "charging_end_time", + "charging_time_label", + "charging_status", + "connection_status", + "remaining_battery_percent", + "remaining_range_electric", + "last_charging_end_result", + "remaining_fuel", + "remaining_range_fuel", + "remaining_fuel_percent", + "condition_based_services", + "check_control_messages", + "door_lock_state", + "timestamp", + "lids", + "windows" + ], + "brand": "bmw", + "drive_train": "ELECTRIC_WITH_RANGE_EXTENDER", + "drive_train_attributes": [ + "remaining_range_total", + "mileage", + "charging_time_remaining", + "charging_end_time", + "charging_time_label", + "charging_status", + "connection_status", + "remaining_battery_percent", + "remaining_range_electric", + "last_charging_end_result", + "remaining_fuel", + "remaining_range_fuel", + "remaining_fuel_percent" + ], + "has_combustion_drivetrain": true, + "has_electric_drivetrain": true, + "is_charging_plan_supported": true, + "is_lsc_enabled": true, + "is_vehicle_active": false, + "is_vehicle_tracking_enabled": false, + "lsc_type": "ACTIVATED", + "mileage": [137009, "km"], + "name": "i3 (+ REX)", + "timestamp": "2022-07-10T09:25:53+00:00", + "vin": "**REDACTED**" + }, + "fingerprint": [ + { + "filename": "bmw-vehicles.json", + "content": [ + { + "appVehicleType": "CONNECTED", + "attributes": { + "a4aType": "USB_ONLY", + "bodyType": "I01", + "brand": "BMW_I", + "color": 4284110934, + "countryOfOrigin": "CZ", + "driveTrain": "ELECTRIC_WITH_RANGE_EXTENDER", + "driverGuideInfo": { + "androidAppScheme": "com.bmwgroup.driversguide.row", + "androidStoreUrl": "https://play.google.com/store/apps/details?id=com.bmwgroup.driversguide.row", + "iosAppScheme": "bmwdriversguide:///open", + "iosStoreUrl": "https://apps.apple.com/de/app/id714042749?mt=8" + }, + "headUnitType": "NBT", + "hmiVersion": "ID4", + "lastFetched": "2022-07-10T09:25:53.104Z", + "model": "i3 (+ REX)", + "softwareVersionCurrent": { + "iStep": 510, + "puStep": { "month": 11, "year": 21 }, + "seriesCluster": "I001" + }, + "softwareVersionExFactory": { + "iStep": 502, + "puStep": { "month": 3, "year": 15 }, + "seriesCluster": "I001" + }, + "year": 2015 + }, + "mappingInfo": { + "isAssociated": false, + "isLmmEnabled": false, + "isPrimaryUser": true, + "mappingStatus": "CONFIRMED" + }, + "vin": "**REDACTED**" + } + ] + }, + { "filename": "mini-vehicles.json", "content": [] }, + { + "filename": "bmw-vehicles_state_WBY0FINGERPRINT01.json", + "content": { + "capabilities": { + "climateFunction": "AIR_CONDITIONING", + "climateNow": true, + "climateTimerTrigger": "DEPARTURE_TIMER", + "horn": true, + "isBmwChargingSupported": true, + "isCarSharingSupported": false, + "isChargeNowForBusinessSupported": false, + "isChargingHistorySupported": true, + "isChargingHospitalityEnabled": false, + "isChargingLoudnessEnabled": false, + "isChargingPlanSupported": true, + "isChargingPowerLimitEnabled": false, + "isChargingSettingsEnabled": false, + "isChargingTargetSocEnabled": false, + "isClimateTimerSupported": true, + "isCustomerEsimSupported": false, + "isDCSContractManagementSupported": true, + "isDataPrivacyEnabled": false, + "isEasyChargeEnabled": false, + "isEvGoChargingSupported": false, + "isMiniChargingSupported": false, + "isNonLscFeatureEnabled": false, + "isRemoteEngineStartSupported": false, + "isRemoteHistoryDeletionSupported": false, + "isRemoteHistorySupported": true, + "isRemoteParkingSupported": false, + "isRemoteServicesActivationRequired": false, + "isRemoteServicesBookingRequired": false, + "isScanAndChargeSupported": false, + "isSustainabilitySupported": false, + "isWifiHotspotServiceSupported": false, + "lastStateCallState": "ACTIVATED", + "lights": true, + "lock": true, + "remoteChargingCommands": {}, + "sendPoi": true, + "specialThemeSupport": [], + "unlock": true, + "vehicleFinder": false, + "vehicleStateSource": "LAST_STATE_CALL" + }, + "state": { + "chargingProfile": { + "chargingControlType": "WEEKLY_PLANNER", + "chargingMode": "DELAYED_CHARGING", + "chargingPreference": "CHARGING_WINDOW", + "chargingSettings": { + "hospitality": "NO_ACTION", + "idcc": "NO_ACTION", + "targetSoc": 100 + }, + "climatisationOn": false, + "departureTimes": [ + { + "action": "DEACTIVATE", + "id": 1, + "timeStamp": { "hour": 7, "minute": 35 }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY" + ] + }, + { + "action": "DEACTIVATE", + "id": 2, + "timeStamp": { "hour": 18, "minute": 0 }, + "timerWeekDays": [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + "SUNDAY" + ] + }, + { + "action": "DEACTIVATE", + "id": 3, + "timeStamp": { "hour": 7, "minute": 0 }, + "timerWeekDays": [] + }, + { "action": "DEACTIVATE", "id": 4, "timerWeekDays": [] } + ], + "reductionOfChargeCurrent": { + "end": { "hour": 1, "minute": 30 }, + "start": { "hour": 18, "minute": 1 } + } + }, + "checkControlMessages": [], + "climateTimers": [ + { + "departureTime": { "hour": 6, "minute": 40 }, + "isWeeklyTimer": true, + "timerAction": "ACTIVATE", + "timerWeekDays": ["THURSDAY", "SUNDAY"] + }, + { + "departureTime": { "hour": 12, "minute": 50 }, + "isWeeklyTimer": false, + "timerAction": "ACTIVATE", + "timerWeekDays": ["MONDAY"] + }, + { + "departureTime": { "hour": 18, "minute": 59 }, + "isWeeklyTimer": true, + "timerAction": "DEACTIVATE", + "timerWeekDays": ["WEDNESDAY"] + } + ], + "combustionFuelLevel": { + "range": 105, + "remainingFuelLiters": 6, + "remainingFuelPercent": 65 + }, + "currentMileage": 137009, + "doorsState": { + "combinedSecurityState": "UNLOCKED", + "combinedState": "CLOSED", + "hood": "CLOSED", + "leftFront": "CLOSED", + "leftRear": "CLOSED", + "rightFront": "CLOSED", + "rightRear": "CLOSED", + "trunk": "CLOSED" + }, + "driverPreferences": { "lscPrivacyMode": "OFF" }, + "electricChargingState": { + "chargingConnectionType": "CONDUCTIVE", + "chargingLevelPercent": 82, + "chargingStatus": "WAITING_FOR_CHARGING", + "chargingTarget": 100, + "isChargerConnected": true, + "range": 174 + }, + "isLeftSteering": true, + "isLscSupported": true, + "lastFetched": "2022-06-22T14:24:23.982Z", + "lastUpdatedAt": "2022-06-22T13:58:52Z", + "range": 174, + "requiredServices": [ + { + "dateTime": "2022-10-01T00:00:00.000Z", + "description": "Next service due by the specified date.", + "status": "OK", + "type": "BRAKE_FLUID" + }, + { + "dateTime": "2023-05-01T00:00:00.000Z", + "description": "Next vehicle check due after the specified distance or date.", + "status": "OK", + "type": "VEHICLE_CHECK" + }, + { + "dateTime": "2023-05-01T00:00:00.000Z", + "description": "Next state inspection due by the specified date.", + "status": "OK", + "type": "VEHICLE_TUV" + } + ], + "roofState": { "roofState": "CLOSED", "roofStateType": "SUN_ROOF" }, + "windowsState": { + "combinedState": "CLOSED", + "leftFront": "CLOSED", + "rightFront": "CLOSED" + } + } + } + } + ] +} diff --git a/tests/components/bmw_connected_drive/test_diagnostics.py b/tests/components/bmw_connected_drive/test_diagnostics.py new file mode 100644 index 00000000000..816154416a8 --- /dev/null +++ b/tests/components/bmw_connected_drive/test_diagnostics.py @@ -0,0 +1,104 @@ +"""Test BMW diagnostics.""" + +import datetime +import json +import os +import time + +from freezegun import freeze_time + +from homeassistant.components.bmw_connected_drive.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from . import setup_mocked_integration + +from tests.common import load_fixture +from tests.components.diagnostics import ( + get_diagnostics_for_config_entry, + get_diagnostics_for_device, +) + + +@freeze_time(datetime.datetime(2022, 7, 10, 11)) +async def test_config_entry_diagnostics(hass: HomeAssistant, hass_client, bmw_fixture): + """Test config entry diagnostics.""" + + # Make sure that local timezone for test is UTC + os.environ["TZ"] = "UTC" + time.tzset() + + mock_config_entry = await setup_mocked_integration(hass) + + diagnostics = await get_diagnostics_for_config_entry( + hass, hass_client, mock_config_entry + ) + + diagnostics_fixture = json.loads( + load_fixture("diagnostics/diagnostics_config_entry.json", DOMAIN) + ) + + assert diagnostics == diagnostics_fixture + + +@freeze_time(datetime.datetime(2022, 7, 10, 11)) +async def test_device_diagnostics(hass: HomeAssistant, hass_client, bmw_fixture): + """Test device diagnostics.""" + + # Make sure that local timezone for test is UTC + os.environ["TZ"] = "UTC" + time.tzset() + + mock_config_entry = await setup_mocked_integration(hass) + + device_registry = dr.async_get(hass) + reg_device = device_registry.async_get_device( + identifiers={(DOMAIN, "WBY00000000REXI01")}, + ) + assert reg_device is not None + + diagnostics = await get_diagnostics_for_device( + hass, hass_client, mock_config_entry, reg_device + ) + + diagnostics_fixture = json.loads( + load_fixture("diagnostics/diagnostics_device.json", DOMAIN) + ) + + assert diagnostics == diagnostics_fixture + + +@freeze_time(datetime.datetime(2022, 7, 10, 11)) +async def test_device_diagnostics_vehicle_not_found( + hass: HomeAssistant, hass_client, bmw_fixture +): + """Test device diagnostics when the vehicle cannot be found.""" + + # Make sure that local timezone for test is UTC + os.environ["TZ"] = "UTC" + time.tzset() + + mock_config_entry = await setup_mocked_integration(hass) + + device_registry = dr.async_get(hass) + reg_device = device_registry.async_get_device( + identifiers={(DOMAIN, "WBY00000000REXI01")}, + ) + assert reg_device is not None + + # Change vehicle identifier so that vehicle will not be found + device_registry.async_update_device( + reg_device.id, new_identifiers={(DOMAIN, "WBY00000000REXI99")} + ) + + diagnostics = await get_diagnostics_for_device( + hass, hass_client, mock_config_entry, reg_device + ) + + diagnostics_fixture = json.loads( + load_fixture("diagnostics/diagnostics_device.json", DOMAIN) + ) + # Mock empty data if car is not found in account anymore + diagnostics_fixture["data"] = None + + assert diagnostics == diagnostics_fixture diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index 40b1b7499a9..9b33e98981d 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -2,10 +2,10 @@ from unittest.mock import patch from pybravia import ( - BraviaTVAuthError, - BraviaTVConnectionError, - BraviaTVError, - BraviaTVNotSupported, + BraviaAuthError, + BraviaConnectionError, + BraviaError, + BraviaNotSupported, ) import pytest @@ -13,7 +13,6 @@ from homeassistant import data_entry_flow from homeassistant.components import ssdp from homeassistant.components.braviatv.const import ( CONF_CLIENT_ID, - CONF_IGNORED_SOURCES, CONF_NICKNAME, CONF_USE_PSK, DOMAIN, @@ -21,7 +20,6 @@ from homeassistant.components.braviatv.const import ( ) from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN -from homeassistant.core import HomeAssistant from homeassistant.helpers import instance_id from tests.common import MockConfigEntry @@ -107,10 +105,10 @@ async def test_ssdp_discovery(hass): assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "confirm" - with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( - "pybravia.BraviaTV.set_wol_mode" - ), patch( - "pybravia.BraviaTV.get_system_info", + with patch("pybravia.BraviaClient.connect"), patch( + "pybravia.BraviaClient.pair" + ), patch("pybravia.BraviaClient.set_wol_mode"), patch( + "pybravia.BraviaClient.get_system_info", return_value=BRAVIA_SYSTEM_INFO, ), patch( "homeassistant.components.braviatv.async_setup_entry", return_value=True @@ -195,17 +193,17 @@ async def test_user_invalid_host(hass): @pytest.mark.parametrize( "side_effect, error_message", [ - (BraviaTVAuthError, "invalid_auth"), - (BraviaTVNotSupported, "unsupported_model"), - (BraviaTVConnectionError, "cannot_connect"), + (BraviaAuthError, "invalid_auth"), + (BraviaNotSupported, "unsupported_model"), + (BraviaConnectionError, "cannot_connect"), ], ) async def test_pin_form_error(hass, side_effect, error_message): """Test that PIN form errors are correct.""" with patch( - "pybravia.BraviaTV.connect", + "pybravia.BraviaClient.connect", side_effect=side_effect, - ), patch("pybravia.BraviaTV.pair"): + ), patch("pybravia.BraviaClient.pair"): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) @@ -222,15 +220,15 @@ async def test_pin_form_error(hass, side_effect, error_message): @pytest.mark.parametrize( "side_effect, error_message", [ - (BraviaTVAuthError, "invalid_auth"), - (BraviaTVNotSupported, "unsupported_model"), - (BraviaTVConnectionError, "cannot_connect"), + (BraviaAuthError, "invalid_auth"), + (BraviaNotSupported, "unsupported_model"), + (BraviaConnectionError, "cannot_connect"), ], ) async def test_psk_form_error(hass, side_effect, error_message): """Test that PSK form errors are correct.""" with patch( - "pybravia.BraviaTV.connect", + "pybravia.BraviaClient.connect", side_effect=side_effect, ): result = await hass.config_entries.flow.async_init( @@ -248,7 +246,7 @@ async def test_psk_form_error(hass, side_effect, error_message): async def test_no_ip_control(hass): """Test that error are shown when IP Control is disabled on the TV.""" - with patch("pybravia.BraviaTV.pair", side_effect=BraviaTVError): + with patch("pybravia.BraviaClient.pair", side_effect=BraviaError): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} ) @@ -274,10 +272,10 @@ async def test_duplicate_error(hass): ) config_entry.add_to_hass(hass) - with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( - "pybravia.BraviaTV.set_wol_mode" - ), patch( - "pybravia.BraviaTV.get_system_info", + with patch("pybravia.BraviaClient.connect"), patch( + "pybravia.BraviaClient.pair" + ), patch("pybravia.BraviaClient.set_wol_mode"), patch( + "pybravia.BraviaClient.get_system_info", return_value=BRAVIA_SYSTEM_INFO, ): result = await hass.config_entries.flow.async_init( @@ -298,10 +296,10 @@ async def test_create_entry(hass): """Test that entry is added correctly with PIN auth.""" uuid = await instance_id.async_get(hass) - with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch( - "pybravia.BraviaTV.set_wol_mode" - ), patch( - "pybravia.BraviaTV.get_system_info", + with patch("pybravia.BraviaClient.connect"), patch( + "pybravia.BraviaClient.pair" + ), patch("pybravia.BraviaClient.set_wol_mode"), patch( + "pybravia.BraviaClient.get_system_info", return_value=BRAVIA_SYSTEM_INFO, ), patch( "homeassistant.components.braviatv.async_setup_entry", return_value=True @@ -339,10 +337,10 @@ async def test_create_entry(hass): async def test_create_entry_psk(hass): """Test that entry is added correctly with PSK auth.""" - with patch("pybravia.BraviaTV.connect"), patch( - "pybravia.BraviaTV.set_wol_mode" + with patch("pybravia.BraviaClient.connect"), patch( + "pybravia.BraviaClient.set_wol_mode" ), patch( - "pybravia.BraviaTV.get_system_info", + "pybravia.BraviaClient.get_system_info", return_value=BRAVIA_SYSTEM_INFO, ), patch( "homeassistant.components.braviatv.async_setup_entry", return_value=True @@ -376,97 +374,6 @@ async def test_create_entry_psk(hass): } -async def test_options_flow(hass: HomeAssistant) -> None: - """Test config flow options.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id="very_unique_string", - data={ - CONF_HOST: "bravia-host", - CONF_PIN: "1234", - CONF_MAC: "AA:BB:CC:DD:EE:FF", - }, - title="TV-Model", - ) - config_entry.add_to_hass(hass) - - with patch("pybravia.BraviaTV.connect"), patch( - "pybravia.BraviaTV.get_power_status", - return_value="active", - ), patch( - "pybravia.BraviaTV.get_external_status", - return_value=BRAVIA_SOURCES, - ), patch( - "pybravia.BraviaTV.send_rest_req", - return_value={}, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - result = await hass.config_entries.options.async_init(config_entry.entry_id) - - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_IGNORED_SOURCES: ["HDMI 1", "HDMI 2"]} - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert config_entry.options == {CONF_IGNORED_SOURCES: ["HDMI 1", "HDMI 2"]} - - # Test that saving with missing sources is ok - with patch( - "pybravia.BraviaTV.get_external_status", - return_value=BRAVIA_SOURCES[1:], - ): - result = await hass.config_entries.options.async_init(config_entry.entry_id) - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_IGNORED_SOURCES: ["HDMI 1"]} - ) - await hass.async_block_till_done() - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert config_entry.options == {CONF_IGNORED_SOURCES: ["HDMI 1"]} - - -async def test_options_flow_error(hass: HomeAssistant) -> None: - """Test config flow options.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id="very_unique_string", - data={ - CONF_HOST: "bravia-host", - CONF_PIN: "1234", - CONF_MAC: "AA:BB:CC:DD:EE:FF", - }, - title="TV-Model", - ) - config_entry.add_to_hass(hass) - - with patch("pybravia.BraviaTV.connect"), patch( - "pybravia.BraviaTV.get_power_status", - return_value="active", - ), patch( - "pybravia.BraviaTV.get_external_status", - return_value=BRAVIA_SOURCES, - ), patch( - "pybravia.BraviaTV.send_rest_req", - return_value={}, - ): - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - with patch( - "pybravia.BraviaTV.send_rest_req", - side_effect=BraviaTVError, - ): - result = await hass.config_entries.options.async_init(config_entry.entry_id) - - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "failed_update" - - @pytest.mark.parametrize( "use_psk, new_pin", [ @@ -488,14 +395,14 @@ async def test_reauth_successful(hass, use_psk, new_pin): ) config_entry.add_to_hass(hass) - with patch("pybravia.BraviaTV.connect"), patch( - "pybravia.BraviaTV.get_power_status", + with patch("pybravia.BraviaClient.connect"), patch( + "pybravia.BraviaClient.get_power_status", return_value="active", ), patch( - "pybravia.BraviaTV.get_external_status", + "pybravia.BraviaClient.get_external_status", return_value=BRAVIA_SOURCES, ), patch( - "pybravia.BraviaTV.send_rest_req", + "pybravia.BraviaClient.send_rest_req", return_value={}, ): result = await hass.config_entries.flow.async_init( diff --git a/tests/components/bsblan/test_config_flow.py b/tests/components/bsblan/test_config_flow.py index a1286f43695..ab8d4237588 100644 --- a/tests/components/bsblan/test_config_flow.py +++ b/tests/components/bsblan/test_config_flow.py @@ -32,7 +32,6 @@ async def test_full_user_flow_implementation( 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"], diff --git a/tests/components/bthome/__init__.py b/tests/components/bthome/__init__.py index 2951413b0e6..d05c92b6902 100644 --- a/tests/components/bthome/__init__.py +++ b/tests/components/bthome/__init__.py @@ -29,7 +29,9 @@ TEMP_HUMI_ENCRYPTED_SERVICE_INFO = BluetoothServiceInfoBleak( rssi=-63, manufacturer_data={}, service_data={ - "0000181e-0000-1000-8000-00805f9b34fb": b'\xfb\xa45\xe4\xd3\xc3\x12\xfb\x00\x11"3W\xd9\n\x99' + "0000181e-0000-1000-8000-00805f9b34fb": ( + b'\xfb\xa45\xe4\xd3\xc3\x12\xfb\x00\x11"3W\xd9\n\x99' + ) }, service_uuids=["0000181e-0000-1000-8000-00805f9b34fb"], source="local", @@ -45,7 +47,9 @@ PRST_SERVICE_INFO = BluetoothServiceInfoBleak( rssi=-63, manufacturer_data={}, service_data={ - "0000181c-0000-1000-8000-00805f9b34fb": b'\x02\x14\x00\n"\x02\xdd\n\x02\x03{\x12\x02\x0c\n\x0b' + "0000181c-0000-1000-8000-00805f9b34fb": ( + b'\x02\x14\x00\n"\x02\xdd\n\x02\x03{\x12\x02\x0c\n\x0b' + ) }, service_uuids=["0000181c-0000-1000-8000-00805f9b34fb"], source="local", diff --git a/tests/components/bthome/test_sensor.py b/tests/components/bthome/test_sensor.py index 5744642e3dd..9d5a5a9f4df 100644 --- a/tests/components/bthome/test_sensor.py +++ b/tests/components/bthome/test_sensor.py @@ -278,7 +278,9 @@ _LOGGER = logging.getLogger(__name__) None, [ { - "sensor_entity": "sensor.test_device_18b2_volatile_organic_compounds", + "sensor_entity": ( + "sensor.test_device_18b2_volatile_organic_compounds" + ), "friendly_name": "Test Device 18B2 Volatile Organic Compounds", "unit_of_measurement": "µg/m³", "state_class": "measurement", @@ -630,7 +632,9 @@ async def test_v1_sensors( None, [ { - "sensor_entity": "sensor.test_device_18b2_volatile_organic_compounds", + "sensor_entity": ( + "sensor.test_device_18b2_volatile_organic_compounds" + ), "friendly_name": "Test Device 18B2 Volatile Organic Compounds", "unit_of_measurement": "µg/m³", "state_class": "measurement", diff --git a/tests/components/button/test_device_trigger.py b/tests/components/button/test_device_trigger.py index f659cc69fb0..d67cfd648c8 100644 --- a/tests/components/button/test_device_trigger.py +++ b/tests/components/button/test_device_trigger.py @@ -138,10 +138,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data": { "some": ( - "to - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }} - " - "{{ trigger.id}}" + "to - {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }} " + "- {{ trigger.id }}" ) }, }, diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index c7031fa9c04..522d5cb2e76 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -10,8 +10,6 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component from homeassistant.util import dt -# pylint: disable=redefined-outer-name - DEVICE_DATA = {"name": "Private Calendar", "device_id": "Private Calendar"} EVENTS = [ @@ -351,12 +349,12 @@ def _mocked_dav_client(*names, calendars=None): def _mock_calendar(name): + calendar = Mock() events = [] for idx, event in enumerate(EVENTS): - events.append(Event(None, "%d.ics" % idx, event, None, str(idx))) + events.append(Event(None, "%d.ics" % idx, event, calendar, str(idx))) - calendar = Mock() - calendar.date_search = MagicMock(return_value=events) + calendar.search = MagicMock(return_value=events) calendar.name = name return calendar diff --git a/tests/components/calendar/test_init.py b/tests/components/calendar/test_init.py index 38c17b15b04..e90fc3b279b 100644 --- a/tests/components/calendar/test_init.py +++ b/tests/components/calendar/test_init.py @@ -1,11 +1,18 @@ """The tests for the calendar component.""" + +from __future__ import annotations + from datetime import timedelta from http import HTTPStatus +from typing import Any from unittest.mock import patch import pytest +import voluptuous as vol from homeassistant.bootstrap import async_setup_component +from homeassistant.components.calendar import DOMAIN +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError import homeassistant.util.dt as dt_util @@ -157,3 +164,165 @@ async def test_unsupported_websocket(hass, hass_ws_client, payload, code): assert resp.get("id") == 1 assert resp.get("error") assert resp["error"].get("code") == code + + +async def test_unsupported_create_event_service(hass): + """Test unsupported service call.""" + + await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) + await hass.async_block_till_done() + + with pytest.raises(HomeAssistantError, match="does not support this service"): + await hass.services.async_call( + DOMAIN, + "create_event", + { + "start_date_time": "1997-07-14T17:00:00+00:00", + "end_date_time": "1997-07-15T04:00:00+00:00", + "summary": "Bastille Day Party", + }, + target={"entity_id": "calendar.calendar_1"}, + blocking=True, + ) + + +@pytest.mark.parametrize( + "date_fields,expected_error,error_match", + [ + ( + {}, + vol.error.MultipleInvalid, + "must contain at least one of start_date, start_date_time, in", + ), + ( + { + "start_date": "2022-04-01", + }, + vol.error.MultipleInvalid, + "Start and end dates must both be specified", + ), + ( + { + "end_date": "2022-04-02", + }, + vol.error.MultipleInvalid, + "must contain at least one of start_date, start_date_time, in.", + ), + ( + { + "start_date_time": "2022-04-01T06:00:00", + }, + vol.error.MultipleInvalid, + "Start and end datetimes must both be specified", + ), + ( + { + "end_date_time": "2022-04-02T07:00:00", + }, + vol.error.MultipleInvalid, + "must contain at least one of start_date, start_date_time, in.", + ), + ( + { + "start_date": "2022-04-01", + "start_date_time": "2022-04-01T06:00:00", + "end_date_time": "2022-04-02T07:00:00", + }, + vol.error.MultipleInvalid, + "must contain at most one of start_date, start_date_time, in.", + ), + ( + { + "start_date_time": "2022-04-01T06:00:00", + "end_date_time": "2022-04-01T07:00:00", + "end_date": "2022-04-02", + }, + vol.error.MultipleInvalid, + "Start and end dates must both be specified", + ), + ( + { + "start_date": "2022-04-01", + "end_date_time": "2022-04-02T07:00:00", + }, + vol.error.MultipleInvalid, + "Start and end dates must both be specified", + ), + ( + { + "start_date_time": "2022-04-01T07:00:00", + "end_date": "2022-04-02", + }, + vol.error.MultipleInvalid, + "Start and end dates must both be specified", + ), + ( + { + "in": { + "days": 2, + "weeks": 2, + } + }, + vol.error.MultipleInvalid, + "two or more values in the same group of exclusion 'event_types'", + ), + ( + { + "start_date": "2022-04-01", + "end_date": "2022-04-02", + "in": { + "days": 2, + }, + }, + vol.error.MultipleInvalid, + "must contain at most one of start_date, start_date_time, in.", + ), + ( + { + "start_date_time": "2022-04-01T07:00:00", + "end_date_time": "2022-04-01T07:00:00", + "in": { + "days": 2, + }, + }, + vol.error.MultipleInvalid, + "must contain at most one of start_date, start_date_time, in.", + ), + ], + ids=[ + "missing_all", + "missing_end_date", + "missing_start_date", + "missing_end_datetime", + "missing_start_datetime", + "multiple_start", + "multiple_end", + "missing_end_date", + "missing_end_date_time", + "multiple_in", + "unexpected_in_with_date", + "unexpected_in_with_datetime", + ], +) +async def test_create_event_service_invalid_params( + hass: HomeAssistant, + date_fields: dict[str, Any], + expected_error: type[Exception], + error_match: str | None, +): + """Test creating an event using the create_event service.""" + + await async_setup_component(hass, "calendar", {"calendar": {"platform": "demo"}}) + await hass.async_block_till_done() + + with pytest.raises(expected_error, match=error_match): + await hass.services.async_call( + "calendar", + "create_event", + { + "summary": "Bastille Day Party", + **date_fields, + }, + target={"entity_id": "calendar.calendar_1"}, + blocking=True, + ) diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index d9946cab6a0..0a895dbe297 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -14,7 +14,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component @@ -53,7 +53,7 @@ async def test_sensors_pro(hass, canary) -> None: "home_dining_room_temperature": ( "20_temperature", "21.12", - TEMP_CELSIUS, + UnitOfTemperature.CELSIUS, SensorDeviceClass.TEMPERATURE, None, ), diff --git a/tests/components/cast/conftest.py b/tests/components/cast/conftest.py index 52152b4a718..b207b77b46a 100644 --- a/tests/components/cast/conftest.py +++ b/tests/components/cast/conftest.py @@ -1,5 +1,5 @@ """Test fixtures for the cast integration.""" -# pylint: disable=protected-access + from unittest.mock import AsyncMock, MagicMock, patch import pychromecast diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index 97218a396dd..f40e9701348 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -5,6 +5,7 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components import cast +from homeassistant.components.cast.home_assistant_cast import CAST_USER_NAME from tests.common import MockConfigEntry @@ -64,7 +65,7 @@ async def test_user_setup(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) users = await hass.auth.async_get_users() - assert len(users) == 1 + assert next(user for user in users if user.name == CAST_USER_NAME) assert result["type"] == "create_entry" assert result["result"].data == { "ignore_cec": [], @@ -86,7 +87,7 @@ async def test_user_setup_options(hass): ) users = await hass.auth.async_get_users() - assert len(users) == 1 + assert next(user for user in users if user.name == CAST_USER_NAME) assert result["type"] == "create_entry" assert result["result"].data == { "ignore_cec": [], @@ -106,7 +107,7 @@ async def test_zeroconf_setup(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) users = await hass.auth.async_get_users() - assert len(users) == 1 + assert next(user for user in users if user.name == CAST_USER_NAME) assert result["type"] == "create_entry" assert result["result"].data == { "ignore_cec": [], @@ -126,7 +127,7 @@ async def test_zeroconf_setup_onboarding(hass): ) users = await hass.auth.async_get_users() - assert len(users) == 1 + assert next(user for user in users if user.name == CAST_USER_NAME) assert result["type"] == "create_entry" assert result["result"].data == { "ignore_cec": [], diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 4df11e49ad5..74ecd4a36fd 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -1,5 +1,5 @@ """The tests for the Cast Media player platform.""" -# pylint: disable=protected-access + from __future__ import annotations import asyncio @@ -992,7 +992,9 @@ async def test_entity_browse_media(hass: HomeAssistant, hass_ws_client): "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", + "media_content_id": ( + "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4" + ), "can_play": True, "can_expand": False, "thumbnail": None, @@ -1048,7 +1050,9 @@ async def test_entity_browse_media_audio_only( "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", + "media_content_id": ( + "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4" + ), "can_play": True, "can_expand": False, "thumbnail": None, @@ -1254,10 +1258,17 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistant, quick_play_mock): ), # Test HLS playlist is forwarded to the device ( - "http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_low/ak/bbc_radio_fourfm.m3u8", + ( + "http://a.files.bbci.co.uk/media/live/manifesto" + "/audio/simulcast/hls/nonuk/sbr_low/ak/bbc_radio_fourfm.m3u8" + ), "bbc_radio_fourfm.m3u8", { - "media_id": "http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_low/ak/bbc_radio_fourfm.m3u8", + "media_id": ( + "http://a.files.bbci.co.uk/media/live/manifesto" + "/audio/simulcast/hls/nonuk/sbr_low/ak" + "/bbc_radio_fourfm.m3u8" + ), "media_type": "audio", }, ), @@ -2237,7 +2248,9 @@ async def test_cast_platform_play_media_local_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", + media_player.ATTR_MEDIA_CONTENT_ID: ( + f"{network.get_url(hass)}/api/hls/bla/master_playlist.m3u8?token=bla" + ), }, blocking=True, ) diff --git a/tests/components/climate/test_device_condition.py b/tests/components/climate/test_device_condition.py index 72298042ee8..2b21b7c4185 100644 --- a/tests/components/climate/test_device_condition.py +++ b/tests/components/climate/test_device_condition.py @@ -173,7 +173,10 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_hvac_mode - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_hvac_mode - {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -192,7 +195,10 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_preset_mode - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_preset_mode - {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py index 00099538a6f..48399f2373e 100644 --- a/tests/components/climate/test_device_trigger.py +++ b/tests/components/climate/test_device_trigger.py @@ -11,7 +11,7 @@ from homeassistant.components.climate import ( device_trigger, ) from homeassistant.components.device_automation import DeviceAutomationType -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import UnitOfTemperature from homeassistant.helpers import config_validation as cv, device_registry from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_registry import RegistryEntryHider @@ -311,13 +311,13 @@ async def test_get_trigger_capabilities_temp_humid(hass, type): capabilities["extra_fields"], custom_serializer=cv.custom_serializer ) == [ { - "description": {"suffix": TEMP_CELSIUS}, + "description": {"suffix": UnitOfTemperature.CELSIUS}, "name": "above", "optional": True, "type": "float", }, { - "description": {"suffix": TEMP_CELSIUS}, + "description": {"suffix": UnitOfTemperature.CELSIUS}, "name": "below", "optional": True, "type": "float", diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 0dbc20d4f91..5a3fb1bf7f1 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -518,8 +518,10 @@ async def test_websocket_update_preferences_alexa_report_state( client = await hass_ws_client(hass) with patch( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" - ".async_get_access_token", + ( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" + ".async_get_access_token" + ), ), patch( "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.set_authorized" ) as set_authorized_mock: @@ -540,8 +542,10 @@ async def test_websocket_update_preferences_require_relink( client = await hass_ws_client(hass) with patch( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" - ".async_get_access_token", + ( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" + ".async_get_access_token" + ), side_effect=alexa_errors.RequireRelink, ), patch( "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.set_authorized" @@ -564,8 +568,10 @@ async def test_websocket_update_preferences_no_token( client = await hass_ws_client(hass) with patch( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" - ".async_get_access_token", + ( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" + ".async_get_access_token" + ), side_effect=alexa_errors.NoTokenAvailable, ), patch( "homeassistant.components.cloud.alexa_config.CloudAlexaConfig.set_authorized" @@ -770,8 +776,10 @@ async def test_sync_alexa_entities_timeout( """Test that timeout syncing Alexa entities.""" client = await hass_ws_client(hass) with patch( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" - ".async_sync_entities", + ( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" + ".async_sync_entities" + ), side_effect=asyncio.TimeoutError, ): await client.send_json({"id": 5, "type": "cloud/alexa/sync"}) @@ -787,8 +795,10 @@ async def test_sync_alexa_entities_no_token( """Test sync Alexa entities when we have no token.""" client = await hass_ws_client(hass) with patch( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" - ".async_sync_entities", + ( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" + ".async_sync_entities" + ), side_effect=alexa_errors.NoTokenAvailable, ): await client.send_json({"id": 5, "type": "cloud/alexa/sync"}) @@ -804,8 +814,10 @@ async def test_enable_alexa_state_report_fail( """Test enable Alexa entities state reporting when no token available.""" client = await hass_ws_client(hass) with patch( - "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" - ".async_sync_entities", + ( + "homeassistant.components.cloud.alexa_config.CloudAlexaConfig" + ".async_sync_entities" + ), side_effect=alexa_errors.NoTokenAvailable, ): await client.send_json({"id": 5, "type": "cloud/alexa/sync"}) diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index bdb36eebaa1..51415d8c42d 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -144,8 +144,10 @@ async def test_update_with_json_attrs(hass: HomeAssistant) -> None: await setup_test_entities( hass, { - "command": 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\":\ - \\"another_json_value\\", \\"key_three\\": \\"value_three\\" }', + "command": ( + 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\": ' + '\\"another_json_value\\", \\"key_three\\": \\"value_three\\" }' + ), "json_attributes": ["key", "another_key", "key_three"], }, ) @@ -218,8 +220,10 @@ async def test_update_with_missing_json_attrs( await setup_test_entities( hass, { - "command": 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\":\ - \\"another_json_value\\", \\"key_three\\": \\"value_three\\" }', + "command": ( + 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\": ' + '\\"another_json_value\\", \\"key_three\\": \\"value_three\\" }' + ), "json_attributes": ["key", "another_key", "key_three", "missing_key"], }, ) @@ -239,8 +243,10 @@ async def test_update_with_unnecessary_json_attrs( await setup_test_entities( hass, { - "command": 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\":\ - \\"another_json_value\\", \\"key_three\\": \\"value_three\\" }', + "command": ( + 'echo { \\"key\\": \\"some_json_value\\", \\"another_key\\": ' + '\\"another_json_value\\", \\"key_three\\": \\"value_three\\" }' + ), "json_attributes": ["key", "another_key"], }, ) diff --git a/tests/components/command_line/test_switch.py b/tests/components/command_line/test_switch.py index 267f7cf7b06..01ccb832e15 100644 --- a/tests/components/command_line/test_switch.py +++ b/tests/components/command_line/test_switch.py @@ -93,7 +93,9 @@ async def test_state_value(hass: HomeAssistant) -> None: "command_on": f"echo 1 > {path}", "command_off": f"echo 0 > {path}", "value_template": '{{ value=="1" }}', - "icon_template": '{% if value=="1" %} mdi:on {% else %} mdi:off {% endif %}', + "icon_template": ( + '{% if value=="1" %} mdi:on {% else %} mdi:off {% endif %}' + ), } }, ) @@ -142,7 +144,10 @@ async def test_state_json_value(hass: HomeAssistant) -> None: "command_on": f"echo '{oncmd}' > {path}", "command_off": f"echo '{offcmd}' > {path}", "value_template": '{{ value_json.status=="ok" }}', - "icon_template": '{% if value_json.status=="ok" %} mdi:on {% else %} mdi:off {% endif %}', + "icon_template": ( + '{% if value_json.status=="ok" %} mdi:on' + "{% else %} mdi:off {% endif %}" + ), } }, ) diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 8ebf59323c8..5bbbb483f64 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -929,8 +929,10 @@ async def test_options_flow_with_invalid_data(hass, client): assert resp.status == HTTPStatus.BAD_REQUEST data = await resp.json() assert data == { - "message": "User input malformed: invalid is not a valid option for " - "dictionary value @ data['choices']" + "message": ( + "User input malformed: invalid is not a valid option for " + "dictionary value @ data['choices']" + ) } diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index 4f47e463751..487658ddb27 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -85,7 +85,10 @@ async def test_list_devices(hass, client, registry): }, ] - registry.async_remove_device(device2.id) + class Unserializable: + """Good luck serializing me.""" + + registry.async_update_device(device2.id, name=Unserializable()) await hass.async_block_till_done() await client.send_json({"id": 6, "type": "config/device_registry/list"}) @@ -111,6 +114,9 @@ async def test_list_devices(hass, client, registry): } ] + # Remove the bad device to avoid errors when test is being torn down + registry.async_remove_device(device2.id) + @pytest.mark.parametrize( "payload_key,payload_value", diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 9caa9cbf3f2..38984d74057 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -6,7 +6,6 @@ from homeassistant.components.config import entity_registry from homeassistant.const import ATTR_ICON from homeassistant.helpers.device_registry import DeviceEntryDisabler from homeassistant.helpers.entity_registry import ( - EVENT_ENTITY_REGISTRY_UPDATED, RegistryEntry, RegistryEntryDisabler, RegistryEntryHider, @@ -95,6 +94,9 @@ async def test_list_entities(hass, client): }, ] + class Unserializable: + """Good luck serializing me.""" + mock_registry( hass, { @@ -104,13 +106,15 @@ async def test_list_entities(hass, client): platform="test_platform", name="Hello World", ), + "test_domain.name_2": RegistryEntry( + entity_id="test_domain.name_2", + unique_id="6789", + platform="test_platform", + name=Unserializable(), + ), }, ) - hass.bus.async_fire( - EVENT_ENTITY_REGISTRY_UPDATED, - {"action": "create", "entity_id": "test_domain.no_name"}, - ) await client.send_json({"id": 6, "type": "config/entity_registry/list"}) msg = await client.receive_json() @@ -217,6 +221,89 @@ async def test_get_entity(hass, client): } +async def test_get_entities(hass, client): + """Test get entry.""" + mock_registry( + hass, + { + "test_domain.name": RegistryEntry( + entity_id="test_domain.name", + unique_id="1234", + platform="test_platform", + name="Hello World", + ), + "test_domain.no_name": RegistryEntry( + entity_id="test_domain.no_name", + unique_id="6789", + platform="test_platform", + ), + }, + ) + + await client.send_json( + { + "id": 5, + "type": "config/entity_registry/get_entries", + "entity_ids": [ + "test_domain.name", + "test_domain.no_name", + "test_domain.no_such_entity", + ], + } + ) + msg = await client.receive_json() + + assert msg["result"] == { + "test_domain.name": { + "aliases": [], + "area_id": None, + "capabilities": None, + "config_entry_id": None, + "device_class": None, + "device_id": None, + "disabled_by": None, + "entity_category": None, + "entity_id": "test_domain.name", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "id": ANY, + "name": "Hello World", + "options": {}, + "original_device_class": None, + "original_icon": None, + "original_name": None, + "platform": "test_platform", + "translation_key": None, + "unique_id": "1234", + }, + "test_domain.no_name": { + "aliases": [], + "area_id": None, + "capabilities": None, + "config_entry_id": None, + "device_class": None, + "device_id": None, + "disabled_by": None, + "entity_category": None, + "entity_id": "test_domain.no_name", + "has_entity_name": False, + "hidden_by": None, + "icon": None, + "id": ANY, + "name": None, + "options": {}, + "original_device_class": None, + "original_icon": None, + "original_name": None, + "platform": "test_platform", + "translation_key": None, + "unique_id": "6789", + }, + "test_domain.no_such_entity": None, + } + + async def test_update_entity(hass, client): """Test updating entity.""" registry = mock_registry( diff --git a/tests/components/conversation/__init__.py b/tests/components/conversation/__init__.py index ea244c00df8..8c5371f8cbe 100644 --- a/tests/components/conversation/__init__.py +++ b/tests/components/conversation/__init__.py @@ -1 +1,25 @@ """Tests for the conversation component.""" +from __future__ import annotations + +from homeassistant.components import conversation +from homeassistant.helpers import intent + + +class MockAgent(conversation.AbstractConversationAgent): + """Test Agent.""" + + def __init__(self) -> None: + """Initialize the agent.""" + self.calls = [] + self.response = "Test response" + + async def async_process( + self, user_input: conversation.ConversationInput + ) -> conversation.ConversationResult: + """Process some text.""" + self.calls.append(user_input) + response = intent.IntentResponse(language=user_input.language) + response.async_set_speech(self.response) + return conversation.ConversationResult( + response=response, conversation_id=user_input.conversation_id + ) diff --git a/tests/components/conversation/conftest.py b/tests/components/conversation/conftest.py new file mode 100644 index 00000000000..35f9937e5a0 --- /dev/null +++ b/tests/components/conversation/conftest.py @@ -0,0 +1,15 @@ +"""Conversation test helpers.""" + +import pytest + +from homeassistant.components import conversation + +from . import MockAgent + + +@pytest.fixture +def mock_agent(hass): + """Mock agent.""" + agent = MockAgent() + conversation.async_set_agent(hass, None, agent) + return agent diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py new file mode 100644 index 00000000000..591802f5888 --- /dev/null +++ b/tests/components/conversation/test_default_agent.py @@ -0,0 +1,44 @@ +"""Test for the default agent.""" +import pytest + +from homeassistant.components import conversation +from homeassistant.core import DOMAIN as HASS_DOMAIN, Context +from homeassistant.helpers import entity, entity_registry, intent +from homeassistant.setup import async_setup_component + +from tests.common import async_mock_service + + +@pytest.fixture +async def init_components(hass): + """Initialize relevant components with empty configs.""" + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "conversation", {}) + assert await async_setup_component(hass, "intent", {}) + + +@pytest.mark.parametrize( + "er_kwargs", + [ + {"hidden_by": entity_registry.RegistryEntryHider.USER}, + {"hidden_by": entity_registry.RegistryEntryHider.INTEGRATION}, + {"entity_category": entity.EntityCategory.CONFIG}, + {"entity_category": entity.EntityCategory.DIAGNOSTIC}, + ], +) +async def test_hidden_entities_skipped(hass, init_components, er_kwargs): + """Test we skip hidden entities.""" + + er = entity_registry.async_get(hass) + er.async_get_or_create( + "light", "demo", "1234", suggested_object_id="Test light", **er_kwargs + ) + hass.states.async_set("light.test_light", "off") + calls = async_mock_service(hass, HASS_DOMAIN, "turn_on") + result = await conversation.async_converse( + hass, "turn on test light", None, Context(), None + ) + + assert len(calls) == 0 + assert result.response.response_type == intent.IntentResponseType.ERROR + assert result.response.error_code == intent.IntentResponseErrorCode.NO_INTENT_MATCH diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index c25df45ce78..54fed8a6139 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -1,15 +1,35 @@ """The tests for the Conversation component.""" from http import HTTPStatus -from unittest.mock import ANY, patch +from unittest.mock import patch import pytest from homeassistant.components import conversation +from homeassistant.components.cover import SERVICE_OPEN_COVER +from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import DOMAIN as HASS_DOMAIN, Context -from homeassistant.helpers import intent +from homeassistant.helpers import ( + area_registry, + device_registry, + entity_registry, + intent, +) from homeassistant.setup import async_setup_component -from tests.common import async_mock_intent, async_mock_service +from tests.common import MockConfigEntry, async_mock_service + + +class OrderBeerIntentHandler(intent.IntentHandler): + """Handle OrderBeer intent.""" + + intent_type = "OrderBeer" + + async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: + """Return speech response.""" + beer_style = intent_obj.slots["beer_style"]["value"] + response = intent_obj.create_response() + response.async_set_speech(f"You ordered a {beer_style}") + return response @pytest.fixture @@ -20,191 +40,19 @@ async def init_components(hass): assert await async_setup_component(hass, "intent", {}) -async def test_calling_intent(hass): - """Test calling an intent from a conversation.""" - intents = async_mock_intent(hass, "OrderBeer") - - result = await async_setup_component(hass, "homeassistant", {}) - assert result - - result = await async_setup_component( - hass, - "conversation", - {"conversation": {"intents": {"OrderBeer": ["I would like the {type} beer"]}}}, - ) - assert result - - context = Context() - - await hass.services.async_call( - "conversation", - "process", - {conversation.ATTR_TEXT: "I would like the Grolsch beer"}, - context=context, - ) - await hass.async_block_till_done() - - assert len(intents) == 1 - intent = intents[0] - assert intent.platform == "conversation" - assert intent.intent_type == "OrderBeer" - assert intent.slots == {"type": {"value": "Grolsch"}} - assert intent.text_input == "I would like the Grolsch beer" - assert intent.context is context - - -async def test_register_before_setup(hass): - """Test calling an intent from a conversation.""" - intents = async_mock_intent(hass, "OrderBeer") - - hass.components.conversation.async_register("OrderBeer", ["A {type} beer, please"]) - - result = await async_setup_component( - hass, - "conversation", - {"conversation": {"intents": {"OrderBeer": ["I would like the {type} beer"]}}}, - ) - assert result - - await hass.services.async_call( - "conversation", "process", {conversation.ATTR_TEXT: "A Grolsch beer, please"} - ) - await hass.async_block_till_done() - - assert len(intents) == 1 - intent = intents[0] - assert intent.platform == "conversation" - assert intent.intent_type == "OrderBeer" - assert intent.slots == {"type": {"value": "Grolsch"}} - assert intent.text_input == "A Grolsch beer, please" - - await hass.services.async_call( - "conversation", - "process", - {conversation.ATTR_TEXT: "I would like the Grolsch beer"}, - ) - await hass.async_block_till_done() - - assert len(intents) == 2 - intent = intents[1] - assert intent.platform == "conversation" - assert intent.intent_type == "OrderBeer" - assert intent.slots == {"type": {"value": "Grolsch"}} - assert intent.text_input == "I would like the Grolsch beer" - - -async def test_http_processing_intent(hass, hass_client, hass_admin_user): +async def test_http_processing_intent( + hass, init_components, hass_client, hass_admin_user +): """Test processing intent via HTTP API.""" - - class TestIntentHandler(intent.IntentHandler): - """Test Intent Handler.""" - - intent_type = "OrderBeer" - - async def async_handle(self, intent): - """Handle the intent.""" - assert intent.context.user_id == hass_admin_user.id - response = intent.create_response() - response.async_set_speech( - "I've ordered a {}!".format(intent.slots["type"]["value"]) - ) - response.async_set_card( - "Beer ordered", "You chose a {}.".format(intent.slots["type"]["value"]) - ) - return response - - intent.async_register(hass, TestIntentHandler()) - - result = await async_setup_component( - hass, - "conversation", - {"conversation": {"intents": {"OrderBeer": ["I would like the {type} beer"]}}}, - ) - assert result + # Add an alias + entities = entity_registry.async_get(hass) + entities.async_get_or_create("light", "demo", "1234", suggested_object_id="kitchen") + entities.async_update_entity("light.kitchen", aliases={"my cool light"}) + hass.states.async_set("light.kitchen", "off") client = await hass_client() resp = await client.post( - "/api/conversation/process", json={"text": "I would like the Grolsch beer"} - ) - - assert resp.status == HTTPStatus.OK - data = await resp.json() - - assert data == { - "response": { - "response_type": "action_done", - "card": { - "simple": {"content": "You chose a Grolsch.", "title": "Beer ordered"} - }, - "speech": { - "plain": { - "extra_data": None, - "speech": "I've ordered a Grolsch!", - } - }, - "language": hass.config.language, - "data": {"targets": [], "success": [], "failed": []}, - }, - "conversation_id": None, - } - - -async def test_http_failed_action(hass, hass_client, hass_admin_user): - """Test processing intent via HTTP API with a partial completion.""" - - class TestIntentHandler(intent.IntentHandler): - """Test Intent Handler.""" - - intent_type = "TurnOffLights" - - async def async_handle(self, handle_intent: intent.Intent): - """Handle the intent.""" - response = handle_intent.create_response() - area = handle_intent.slots["area"]["value"] - - # Mark some targets as successful, others as failed - response.async_set_targets( - intent_targets=[ - intent.IntentResponseTarget( - type=intent.IntentResponseTargetType.AREA, name=area, id=area - ) - ] - ) - response.async_set_results( - success_results=[ - intent.IntentResponseTarget( - type=intent.IntentResponseTargetType.ENTITY, - name="light1", - id="light.light1", - ) - ], - failed_results=[ - intent.IntentResponseTarget( - type=intent.IntentResponseTargetType.ENTITY, - name="light2", - id="light.light2", - ) - ], - ) - - return response - - intent.async_register(hass, TestIntentHandler()) - - result = await async_setup_component( - hass, - "conversation", - { - "conversation": { - "intents": {"TurnOffLights": ["turn off the lights in the {area}"]} - } - }, - ) - assert result - - client = await hass_client() - resp = await client.post( - "/api/conversation/process", json={"text": "Turn off the lights in the kitchen"} + "/api/conversation/process", json={"text": "turn on my cool light"} ) assert resp.status == HTTPStatus.OK @@ -214,18 +62,162 @@ async def test_http_failed_action(hass, hass_client, hass_admin_user): "response": { "response_type": "action_done", "card": {}, - "speech": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on my cool light", + } + }, "language": hass.config.language, "data": { - "targets": [{"type": "area", "id": "kitchen", "name": "kitchen"}], - "success": [{"type": "entity", "id": "light.light1", "name": "light1"}], - "failed": [{"type": "entity", "id": "light.light2", "name": "light2"}], + "targets": [], + "success": [ + {"id": "light.kitchen", "name": "kitchen", "type": "entity"} + ], + "failed": [], }, }, "conversation_id": None, } +async def test_http_processing_intent_entity_added( + hass, init_components, hass_client, hass_admin_user +): + """Test processing intent via HTTP API with entities added later. + + We want to ensure that adding an entity later busts the cache + so that the new entity is available as well as any aliases. + """ + er = entity_registry.async_get(hass) + er.async_get_or_create("light", "demo", "1234", suggested_object_id="kitchen") + er.async_update_entity("light.kitchen", aliases={"my cool light"}) + hass.states.async_set("light.kitchen", "off") + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on my cool light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on my cool light", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [ + {"id": "light.kitchen", "name": "kitchen", "type": "entity"} + ], + "failed": [], + }, + }, + "conversation_id": None, + } + + # Add an alias + er.async_get_or_create("light", "demo", "5678", suggested_object_id="late") + hass.states.async_set("light.late", "off", {"friendly_name": "friendly light"}) + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on friendly light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on friendly light", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [ + {"id": "light.late", "name": "friendly light", "type": "entity"} + ], + "failed": [], + }, + }, + "conversation_id": None, + } + + # Now add an alias + er.async_update_entity("light.late", aliases={"late added light"}) + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on late added light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Turned on late added light", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [ + {"id": "light.late", "name": "friendly light", "type": "entity"} + ], + "failed": [], + }, + }, + "conversation_id": None, + } + + # Now delete the entity + er.async_remove("light.late") + + client = await hass_client() + resp = await client.post( + "/api/conversation/process", json={"text": "turn on late added light"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + assert data == { + "conversation_id": None, + "response": { + "card": {}, + "data": {"code": "no_intent_match"}, + "language": hass.config.language, + "response_type": "error", + "speech": { + "plain": { + "extra_data": None, + "speech": "Sorry, I couldn't understand " "that", + } + }, + }, + } + + @pytest.mark.parametrize("sentence", ("turn on kitchen", "turn kitchen on")) async def test_turn_on_intent(hass, init_components, sentence): """Test calling the turn on intent.""" @@ -262,24 +254,6 @@ async def test_turn_off_intent(hass, init_components, sentence): assert call.data == {"entity_id": "light.kitchen"} -@pytest.mark.parametrize("sentence", ("toggle kitchen", "kitchen toggle")) -async def test_toggle_intent(hass, init_components, sentence): - """Test calling the turn on intent.""" - hass.states.async_set("light.kitchen", "on") - calls = async_mock_service(hass, HASS_DOMAIN, "toggle") - - await hass.services.async_call( - "conversation", "process", {conversation.ATTR_TEXT: sentence} - ) - await hass.async_block_till_done() - - assert len(calls) == 1 - call = calls[0] - assert call.domain == HASS_DOMAIN - assert call.service == "toggle" - assert call.data == {"entity_id": "light.kitchen"} - - async def test_http_api(hass, init_components, hass_client): """Test the HTTP conversation API.""" client = await hass_client() @@ -295,7 +269,7 @@ async def test_http_api(hass, init_components, hass_client): assert data == { "response": { "card": {}, - "speech": {"plain": {"extra_data": None, "speech": "Turned kitchen on"}}, + "speech": {"plain": {"extra_data": None, "speech": "Turned on kitchen"}}, "language": hass.config.language, "response_type": "action_done", "data": { @@ -324,38 +298,9 @@ async def test_http_api_no_match(hass, init_components, hass_client): """Test the HTTP conversation API with an intent match failure.""" client = await hass_client() - # Sentence should not match any intents + # Shouldn't match any intents resp = await client.post("/api/conversation/process", json={"text": "do something"}) - assert resp.status == HTTPStatus.OK - data = await resp.json() - assert data == { - "response": { - "card": {}, - "speech": { - "plain": { - "extra_data": None, - "speech": "Sorry, I didn't understand that", - }, - }, - "language": hass.config.language, - "response_type": "error", - "data": { - "code": "no_intent_match", - }, - }, - "conversation_id": None, - } - - -async def test_http_api_no_valid_targets(hass, init_components, hass_client): - """Test the HTTP conversation API with no valid targets.""" - client = await hass_client() - - # No kitchen light - resp = await client.post( - "/api/conversation/process", json={"text": "turn on the kitchen"} - ) assert resp.status == HTTPStatus.OK data = await resp.json() @@ -365,14 +310,12 @@ async def test_http_api_no_valid_targets(hass, init_components, hass_client): "card": {}, "speech": { "plain": { + "speech": "Sorry, I couldn't understand that", "extra_data": None, - "speech": "Unable to find an entity called kitchen", }, }, "language": hass.config.language, - "data": { - "code": "no_valid_targets", - }, + "data": {"code": "no_intent_match"}, }, "conversation_id": None, } @@ -384,11 +327,9 @@ async def test_http_api_handle_failure(hass, init_components, hass_client): hass.states.async_set("light.kitchen", "off") - # Raise an "unexpected" error during intent handling + # Raise an error during intent handling def async_handle_error(*args, **kwargs): - raise intent.IntentUnexpectedError( - "Unexpected error turning on the kitchen light" - ) + raise intent.IntentHandleError() with patch("homeassistant.helpers.intent.async_handle", new=async_handle_error): resp = await client.post( @@ -405,7 +346,7 @@ async def test_http_api_handle_failure(hass, init_components, hass_client): "speech": { "plain": { "extra_data": None, - "speech": "Unexpected error turning on the kitchen light", + "speech": "An unexpected error occurred while handling the intent", } }, "language": hass.config.language, @@ -417,6 +358,43 @@ async def test_http_api_handle_failure(hass, init_components, hass_client): } +async def test_http_api_unexpected_failure(hass, init_components, hass_client): + """Test the HTTP conversation API with an unexpected error during handling.""" + client = await hass_client() + + hass.states.async_set("light.kitchen", "off") + + # Raise an "unexpected" error during intent handling + def async_handle_error(*args, **kwargs): + raise intent.IntentUnexpectedError() + + with patch("homeassistant.helpers.intent.async_handle", new=async_handle_error): + resp = await client.post( + "/api/conversation/process", json={"text": "turn on the kitchen"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "response_type": "error", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "An unexpected error occurred while handling the intent", + } + }, + "language": hass.config.language, + "data": { + "code": "unknown", + }, + }, + "conversation_id": None, + } + + async def test_http_api_wrong_data(hass, init_components, hass_client): """Test the HTTP conversation API.""" client = await hass_client() @@ -428,25 +406,8 @@ async def test_http_api_wrong_data(hass, init_components, hass_client): assert resp.status == HTTPStatus.BAD_REQUEST -async def test_custom_agent(hass, hass_client, hass_admin_user): +async def test_custom_agent(hass, hass_client, hass_admin_user, mock_agent): """Test a custom conversation agent.""" - - calls = [] - - class MyAgent(conversation.AbstractConversationAgent): - """Test Agent.""" - - async def async_process(self, text, context, conversation_id, language): - """Process some text.""" - calls.append((text, context, conversation_id, language)) - response = intent.IntentResponse(language=language) - response.async_set_speech("Test response") - return conversation.ConversationResult( - response=response, conversation_id=conversation_id - ) - - conversation.async_set_agent(hass, MyAgent()) - assert await async_setup_component(hass, "conversation", {}) client = await hass_client() @@ -476,11 +437,11 @@ async def test_custom_agent(hass, hass_client, hass_admin_user): "conversation_id": "test-conv-id", } - assert len(calls) == 1 - assert calls[0][0] == "Test Text" - assert calls[0][1].user_id == hass_admin_user.id - assert calls[0][2] == "test-conv-id" - assert calls[0][3] == "test-language" + assert len(mock_agent.calls) == 1 + assert mock_agent.calls[0].text == "Test Text" + assert mock_agent.calls[0].context.user_id == hass_admin_user.id + assert mock_agent.calls[0].conversation_id == "test-conv-id" + assert mock_agent.calls[0].language == "test-language" @pytest.mark.parametrize( @@ -525,11 +486,343 @@ async def test_ws_api(hass, hass_ws_client, payload): "speech": { "plain": { "extra_data": None, - "speech": "Sorry, I didn't understand that", + "speech": "Sorry, I couldn't understand that", } }, "language": payload.get("language", hass.config.language), "data": {"code": "no_intent_match"}, }, - "conversation_id": payload.get("conversation_id") or ANY, + "conversation_id": None, } + + +async def test_ws_prepare(hass, hass_ws_client): + """Test the Websocket prepare conversation API.""" + assert await async_setup_component(hass, "conversation", {}) + agent = await conversation._get_agent(hass) + assert isinstance(agent, conversation.DefaultAgent) + + # No intents should be loaded yet + assert not agent._lang_intents.get(hass.config.language) + + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 5, + "type": "conversation/prepare", + } + ) + + msg = await client.receive_json() + + assert msg["success"] + assert msg["id"] == 5 + + # Intents should now be load + assert agent._lang_intents.get(hass.config.language) + + +async def test_custom_sentences(hass, hass_client, hass_admin_user): + """Test custom sentences with a custom intent.""" + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "conversation", {}) + assert await async_setup_component(hass, "intent", {}) + + # Expecting testing_config/custom_sentences/en/beer.yaml + intent.async_register(hass, OrderBeerIntentHandler()) + + # Invoke intent via HTTP API + client = await hass_client() + for beer_style in ("stout", "lager"): + resp = await client.post( + "/api/conversation/process", + json={"text": f"I'd like to order a {beer_style}, please"}, + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": f"You ordered a {beer_style}", + } + }, + "language": hass.config.language, + "response_type": "action_done", + "data": { + "targets": [], + "success": [], + "failed": [], + }, + }, + "conversation_id": None, + } + + +async def test_custom_sentences_config(hass, hass_client, hass_admin_user): + """Test custom sentences with a custom intent in config.""" + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component( + hass, + "conversation", + {"conversation": {"intents": {"StealthMode": ["engage stealth mode"]}}}, + ) + assert await async_setup_component(hass, "intent", {}) + assert await async_setup_component( + hass, + "intent_script", + { + "intent_script": { + "StealthMode": {"speech": {"text": "Stealth mode engaged"}} + } + }, + ) + + # Invoke intent via HTTP API + client = await hass_client() + resp = await client.post( + "/api/conversation/process", + json={"text": "engage stealth mode"}, + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Stealth mode engaged", + } + }, + "language": hass.config.language, + "response_type": "action_done", + "data": { + "targets": [], + "success": [], + "failed": [], + }, + }, + "conversation_id": None, + } + + +async def test_prepare_reload(hass): + """Test calling the reload service.""" + language = hass.config.language + assert await async_setup_component(hass, "conversation", {}) + + # Load intents + agent = await conversation._get_agent(hass) + assert isinstance(agent, conversation.DefaultAgent) + await agent.async_prepare(language) + + # Confirm intents are loaded + assert agent._lang_intents.get(language) + + # Clear cache + await hass.services.async_call("conversation", "reload", {}) + await hass.async_block_till_done() + + # Confirm intent cache is cleared + assert not agent._lang_intents.get(language) + + +async def test_prepare_fail(hass): + """Test calling prepare with a non-existent language.""" + assert await async_setup_component(hass, "conversation", {}) + + # Load intents + agent = await conversation._get_agent(hass) + assert isinstance(agent, conversation.DefaultAgent) + await agent.async_prepare("not-a-language") + + # Confirm no intents were loaded + assert not agent._lang_intents.get("not-a-language") + + +async def test_language_region(hass, init_components): + """Test calling the turn on intent.""" + hass.states.async_set("light.kitchen", "off") + calls = async_mock_service(hass, HASS_DOMAIN, "turn_on") + + # Add fake region + language = f"{hass.config.language}-YZ" + await hass.services.async_call( + "conversation", + "process", + { + conversation.ATTR_TEXT: "turn on the kitchen", + conversation.ATTR_LANGUAGE: language, + }, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == "turn_on" + assert call.data == {"entity_id": "light.kitchen"} + + +async def test_reload_on_new_component(hass): + """Test intents being reloaded when a new component is loaded.""" + language = hass.config.language + assert await async_setup_component(hass, "conversation", {}) + + # Load intents + agent = await conversation._get_agent(hass) + assert isinstance(agent, conversation.DefaultAgent) + await agent.async_prepare() + + lang_intents = agent._lang_intents.get(language) + assert lang_intents is not None + loaded_components = set(lang_intents.loaded_components) + + # Load another component + assert await async_setup_component(hass, "light", {}) + + # Intents should reload + await agent.async_prepare() + lang_intents = agent._lang_intents.get(language) + assert lang_intents is not None + + assert {"light"} == (lang_intents.loaded_components - loaded_components) + + +async def test_non_default_response(hass, init_components): + """Test intent response that is not the default.""" + hass.states.async_set("cover.front_door", "closed") + async_mock_service(hass, "cover", SERVICE_OPEN_COVER) + + agent = await conversation._get_agent(hass) + assert isinstance(agent, conversation.DefaultAgent) + + result = await agent.async_process( + conversation.ConversationInput( + text="open the front door", + context=Context(), + conversation_id=None, + language=hass.config.language, + ) + ) + assert result.response.speech["plain"]["speech"] == "Opened front door" + + +async def test_turn_on_area(hass, init_components): + """Test turning on an area.""" + er = entity_registry.async_get(hass) + dr = device_registry.async_get(hass) + ar = area_registry.async_get(hass) + entry = MockConfigEntry(domain="test") + + device = dr.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + kitchen_area = ar.async_create("kitchen") + dr.async_update_device(device.id, area_id=kitchen_area.id) + + er.async_get_or_create("light", "demo", "1234", suggested_object_id="stove") + er.async_update_entity( + "light.stove", aliases={"my stove light"}, area_id=kitchen_area.id + ) + hass.states.async_set("light.stove", "off") + + calls = async_mock_service(hass, HASS_DOMAIN, "turn_on") + + await hass.services.async_call( + "conversation", + "process", + {conversation.ATTR_TEXT: "turn on lights in the kitchen"}, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == "turn_on" + assert call.data == {"entity_id": "light.stove"} + + basement_area = ar.async_create("basement") + dr.async_update_device(device.id, area_id=basement_area.id) + er.async_update_entity("light.stove", area_id=basement_area.id) + calls.clear() + + # Test that the area is updated + await hass.services.async_call( + "conversation", + "process", + {conversation.ATTR_TEXT: "turn on lights in the kitchen"}, + ) + await hass.async_block_till_done() + + assert len(calls) == 0 + + # Test the new area works + await hass.services.async_call( + "conversation", + "process", + {conversation.ATTR_TEXT: "turn on lights in the basement"}, + ) + await hass.async_block_till_done() + + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == "turn_on" + assert call.data == {"entity_id": "light.stove"} + + +async def test_light_area_same_name(hass, init_components): + """Test turning on a light with the same name as an area.""" + entities = entity_registry.async_get(hass) + devices = device_registry.async_get(hass) + areas = area_registry.async_get(hass) + entry = MockConfigEntry(domain="test") + + device = devices.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + kitchen_area = areas.async_create("kitchen") + devices.async_update_device(device.id, area_id=kitchen_area.id) + + kitchen_light = entities.async_get_or_create( + "light", "demo", "1234", original_name="kitchen light" + ) + entities.async_update_entity(kitchen_light.entity_id, area_id=kitchen_area.id) + hass.states.async_set( + kitchen_light.entity_id, "off", attributes={ATTR_FRIENDLY_NAME: "kitchen light"} + ) + + ceiling_light = entities.async_get_or_create( + "light", "demo", "5678", original_name="ceiling light" + ) + entities.async_update_entity(ceiling_light.entity_id, area_id=kitchen_area.id) + hass.states.async_set( + ceiling_light.entity_id, "off", attributes={ATTR_FRIENDLY_NAME: "ceiling light"} + ) + + calls = async_mock_service(hass, HASS_DOMAIN, "turn_on") + + await hass.services.async_call( + "conversation", + "process", + {conversation.ATTR_TEXT: "turn on kitchen light"}, + ) + await hass.async_block_till_done() + + # Should only turn on one light instead of all lights in the kitchen + assert len(calls) == 1 + call = calls[0] + assert call.domain == HASS_DOMAIN + assert call.service == "turn_on" + assert call.data == {"entity_id": kitchen_light.entity_id} diff --git a/tests/components/coolmaster/conftest.py b/tests/components/coolmaster/conftest.py new file mode 100644 index 00000000000..fadce747d6a --- /dev/null +++ b/tests/components/coolmaster/conftest.py @@ -0,0 +1,134 @@ +"""Fixtures for the Coolmaster integration.""" +from __future__ import annotations + +import copy +from typing import Any +from unittest.mock import patch + +import pytest + +from homeassistant.components.climate import HVACMode +from homeassistant.components.coolmaster.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +DEFAULT_INFO: dict[str, str] = { + "version": "1", +} + +TEST_UNITS: dict[dict[str, Any]] = { + "L1.100": { + "is_on": False, + "thermostat": 20, + "temperature": 25, + "temperature_unit": "celsius", + "fan_speed": "low", + "mode": "cool", + "error_code": None, + "clean_filter": False, + "swing": None, + }, + "L1.101": { + "is_on": True, + "thermostat": 68, + "temperature": 50, + "temperature_unit": "imperial", + "fan_speed": "high", + "mode": "heat", + "error_code": "Err1", + "clean_filter": True, + "swing": "horizontal", + }, +} + + +class CoolMasterNetUnitMock: + """Mock for CoolMasterNetUnit.""" + + def __init__(self, unit_id: str, attributes: dict[str, Any]) -> None: + """Initialize the CoolMasterNetUnitMock.""" + self.unit_id = unit_id + self._attributes = attributes + for key, value in attributes.items(): + setattr(self, key, value) + + async def set_fan_speed(self, value: str) -> CoolMasterNetUnitMock: + """Set the fan speed.""" + self._attributes["fan_speed"] = value + return CoolMasterNetUnitMock(self.unit_id, self._attributes) + + async def set_mode(self, value: str) -> CoolMasterNetUnitMock: + """Set the mode.""" + self._attributes["mode"] = value + return CoolMasterNetUnitMock(self.unit_id, self._attributes) + + async def set_thermostat(self, value: int | float) -> CoolMasterNetUnitMock: + """Set the target temperature.""" + self._attributes["thermostat"] = value + return CoolMasterNetUnitMock(self.unit_id, self._attributes) + + async def set_swing(self, value: str | None) -> CoolMasterNetUnitMock: + """Set the swing mode.""" + if value == "": + raise ValueError() + self._attributes["swing"] = value + return CoolMasterNetUnitMock(self.unit_id, self._attributes) + + async def turn_on(self) -> CoolMasterNetUnitMock: + """Turn a unit on.""" + self._attributes["is_on"] = True + return CoolMasterNetUnitMock(self.unit_id, self._attributes) + + async def turn_off(self) -> CoolMasterNetUnitMock: + """Turn a unit off.""" + self._attributes["is_on"] = False + return CoolMasterNetUnitMock(self.unit_id, self._attributes) + + async def reset_filter(self) -> CoolMasterNetUnitMock: + """Report that the air filter was cleaned and reset the timer.""" + self._attributes["clean_filter"] = False + return CoolMasterNetUnitMock(self.unit_id, self._attributes) + + +class CoolMasterNetMock: + """Mock for CoolMasterNet.""" + + def __init__(self, *_args: Any, **kwargs: Any) -> None: + """Initialize the CoolMasterNetMock.""" + self._units = copy.deepcopy(TEST_UNITS) + + async def info(self) -> dict[str, Any]: + """Return info about the bridge device.""" + return DEFAULT_INFO + + async def status(self) -> dict[str, CoolMasterNetUnitMock]: + """Return the units.""" + return { + unit_id: CoolMasterNetUnitMock(unit_id, attributes) + for unit_id, attributes in self._units.items() + } + + +@pytest.fixture +async def load_int(hass: HomeAssistant) -> MockConfigEntry: + """Set up the Coolmaster integration in Home Assistant.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": "1.2.3.4", + "port": 1234, + "supported_modes": [HVACMode.OFF, HVACMode.COOL, HVACMode.HEAT], + }, + ) + + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.coolmaster.CoolMasterNet", + new=CoolMasterNetMock, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry diff --git a/tests/components/coolmaster/test_binary_sensor.py b/tests/components/coolmaster/test_binary_sensor.py new file mode 100644 index 00000000000..2f5c8c5f1be --- /dev/null +++ b/tests/components/coolmaster/test_binary_sensor.py @@ -0,0 +1,14 @@ +"""The test for the Coolmaster binary sensor platform.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + + +async def test_binary_sensor( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster binary sensor.""" + assert hass.states.get("binary_sensor.l1_100_clean_filter").state == "off" + assert hass.states.get("binary_sensor.l1_101_clean_filter").state == "on" diff --git a/tests/components/coolmaster/test_button.py b/tests/components/coolmaster/test_button.py new file mode 100644 index 00000000000..67461f63087 --- /dev/null +++ b/tests/components/coolmaster/test_button.py @@ -0,0 +1,29 @@ +"""The test for the Coolmaster button platform.""" +from __future__ import annotations + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + + +async def test_button( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster button.""" + assert hass.states.get("binary_sensor.l1_101_clean_filter").state == "on" + + button = hass.states.get("button.l1_101_reset_filter") + assert button is not None + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + { + ATTR_ENTITY_ID: button.entity_id, + }, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.l1_101_clean_filter").state == "off" diff --git a/tests/components/coolmaster/test_climate.py b/tests/components/coolmaster/test_climate.py new file mode 100644 index 00000000000..5f98082e822 --- /dev/null +++ b/tests/components/coolmaster/test_climate.py @@ -0,0 +1,290 @@ +"""The test for the Coolmaster climate platform.""" +from __future__ import annotations + +from pycoolmasternet_async import SWING_MODES +import pytest + +from homeassistant.components.climate import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_FAN_MODE, + ATTR_FAN_MODES, + ATTR_HVAC_MODE, + ATTR_HVAC_MODES, + ATTR_SWING_MODE, + ATTR_SWING_MODES, + DOMAIN as CLIMATE_DOMAIN, + FAN_HIGH, + FAN_LOW, + SERVICE_SET_FAN_MODE, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_SWING_MODE, + SERVICE_SET_TEMPERATURE, + ClimateEntityFeature, + HVACMode, +) +from homeassistant.components.coolmaster.climate import FAN_MODES +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_SUPPORTED_FEATURES, + ATTR_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + + +async def test_climate_state( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate state.""" + assert hass.states.get("climate.l1_100").state == HVACMode.OFF + assert hass.states.get("climate.l1_101").state == HVACMode.HEAT + + +async def test_climate_friendly_name( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate friendly name.""" + assert hass.states.get("climate.l1_100").attributes[ATTR_FRIENDLY_NAME] == "L1.100" + assert hass.states.get("climate.l1_101").attributes[ATTR_FRIENDLY_NAME] == "L1.101" + + +async def test_climate_supported_features( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate supported features.""" + assert hass.states.get("climate.l1_100").attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ) + assert hass.states.get("climate.l1_101").attributes[ATTR_SUPPORTED_FEATURES] == ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.SWING_MODE + ) + + +async def test_climate_temperature( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate current temperature.""" + assert hass.states.get("climate.l1_100").attributes[ATTR_CURRENT_TEMPERATURE] == 25 + assert hass.states.get("climate.l1_101").attributes[ATTR_CURRENT_TEMPERATURE] == 10 + + +async def test_climate_thermostat( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate thermostat.""" + assert hass.states.get("climate.l1_100").attributes[ATTR_TEMPERATURE] == 20 + assert hass.states.get("climate.l1_101").attributes[ATTR_TEMPERATURE] == 20 + + +async def test_climate_hvac_modes( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate hvac modes.""" + assert hass.states.get("climate.l1_100").attributes[ATTR_HVAC_MODES] == [ + HVACMode.OFF, + HVACMode.COOL, + HVACMode.HEAT, + ] + assert ( + hass.states.get("climate.l1_101").attributes[ATTR_HVAC_MODES] + == hass.states.get("climate.l1_100").attributes[ATTR_HVAC_MODES] + ) + + +async def test_climate_fan_mode( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate fan mode.""" + assert hass.states.get("climate.l1_100").attributes[ATTR_FAN_MODE] == FAN_LOW + assert hass.states.get("climate.l1_101").attributes[ATTR_FAN_MODE] == FAN_HIGH + + +async def test_climate_fan_modes( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate fan modes.""" + assert hass.states.get("climate.l1_100").attributes[ATTR_FAN_MODES] == FAN_MODES + assert ( + hass.states.get("climate.l1_101").attributes[ATTR_FAN_MODES] + == hass.states.get("climate.l1_100").attributes[ATTR_FAN_MODES] + ) + + +async def test_climate_swing_mode( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate swing mode.""" + assert ATTR_SWING_MODE not in hass.states.get("climate.l1_100").attributes + assert hass.states.get("climate.l1_101").attributes[ATTR_SWING_MODE] == "horizontal" + + +async def test_climate_swing_modes( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate swing modes.""" + assert ATTR_SWING_MODES not in hass.states.get("climate.l1_100").attributes + assert hass.states.get("climate.l1_101").attributes[ATTR_SWING_MODES] == SWING_MODES + + +async def test_set_temperature( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate set temperature.""" + assert hass.states.get("climate.l1_100").attributes[ATTR_TEMPERATURE] == 20 + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: "climate.l1_100", + ATTR_TEMPERATURE: 30, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("climate.l1_100").attributes[ATTR_TEMPERATURE] == 30 + + +async def test_set_fan_mode( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate set fan mode.""" + assert hass.states.get("climate.l1_100").attributes[ATTR_FAN_MODE] == FAN_LOW + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_FAN_MODE, + { + ATTR_ENTITY_ID: "climate.l1_100", + ATTR_FAN_MODE: FAN_HIGH, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("climate.l1_100").attributes[ATTR_FAN_MODE] == FAN_HIGH + + +async def test_set_swing_mode( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate set swing mode.""" + assert hass.states.get("climate.l1_101").attributes[ATTR_SWING_MODE] == "horizontal" + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_MODE, + { + ATTR_ENTITY_ID: "climate.l1_101", + ATTR_SWING_MODE: "vertical", + }, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("climate.l1_101").attributes[ATTR_SWING_MODE] == "vertical" + + +async def test_set_swing_mode_error( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate set swing mode with error.""" + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_SWING_MODE, + { + ATTR_ENTITY_ID: "climate.l1_101", + ATTR_SWING_MODE: "", + }, + blocking=True, + ) + + +async def test_set_hvac_mode( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate set hvac mode.""" + assert hass.states.get("climate.l1_100").state == HVACMode.OFF + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: "climate.l1_100", + ATTR_HVAC_MODE: HVACMode.HEAT, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("climate.l1_100").state == HVACMode.HEAT + + +async def test_set_hvac_mode_off( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate set hvac mode to off.""" + assert hass.states.get("climate.l1_101").state == HVACMode.HEAT + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: "climate.l1_101", + ATTR_HVAC_MODE: HVACMode.OFF, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("climate.l1_101").state == HVACMode.OFF + + +async def test_turn_on( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate turn on.""" + assert hass.states.get("climate.l1_100").state == HVACMode.OFF + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "climate.l1_100", + }, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("climate.l1_100").state == HVACMode.COOL + + +async def test_turn_off( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster climate turn off.""" + assert hass.states.get("climate.l1_101").state == HVACMode.HEAT + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: "climate.l1_101", + }, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("climate.l1_101").state == HVACMode.OFF diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py index 1f69e2336bc..2927f438af8 100644 --- a/tests/components/coolmaster/test_config_flow.py +++ b/tests/components/coolmaster/test_config_flow.py @@ -10,6 +10,7 @@ def _flow_data(): options = {"host": "1.1.1.1"} for mode in AVAILABLE_MODES: options[mode] = True + options["swing_support"] = False return options @@ -39,6 +40,7 @@ async def test_form(hass): "host": "1.1.1.1", "port": 10102, "supported_modes": AVAILABLE_MODES, + "swing_support": False, } assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/coolmaster/test_init.py b/tests/components/coolmaster/test_init.py new file mode 100644 index 00000000000..ce6dd8f60a4 --- /dev/null +++ b/tests/components/coolmaster/test_init.py @@ -0,0 +1,26 @@ +"""The test for the Coolmaster integration.""" +from homeassistant.components.coolmaster.const import DOMAIN +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.core import HomeAssistant + + +async def test_load_entry( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test Coolmaster initial load.""" + # 2 units times 4 entities (climate, binary_sensor, sensor, button). + assert hass.states.async_entity_ids_count() == 8 + assert load_int.state is ConfigEntryState.LOADED + + +async def test_unload_entry( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test Coolmaster unloading an entry.""" + assert load_int.entry_id in hass.data.get(DOMAIN) + await hass.config_entries.async_unload(load_int.entry_id) + await hass.async_block_till_done() + assert load_int.state is ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) diff --git a/tests/components/coolmaster/test_sensor.py b/tests/components/coolmaster/test_sensor.py new file mode 100644 index 00000000000..3072106ec62 --- /dev/null +++ b/tests/components/coolmaster/test_sensor.py @@ -0,0 +1,14 @@ +"""The test for the Coolmaster sensor platform.""" +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + + +async def test_sensor( + hass: HomeAssistant, + load_int: ConfigEntry, +) -> None: + """Test the Coolmaster sensor.""" + assert hass.states.get("sensor.l1_100_error_code").state == "OK" + assert hass.states.get("sensor.l1_101_error_code").state == "Err1" diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py index eb0907d87d4..6d00cf24949 100644 --- a/tests/components/counter/test_init.py +++ b/tests/components/counter/test_init.py @@ -1,5 +1,5 @@ """The tests for the counter component.""" -# pylint: disable=protected-access + import logging import pytest diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index 53be60fccd4..5c289fc6321 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -340,7 +340,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_open - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_open " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -358,7 +362,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_closed - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_closed " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -376,7 +384,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_opening - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_opening " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -394,7 +406,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_closing - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_closing " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -457,14 +473,22 @@ async def test_if_position(hass, calls, caplog, enable_custom_integrations): "sequence": { "service": "test.automation", "data_template": { - "some": "is_pos_gt_45 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_pos_gt_45 " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, "default": { "service": "test.automation", "data_template": { - "some": "is_pos_not_gt_45 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_pos_not_gt_45 " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -484,7 +508,11 @@ async def test_if_position(hass, calls, caplog, enable_custom_integrations): "action": { "service": "test.automation", "data_template": { - "some": "is_pos_lt_90 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_pos_lt_90 " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -504,7 +532,11 @@ async def test_if_position(hass, calls, caplog, enable_custom_integrations): "action": { "service": "test.automation", "data_template": { - "some": "is_pos_gt_45_lt_90 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_pos_gt_45_lt_90 " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -586,14 +618,22 @@ async def test_if_tilt_position(hass, calls, caplog, enable_custom_integrations) "sequence": { "service": "test.automation", "data_template": { - "some": "is_pos_gt_45 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_pos_gt_45 " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, "default": { "service": "test.automation", "data_template": { - "some": "is_pos_not_gt_45 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_pos_not_gt_45 " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -613,7 +653,11 @@ async def test_if_tilt_position(hass, calls, caplog, enable_custom_integrations) "action": { "service": "test.automation", "data_template": { - "some": "is_pos_lt_90 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_pos_lt_90 " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -633,7 +677,11 @@ async def test_if_tilt_position(hass, calls, caplog, enable_custom_integrations) "action": { "service": "test.automation", "data_template": { - "some": "is_pos_gt_45_lt_90 - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_pos_gt_45_lt_90 " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index ebd48951853..a8be2f681a0 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -359,9 +359,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "opened - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "opened " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -378,9 +381,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "closed - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "closed " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -397,9 +403,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "opening - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "opening " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -416,9 +425,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "closing - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "closing " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -543,9 +555,12 @@ async def test_if_fires_on_position(hass, calls, enable_custom_integrations): "service": "test.automation", "data_template": { "some": ( - "is_pos_gt_45 - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "is_pos_gt_45 " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -565,9 +580,12 @@ async def test_if_fires_on_position(hass, calls, enable_custom_integrations): "service": "test.automation", "data_template": { "some": ( - "is_pos_lt_90 - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "is_pos_lt_90 " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -588,9 +606,12 @@ async def test_if_fires_on_position(hass, calls, enable_custom_integrations): "service": "test.automation", "data_template": { "some": ( - "is_pos_gt_45_lt_90 - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "is_pos_gt_45_lt_90 " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -611,7 +632,10 @@ async def test_if_fires_on_position(hass, calls, enable_custom_integrations): [calls[0].data["some"], calls[1].data["some"], calls[2].data["some"]] ) == sorted( [ - "is_pos_gt_45_lt_90 - device - cover.set_position_cover - closed - open - None", + ( + "is_pos_gt_45_lt_90 - device - cover.set_position_cover - closed - open" + " - None" + ), "is_pos_lt_90 - device - cover.set_position_cover - closed - open - None", "is_pos_gt_45 - device - cover.set_position_cover - open - closed - None", ] @@ -670,9 +694,12 @@ async def test_if_fires_on_tilt_position(hass, calls, enable_custom_integrations "service": "test.automation", "data_template": { "some": ( - "is_pos_gt_45 - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "is_pos_gt_45 " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -692,9 +719,12 @@ async def test_if_fires_on_tilt_position(hass, calls, enable_custom_integrations "service": "test.automation", "data_template": { "some": ( - "is_pos_lt_90 - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "is_pos_lt_90 " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -715,9 +745,12 @@ async def test_if_fires_on_tilt_position(hass, calls, enable_custom_integrations "service": "test.automation", "data_template": { "some": ( - "is_pos_gt_45_lt_90 - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "is_pos_gt_45_lt_90 " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -740,7 +773,10 @@ async def test_if_fires_on_tilt_position(hass, calls, enable_custom_integrations [calls[0].data["some"], calls[1].data["some"], calls[2].data["some"]] ) == sorted( [ - "is_pos_gt_45_lt_90 - device - cover.set_position_cover - closed - open - None", + ( + "is_pos_gt_45_lt_90 - device - cover.set_position_cover - closed - open" + " - None" + ), "is_pos_lt_90 - device - cover.set_position_cover - closed - open - None", "is_pos_gt_45 - device - cover.set_position_cover - open - closed - None", ] diff --git a/tests/components/cpuspeed/test_config_flow.py b/tests/components/cpuspeed/test_config_flow.py index c016cfdb1d9..2d0f4c6df22 100644 --- a/tests/components/cpuspeed/test_config_flow.py +++ b/tests/components/cpuspeed/test_config_flow.py @@ -22,7 +22,6 @@ async def test_full_user_flow( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -69,7 +68,6 @@ async def test_not_compatible( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_cpuinfo_config_flow.return_value = {} result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index bd118884edc..09b191ad831 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -1,4 +1,3 @@ -# pylint: disable=redefined-outer-name """Tests for the Daikin config flow.""" import asyncio from unittest.mock import PropertyMock, patch diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 73df9c3fd1a..0bae5f06085 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -62,9 +62,8 @@ TEST_DATA = [ "state": "poor", "entity_category": None, "device_class": None, - "state_class": SensorStateClass.MEASUREMENT, + "state_class": None, "attributes": { - "state_class": "measurement", "friendly_name": "BOSCH Air quality sensor", }, "websocket_event": {"state": {"airquality": "excellent"}}, diff --git a/tests/components/demo/test_geo_location.py b/tests/components/demo/test_geo_location.py index b75a896256c..3f12cfcffd1 100644 --- a/tests/components/demo/test_geo_location.py +++ b/tests/components/demo/test_geo_location.py @@ -11,7 +11,7 @@ from homeassistant.const import ( ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_UNIT_OF_MEASUREMENT, - LENGTH_KILOMETERS, + UnitOfLength, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -45,7 +45,7 @@ async def test_setup_platform(hass): continue assert abs(state.attributes[ATTR_LATITUDE] - hass.config.latitude) < 1.0 assert abs(state.attributes[ATTR_LONGITUDE] - hass.config.longitude) < 1.0 - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == LENGTH_KILOMETERS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfLength.KILOMETERS # Update (replaces 1 device). async_fire_time_changed(hass, utcnow + DEFAULT_UPDATE_INTERVAL) diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 45fa602b143..ecd89cadaf6 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -1,25 +1,12 @@ """The tests for the Demo component.""" -import datetime -from http import HTTPStatus import json -from unittest.mock import ANY, patch +from unittest.mock import patch import pytest from homeassistant.components.demo import DOMAIN -from homeassistant.components.recorder import get_instance -from homeassistant.components.recorder.statistics import ( - async_add_external_statistics, - get_last_statistics, - list_statistic_ids, -) -from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util -from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM - -from tests.components.recorder.common import async_wait_recording_done @pytest.fixture @@ -47,267 +34,5 @@ async def test_setting_up_demo(mock_history, hass): json.dumps(hass.states.async_all(), cls=JSONEncoder) except Exception: # pylint: disable=broad-except pytest.fail( - "Unable to convert all demo entities to JSON. " - "Wrong data in state machine!" + "Unable to convert all demo entities to JSON. Wrong data in state machine!" ) - - -async def test_demo_statistics(recorder_mock, mock_history, hass): - """Test that the demo components makes some statistics available.""" - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - await hass.async_block_till_done() - await hass.async_start() - await async_wait_recording_done(hass) - - statistic_ids = await get_instance(hass).async_add_executor_job( - list_statistic_ids, hass - ) - assert { - "display_unit_of_measurement": "°C", - "has_mean": True, - "has_sum": False, - "name": "Outdoor temperature", - "source": "demo", - "statistic_id": "demo:temperature_outdoor", - "statistics_unit_of_measurement": "°C", - "unit_class": "temperature", - } in statistic_ids - assert { - "display_unit_of_measurement": "kWh", - "has_mean": False, - "has_sum": True, - "name": "Energy consumption 1", - "source": "demo", - "statistic_id": "demo:energy_consumption_kwh", - "statistics_unit_of_measurement": "kWh", - "unit_class": "energy", - } in statistic_ids - - -async def test_demo_statistics_growth(recorder_mock, mock_history, hass): - """Test that the demo sum statistics adds to the previous state.""" - hass.config.units = US_CUSTOMARY_SYSTEM - - now = dt_util.now() - last_week = now - datetime.timedelta(days=7) - last_week_midnight = last_week.replace(hour=0, minute=0, second=0, microsecond=0) - - statistic_id = f"{DOMAIN}:energy_consumption_kwh" - metadata = { - "source": DOMAIN, - "name": "Energy consumption 1", - "statistic_id": statistic_id, - "unit_of_measurement": "m³", - "has_mean": False, - "has_sum": True, - } - statistics = [ - { - "start": last_week_midnight, - "sum": 2**20, - } - ] - async_add_external_statistics(hass, metadata, statistics) - await async_wait_recording_done(hass) - - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - await hass.async_block_till_done() - await hass.async_start() - await async_wait_recording_done(hass) - - statistics = await get_instance(hass).async_add_executor_job( - get_last_statistics, hass, 1, statistic_id, False, {"sum"} - ) - assert statistics[statistic_id][0]["sum"] > 2**20 - assert statistics[statistic_id][0]["sum"] <= (2**20 + 24) - - -async def test_issues_created(mock_history, hass, hass_client, hass_ws_client): - """Test issues are created and can be fixed.""" - assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) - assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) - await hass.async_block_till_done() - await hass.async_start() - - ws_client = await hass_ws_client(hass) - client = await hass_client() - - await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) - msg = await ws_client.receive_json() - - assert msg["success"] - assert msg["result"] == { - "issues": [ - { - "breaks_in_ha_version": "2023.1.1", - "created": ANY, - "dismissed_version": None, - "domain": "demo", - "ignored": False, - "is_fixable": False, - "issue_id": "transmogrifier_deprecated", - "issue_domain": None, - "learn_more_url": "https://en.wiktionary.org/wiki/transmogrifier", - "severity": "warning", - "translation_key": "transmogrifier_deprecated", - "translation_placeholders": None, - }, - { - "breaks_in_ha_version": "2023.1.1", - "created": ANY, - "dismissed_version": None, - "domain": "demo", - "ignored": False, - "is_fixable": True, - "issue_id": "out_of_blinker_fluid", - "issue_domain": None, - "learn_more_url": "https://www.youtube.com/watch?v=b9rntRxLlbU", - "severity": "critical", - "translation_key": "out_of_blinker_fluid", - "translation_placeholders": None, - }, - { - "breaks_in_ha_version": None, - "created": ANY, - "dismissed_version": None, - "domain": "demo", - "ignored": False, - "is_fixable": False, - "issue_id": "unfixable_problem", - "issue_domain": None, - "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - "severity": "warning", - "translation_key": "unfixable_problem", - "translation_placeholders": None, - }, - { - "breaks_in_ha_version": None, - "created": ANY, - "dismissed_version": None, - "domain": "demo", - "ignored": False, - "is_fixable": True, - "issue_domain": None, - "issue_id": "bad_psu", - "learn_more_url": "https://www.youtube.com/watch?v=b9rntRxLlbU", - "severity": "critical", - "translation_key": "bad_psu", - "translation_placeholders": None, - }, - { - "breaks_in_ha_version": None, - "created": ANY, - "dismissed_version": None, - "domain": "demo", - "is_fixable": True, - "issue_domain": None, - "issue_id": "cold_tea", - "learn_more_url": None, - "severity": "warning", - "translation_key": "cold_tea", - "translation_placeholders": None, - "ignored": False, - }, - ] - } - - url = "/api/repairs/issues/fix" - resp = await client.post( - url, json={"handler": "demo", "issue_id": "out_of_blinker_fluid"} - ) - - assert resp.status == HTTPStatus.OK - data = await resp.json() - - flow_id = data["flow_id"] - assert data == { - "data_schema": [], - "description_placeholders": None, - "errors": None, - "flow_id": ANY, - "handler": "demo", - "last_step": None, - "step_id": "confirm", - "type": "form", - } - - url = f"/api/repairs/issues/fix/{flow_id}" - resp = await client.post(url) - - assert resp.status == HTTPStatus.OK - data = await resp.json() - - flow_id = data["flow_id"] - assert data == { - "description": None, - "description_placeholders": None, - "flow_id": flow_id, - "handler": "demo", - "type": "create_entry", - "version": 1, - } - - await ws_client.send_json({"id": 4, "type": "repairs/list_issues"}) - msg = await ws_client.receive_json() - - assert msg["success"] - assert msg["result"] == { - "issues": [ - { - "breaks_in_ha_version": "2023.1.1", - "created": ANY, - "dismissed_version": None, - "domain": "demo", - "ignored": False, - "is_fixable": False, - "issue_id": "transmogrifier_deprecated", - "issue_domain": None, - "learn_more_url": "https://en.wiktionary.org/wiki/transmogrifier", - "severity": "warning", - "translation_key": "transmogrifier_deprecated", - "translation_placeholders": None, - }, - { - "breaks_in_ha_version": None, - "created": ANY, - "dismissed_version": None, - "domain": "demo", - "ignored": False, - "is_fixable": False, - "issue_id": "unfixable_problem", - "issue_domain": None, - "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", - "severity": "warning", - "translation_key": "unfixable_problem", - "translation_placeholders": None, - }, - { - "breaks_in_ha_version": None, - "created": ANY, - "dismissed_version": None, - "domain": "demo", - "ignored": False, - "is_fixable": True, - "issue_domain": None, - "issue_id": "bad_psu", - "learn_more_url": "https://www.youtube.com/watch?v=b9rntRxLlbU", - "severity": "critical", - "translation_key": "bad_psu", - "translation_placeholders": None, - }, - { - "breaks_in_ha_version": None, - "created": ANY, - "dismissed_version": None, - "domain": "demo", - "is_fixable": True, - "issue_domain": None, - "issue_id": "cold_tea", - "learn_more_url": None, - "severity": "warning", - "translation_key": "cold_tea", - "translation_placeholders": None, - "ignored": False, - }, - ] - } diff --git a/tests/components/demo/test_stt.py b/tests/components/demo/test_stt.py index eced954a837..c50f2fa2b68 100644 --- a/tests/components/demo/test_stt.py +++ b/tests/components/demo/test_stt.py @@ -47,7 +47,10 @@ async def test_demo_speech_wrong_metadata(hass_client): response = await client.post( "/api/stt/demo", headers={ - "X-Speech-Content": "format=wav; codec=pcm; sample_rate=8000; bit_rate=16; channel=1; language=de" + "X-Speech-Content": ( + "format=wav; codec=pcm; sample_rate=8000; bit_rate=16; channel=1;" + " language=de" + ) }, data=b"Test", ) @@ -61,7 +64,10 @@ async def test_demo_speech(hass_client): response = await client.post( "/api/stt/demo", headers={ - "X-Speech-Content": "format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=2; language=de" + "X-Speech-Content": ( + "format=wav; codec=pcm; sample_rate=16000; bit_rate=16; channel=2;" + " language=de" + ) }, data=b"Test", ) diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index b93aef50774..eef452d71cb 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -4,7 +4,7 @@ from math import sin import random from unittest.mock import patch -from homeassistant.const import POWER_WATT, TIME_HOURS, TIME_MINUTES, TIME_SECONDS +from homeassistant.const import UnitOfPower, UnitOfTime from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -85,7 +85,7 @@ async def test_dataSet1(hass): """Test derivative sensor state.""" await setup_tests( hass, - {"unit_time": TIME_SECONDS}, + {"unit_time": UnitOfTime.SECONDS}, times=[20, 30, 40, 50], values=[10, 30, 5, 0], expected_state=-0.5, @@ -96,7 +96,7 @@ async def test_dataSet2(hass): """Test derivative sensor state.""" await setup_tests( hass, - {"unit_time": TIME_SECONDS}, + {"unit_time": UnitOfTime.SECONDS}, times=[20, 30], values=[5, 0], expected_state=-0.5, @@ -107,20 +107,20 @@ async def test_dataSet3(hass): """Test derivative sensor state.""" state = await setup_tests( hass, - {"unit_time": TIME_SECONDS}, + {"unit_time": UnitOfTime.SECONDS}, times=[20, 30], values=[5, 10], expected_state=0.5, ) - assert state.attributes.get("unit_of_measurement") == f"/{TIME_SECONDS}" + assert state.attributes.get("unit_of_measurement") == f"/{UnitOfTime.SECONDS}" async def test_dataSet4(hass): """Test derivative sensor state.""" await setup_tests( hass, - {"unit_time": TIME_SECONDS}, + {"unit_time": UnitOfTime.SECONDS}, times=[20, 30], values=[5, 5], expected_state=0, @@ -131,7 +131,7 @@ async def test_dataSet5(hass): """Test derivative sensor state.""" await setup_tests( hass, - {"unit_time": TIME_SECONDS}, + {"unit_time": UnitOfTime.SECONDS}, times=[20, 30], values=[10, -10], expected_state=-2, @@ -162,7 +162,7 @@ async def test_data_moving_average_for_discrete_sensor(hass): hass, { "time_window": {"seconds": time_window}, - "unit_time": TIME_MINUTES, + "unit_time": UnitOfTime.MINUTES, "round": 1, }, ) # two minute window @@ -205,7 +205,7 @@ async def test_data_moving_average_for_irregular_times(hass): hass, { "time_window": {"seconds": time_window}, - "unit_time": TIME_MINUTES, + "unit_time": UnitOfTime.MINUTES, "round": 3, }, ) @@ -245,7 +245,7 @@ async def test_double_signal_after_delay(hass): hass, { "time_window": {"seconds": time_window}, - "unit_time": TIME_MINUTES, + "unit_time": UnitOfTime.MINUTES, "round": 3, }, ) @@ -285,13 +285,19 @@ async def test_prefix(hass): with patch("homeassistant.util.dt.utcnow") as now: now.return_value = base hass.states.async_set( - entity_id, 1000, {"unit_of_measurement": POWER_WATT}, force_update=True + entity_id, + 1000, + {"unit_of_measurement": UnitOfPower.WATT}, + force_update=True, ) await hass.async_block_till_done() now.return_value += timedelta(seconds=3600) hass.states.async_set( - entity_id, 1000, {"unit_of_measurement": POWER_WATT}, force_update=True + entity_id, + 1000, + {"unit_of_measurement": UnitOfPower.WATT}, + force_update=True, ) await hass.async_block_till_done() @@ -300,7 +306,7 @@ async def test_prefix(hass): # Testing a power sensor at 1000 Watts for 1hour = 0kW/h assert round(float(state.state), config["sensor"]["round"]) == 0.0 - assert state.attributes.get("unit_of_measurement") == f"kW/{TIME_HOURS}" + assert state.attributes.get("unit_of_measurement") == f"kW/{UnitOfTime.HOURS}" async def test_suffix(hass): @@ -312,7 +318,7 @@ async def test_suffix(hass): "source": "sensor.bytes_per_second", "round": 2, "unit_prefix": "k", - "unit_time": TIME_SECONDS, + "unit_time": UnitOfTime.SECONDS, } } diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 3ead6fcb35d..a2589693238 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -1,19 +1,31 @@ """The test for light device automation.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch import pytest +from pytest_unordered import unordered +import voluptuous as vol +from homeassistant import config_entries, loader from homeassistant.components import device_automation import homeassistant.components.automation as automation +from homeassistant.components.device_automation import ( + InvalidDeviceAutomationConfig, + toggle_entity, +) from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import CONF_PLATFORM, STATE_OFF, STATE_ON +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry +from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, + MockModule, async_mock_service, mock_device_registry, + mock_integration, + mock_platform, mock_registry, ) from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 @@ -31,18 +43,73 @@ def entity_reg(hass): return mock_registry(hass) -def _same_lists(a, b): - if len(a) != len(b): - return False +@pytest.fixture +def fake_integration(hass): + """Set up a mock integration with device automation support.""" + DOMAIN = "fake_integration" - for d in a: - if d not in b: - return False - return True + hass.config.components.add(DOMAIN) + + async def _async_get_actions( + hass: HomeAssistant, device_id: str + ) -> list[dict[str, str]]: + """List device actions.""" + return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) + + async def _async_get_conditions( + hass: HomeAssistant, device_id: str + ) -> list[dict[str, str]]: + """List device conditions.""" + return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) + + async def _async_get_triggers( + hass: HomeAssistant, device_id: str + ) -> list[dict[str, str]]: + """List device triggers.""" + return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) + + mock_platform( + hass, + f"{DOMAIN}.device_action", + Mock( + ACTION_SCHEMA=toggle_entity.ACTION_SCHEMA.extend( + {vol.Required("domain"): DOMAIN} + ), + async_get_actions=_async_get_actions, + spec=["ACTION_SCHEMA", "async_get_actions"], + ), + ) + + mock_platform( + hass, + f"{DOMAIN}.device_condition", + Mock( + CONDITION_SCHEMA=toggle_entity.CONDITION_SCHEMA.extend( + {vol.Required("domain"): DOMAIN} + ), + async_get_conditions=_async_get_conditions, + spec=["CONDITION_SCHEMA", "async_get_conditions"], + ), + ) + + mock_platform( + hass, + f"{DOMAIN}.device_trigger", + Mock( + TRIGGER_SCHEMA=vol.All( + toggle_entity.TRIGGER_SCHEMA, + vol.Schema({vol.Required("domain"): DOMAIN}, extra=vol.ALLOW_EXTRA), + ), + async_get_triggers=_async_get_triggers, + spec=["TRIGGER_SCHEMA", "async_get_triggers"], + ), + ) -async def test_websocket_get_actions(hass, hass_ws_client, device_reg, entity_reg): - """Test we get the expected conditions from a light through websocket.""" +async def test_websocket_get_actions( + hass, hass_ws_client, device_reg, entity_reg, fake_integration +): + """Test we get the expected actions through websocket.""" await async_setup_component(hass, "device_automation", {}) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -50,27 +117,29 @@ async def test_websocket_get_actions(hass, hass_ws_client, device_reg, entity_re config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + entity_reg.async_get_or_create( + "fake_integration", "test", "5678", device_id=device_entry.id + ) expected_actions = [ { - "domain": "light", + "domain": "fake_integration", "type": "turn_off", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": "fake_integration.test_5678", "metadata": {"secondary": False}, }, { - "domain": "light", + "domain": "fake_integration", "type": "turn_on", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": "fake_integration.test_5678", "metadata": {"secondary": False}, }, { - "domain": "light", + "domain": "fake_integration", "type": "toggle", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": "fake_integration.test_5678", "metadata": {"secondary": False}, }, ] @@ -85,11 +154,13 @@ async def test_websocket_get_actions(hass, hass_ws_client, device_reg, entity_re assert msg["type"] == TYPE_RESULT assert msg["success"] actions = msg["result"] - assert _same_lists(actions, expected_actions) + assert actions == unordered(expected_actions) -async def test_websocket_get_conditions(hass, hass_ws_client, device_reg, entity_reg): - """Test we get the expected conditions from a light through websocket.""" +async def test_websocket_get_conditions( + hass, hass_ws_client, device_reg, entity_reg, fake_integration +): + """Test we get the expected conditions through websocket.""" await async_setup_component(hass, "device_automation", {}) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -97,22 +168,24 @@ async def test_websocket_get_conditions(hass, hass_ws_client, device_reg, entity config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + entity_reg.async_get_or_create( + "fake_integration", "test", "5678", device_id=device_entry.id + ) expected_conditions = [ { "condition": "device", - "domain": "light", + "domain": "fake_integration", "type": "is_off", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": "fake_integration.test_5678", "metadata": {"secondary": False}, }, { "condition": "device", - "domain": "light", + "domain": "fake_integration", "type": "is_on", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": "fake_integration.test_5678", "metadata": {"secondary": False}, }, ] @@ -131,11 +204,13 @@ async def test_websocket_get_conditions(hass, hass_ws_client, device_reg, entity assert msg["type"] == TYPE_RESULT assert msg["success"] conditions = msg["result"] - assert _same_lists(conditions, expected_conditions) + assert conditions == unordered(expected_conditions) -async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_reg): - """Test we get the expected triggers from a light through websocket.""" +async def test_websocket_get_triggers( + hass, hass_ws_client, device_reg, entity_reg, fake_integration +): + """Test we get the expected triggers through websocket.""" await async_setup_component(hass, "device_automation", {}) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -143,30 +218,32 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + entity_reg.async_get_or_create( + "fake_integration", "test", "5678", device_id=device_entry.id + ) expected_triggers = [ { "platform": "device", - "domain": "light", + "domain": "fake_integration", "type": "changed_states", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": "fake_integration.test_5678", "metadata": {"secondary": False}, }, { "platform": "device", - "domain": "light", + "domain": "fake_integration", "type": "turned_off", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": "fake_integration.test_5678", "metadata": {"secondary": False}, }, { "platform": "device", - "domain": "light", + "domain": "fake_integration", "type": "turned_on", "device_id": device_entry.id, - "entity_id": "light.test_5678", + "entity_id": "fake_integration.test_5678", "metadata": {"secondary": False}, }, ] @@ -185,13 +262,13 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r assert msg["type"] == TYPE_RESULT assert msg["success"] triggers = msg["result"] - assert _same_lists(triggers, expected_triggers) + assert triggers == unordered(expected_triggers) async def test_websocket_get_action_capabilities( - hass, hass_ws_client, device_reg, entity_reg + hass, hass_ws_client, device_reg, entity_reg, fake_integration ): - """Test we get the expected action capabilities for an alarm through websocket.""" + """Test we get the expected action capabilities through websocket.""" await async_setup_component(hass, "device_automation", {}) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -200,22 +277,28 @@ async def test_websocket_get_action_capabilities( connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entity_reg.async_get_or_create( - "alarm_control_panel", "test", "5678", device_id=device_entry.id - ) - hass.states.async_set( - "alarm_control_panel.test_5678", "attributes", {"supported_features": 47} + "fake_integration", "test", "5678", device_id=device_entry.id ) expected_capabilities = { - "arm_away": {"extra_fields": []}, - "arm_home": {"extra_fields": []}, - "arm_night": {"extra_fields": []}, - "arm_vacation": {"extra_fields": []}, - "disarm": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "turn_on": { + "extra_fields": [{"type": "string", "name": "code", "optional": True}] }, - "trigger": {"extra_fields": []}, + "turn_off": {"extra_fields": []}, + "toggle": {"extra_fields": []}, } + async def _async_get_action_capabilities( + hass: HomeAssistant, config: ConfigType + ) -> dict[str, vol.Schema]: + """List action capabilities.""" + if config["type"] == "turn_on": + return {"extra_fields": vol.Schema({vol.Optional("code"): str})} + return {} + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_action"] + module.async_get_action_capabilities = _async_get_action_capabilities + client = await hass_ws_client(hass) await client.send_json( {"id": 1, "type": "device_automation/action/list", "device_id": device_entry.id} @@ -228,7 +311,7 @@ async def test_websocket_get_action_capabilities( actions = msg["result"] id = 2 - assert len(actions) == 6 + assert len(actions) == 3 for action in actions: await client.send_json( { @@ -246,7 +329,7 @@ async def test_websocket_get_action_capabilities( id = id + 1 -async def test_websocket_get_bad_action_capabilities( +async def test_websocket_get_action_capabilities_unknown_domain( hass, hass_ws_client, device_reg, entity_reg ): """Test we get no action capabilities for a non existing domain.""" @@ -269,10 +352,14 @@ async def test_websocket_get_bad_action_capabilities( assert capabilities == expected_capabilities -async def test_websocket_get_no_action_capabilities( - hass, hass_ws_client, device_reg, entity_reg +async def test_websocket_get_action_capabilities_no_capabilities( + hass, hass_ws_client, device_reg, entity_reg, fake_integration ): - """Test we get no action capabilities for a domain with no device action capabilities.""" + """Test we get no action capabilities for a domain which has none. + + The tests tests a domain which has a device action platform, but no + async_get_action_capabilities. + """ await async_setup_component(hass, "device_automation", {}) expected_capabilities = {} @@ -281,7 +368,7 @@ async def test_websocket_get_no_action_capabilities( { "id": 1, "type": "device_automation/action/capabilities", - "action": {"domain": "deconz"}, + "action": {"domain": "fake_integration"}, } ) msg = await client.receive_json() @@ -292,10 +379,40 @@ async def test_websocket_get_no_action_capabilities( assert capabilities == expected_capabilities -async def test_websocket_get_condition_capabilities( - hass, hass_ws_client, device_reg, entity_reg +async def test_websocket_get_action_capabilities_bad_action( + hass, hass_ws_client, device_reg, entity_reg, fake_integration ): - """Test we get the expected condition capabilities for a light through websocket.""" + """Test we get no action capabilities when there is an error.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_action"] + module.async_get_action_capabilities = Mock( + side_effect=InvalidDeviceAutomationConfig + ) + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/action/capabilities", + "action": {"domain": "fake_integration"}, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + module.async_get_action_capabilities.assert_called_once() + + +async def test_websocket_get_condition_capabilities( + hass, hass_ws_client, device_reg, entity_reg, fake_integration +): + """Test we get the expected condition capabilities through websocket.""" await async_setup_component(hass, "device_automation", {}) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -303,13 +420,25 @@ async def test_websocket_get_condition_capabilities( config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + entity_reg.async_get_or_create( + "fake_integration", "test", "5678", device_id=device_entry.id + ) expected_capabilities = { "extra_fields": [ {"name": "for", "optional": True, "type": "positive_time_period_dict"} ] } + async def _async_get_condition_capabilities( + hass: HomeAssistant, config: ConfigType + ) -> dict[str, vol.Schema]: + """List condition capabilities.""" + return await toggle_entity.async_get_condition_capabilities(hass, config) + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_condition"] + module.async_get_condition_capabilities = _async_get_condition_capabilities + client = await hass_ws_client(hass) await client.send_json( { @@ -344,7 +473,7 @@ async def test_websocket_get_condition_capabilities( id = id + 1 -async def test_websocket_get_bad_condition_capabilities( +async def test_websocket_get_condition_capabilities_unknown_domain( hass, hass_ws_client, device_reg, entity_reg ): """Test we get no condition capabilities for a non existing domain.""" @@ -367,10 +496,14 @@ async def test_websocket_get_bad_condition_capabilities( assert capabilities == expected_capabilities -async def test_websocket_get_no_condition_capabilities( - hass, hass_ws_client, device_reg, entity_reg +async def test_websocket_get_condition_capabilities_no_capabilities( + hass, hass_ws_client, device_reg, entity_reg, fake_integration ): - """Test we get no condition capabilities for a domain with no device condition capabilities.""" + """Test we get no condition capabilities for a domain which has none. + + The tests tests a domain which has a device condition platform, but no + async_get_condition_capabilities. + """ await async_setup_component(hass, "device_automation", {}) expected_capabilities = {} @@ -381,8 +514,8 @@ async def test_websocket_get_no_condition_capabilities( "type": "device_automation/condition/capabilities", "condition": { "condition": "device", - "domain": "deconz", "device_id": "abcd", + "domain": "fake_integration", }, } ) @@ -394,6 +527,40 @@ async def test_websocket_get_no_condition_capabilities( assert capabilities == expected_capabilities +async def test_websocket_get_condition_capabilities_bad_condition( + hass, hass_ws_client, device_reg, entity_reg, fake_integration +): + """Test we get no condition capabilities when there is an error.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_condition"] + module.async_get_condition_capabilities = Mock( + side_effect=InvalidDeviceAutomationConfig + ) + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/condition/capabilities", + "condition": { + "condition": "device", + "device_id": "abcd", + "domain": "fake_integration", + }, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + module.async_get_condition_capabilities.assert_called_once() + + async def test_async_get_device_automations_single_device_trigger( hass, device_reg, entity_reg ): @@ -495,9 +662,9 @@ async def test_async_get_device_automations_all_devices_action_exception_throw( async def test_websocket_get_trigger_capabilities( - hass, hass_ws_client, device_reg, entity_reg + hass, hass_ws_client, device_reg, entity_reg, fake_integration ): - """Test we get the expected trigger capabilities for a light through websocket.""" + """Test we get the expected trigger capabilities through websocket.""" await async_setup_component(hass, "device_automation", {}) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -505,13 +672,25 @@ async def test_websocket_get_trigger_capabilities( config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + entity_reg.async_get_or_create( + "fake_integration", "test", "5678", device_id=device_entry.id + ) expected_capabilities = { "extra_fields": [ {"name": "for", "optional": True, "type": "positive_time_period_dict"} ] } + async def _async_get_trigger_capabilities( + hass: HomeAssistant, config: ConfigType + ) -> dict[str, vol.Schema]: + """List trigger capabilities.""" + return await toggle_entity.async_get_trigger_capabilities(hass, config) + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_trigger"] + module.async_get_trigger_capabilities = _async_get_trigger_capabilities + client = await hass_ws_client(hass) await client.send_json( { @@ -546,7 +725,7 @@ async def test_websocket_get_trigger_capabilities( id = id + 1 -async def test_websocket_get_bad_trigger_capabilities( +async def test_websocket_get_trigger_capabilities_unknown_domain( hass, hass_ws_client, device_reg, entity_reg ): """Test we get no trigger capabilities for a non existing domain.""" @@ -569,10 +748,14 @@ async def test_websocket_get_bad_trigger_capabilities( assert capabilities == expected_capabilities -async def test_websocket_get_no_trigger_capabilities( - hass, hass_ws_client, device_reg, entity_reg +async def test_websocket_get_trigger_capabilities_no_capabilities( + hass, hass_ws_client, device_reg, entity_reg, fake_integration ): - """Test we get no trigger capabilities for a domain with no device trigger capabilities.""" + """Test we get no trigger capabilities for a domain which has none. + + The tests tests a domain which has a device trigger platform, but no + async_get_trigger_capabilities. + """ await async_setup_component(hass, "device_automation", {}) expected_capabilities = {} @@ -581,7 +764,11 @@ async def test_websocket_get_no_trigger_capabilities( { "id": 1, "type": "device_automation/trigger/capabilities", - "trigger": {"platform": "device", "domain": "deconz", "device_id": "abcd"}, + "trigger": { + "platform": "device", + "device_id": "abcd", + "domain": "fake_integration", + }, } ) msg = await client.receive_json() @@ -592,8 +779,42 @@ async def test_websocket_get_no_trigger_capabilities( assert capabilities == expected_capabilities +async def test_websocket_get_trigger_capabilities_bad_trigger( + hass, hass_ws_client, device_reg, entity_reg, fake_integration +): + """Test we get no trigger capabilities when there is an error.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_trigger"] + module.async_get_trigger_capabilities = Mock( + side_effect=InvalidDeviceAutomationConfig + ) + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/trigger/capabilities", + "trigger": { + "platform": "device", + "device_id": "abcd", + "domain": "fake_integration", + }, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + module.async_get_trigger_capabilities.assert_called_once() + + async def test_automation_with_non_existing_integration(hass, caplog): - """Test device automation with non existing integration.""" + """Test device automation trigger with non existing integration.""" assert await async_setup_component( hass, automation.DOMAIN, @@ -613,10 +834,73 @@ async def test_automation_with_non_existing_integration(hass, caplog): assert "Integration 'beer' not found" in caplog.text -async def test_automation_with_integration_without_device_action( - hass, caplog, enable_custom_integrations +async def test_automation_with_device_action(hass, caplog, fake_integration): + """Test automation with a device action.""" + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_action"] + module.async_call_action_from_config = AsyncMock() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "action": { + "device_id": "", + "domain": "fake_integration", + "entity_id": "blah.blah", + "type": "turn_on", + }, + } + }, + ) + + module.async_call_action_from_config.assert_not_called() + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + + module.async_call_action_from_config.assert_awaited_once() + + +async def test_automation_with_dynamically_validated_action( + hass, caplog, device_reg, fake_integration ): - """Test automation with integration without device action support.""" + """Test device automation with an action which is dynamically validated.""" + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_action"] + module.async_validate_action_config = AsyncMock() + + config_entry = MockConfigEntry(domain="fake_integration", data={}) + config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "action": {"device_id": device_entry.id, "domain": "fake_integration"}, + } + }, + ) + + module.async_validate_action_config.assert_awaited_once() + + +async def test_automation_with_integration_without_device_action(hass, caplog): + """Test device automation action with integration without device action support.""" + mock_integration(hass, MockModule(domain="test")) assert await async_setup_component( hass, automation.DOMAIN, @@ -634,10 +918,75 @@ async def test_automation_with_integration_without_device_action( ) -async def test_automation_with_integration_without_device_condition( - hass, caplog, enable_custom_integrations +async def test_automation_with_device_condition(hass, caplog, fake_integration): + """Test automation with a device condition.""" + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_condition"] + module.async_condition_from_config = Mock() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": { + "condition": "device", + "device_id": "none", + "domain": "fake_integration", + "entity_id": "blah.blah", + "type": "is_on", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + module.async_condition_from_config.assert_called_once() + + +async def test_automation_with_dynamically_validated_condition( + hass, caplog, device_reg, fake_integration ): - """Test automation with integration without device condition support.""" + """Test device automation with a condition which is dynamically validated.""" + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_condition"] + module.async_validate_condition_config = AsyncMock() + + config_entry = MockConfigEntry(domain="fake_integration", data={}) + config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": { + "condition": "device", + "device_id": device_entry.id, + "domain": "fake_integration", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + module.async_validate_condition_config.assert_awaited_once() + + +async def test_automation_with_integration_without_device_condition(hass, caplog): + """Test device automation condition with integration without device condition support.""" + mock_integration(hass, MockModule(domain="test")) assert await async_setup_component( hass, automation.DOMAIN, @@ -661,10 +1010,78 @@ async def test_automation_with_integration_without_device_condition( ) -async def test_automation_with_integration_without_device_trigger( - hass, caplog, enable_custom_integrations +async def test_automation_with_device_trigger(hass, caplog, fake_integration): + """Test automation with a device trigger.""" + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_trigger"] + module.async_attach_trigger = AsyncMock() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": { + "platform": "device", + "device_id": "none", + "domain": "fake_integration", + "entity_id": "blah.blah", + "type": "turned_off", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + module.async_attach_trigger.assert_awaited_once() + + +async def test_automation_with_dynamically_validated_trigger( + hass, caplog, device_reg, entity_reg, fake_integration ): - """Test automation with integration without device trigger support.""" + """Test device automation with a trigger which is dynamically validated.""" + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_trigger"] + module.async_attach_trigger = AsyncMock() + module.async_validate_trigger_config = AsyncMock(wraps=lambda hass, config: config) + + config_entry = MockConfigEntry(domain="fake_integration", data={}) + config_entry.state = config_entries.ConfigEntryState.LOADED + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + "fake_integration", "test", "5678", device_id=device_entry.id + ) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": { + "platform": "device", + "device_id": device_entry.id, + "domain": "fake_integration", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + module.async_validate_trigger_config.assert_awaited_once() + module.async_attach_trigger.assert_awaited_once() + + +async def test_automation_with_integration_without_device_trigger(hass, caplog): + """Test device automation trigger with integration without device trigger support.""" + mock_integration(hass, MockModule(domain="test")) assert await async_setup_component( hass, automation.DOMAIN, @@ -720,6 +1137,24 @@ async def test_automation_with_bad_condition_action(hass, caplog): assert "required key not provided" in caplog.text +async def test_automation_with_bad_condition_missing_domain(hass, caplog): + """Test automation with bad device condition.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": {"condition": "device", "device_id": "hello.device"}, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + assert "required key not provided @ data['condition'][0]['domain']" in caplog.text + + async def test_automation_with_bad_condition(hass, caplog): """Test automation with bad device condition.""" assert await async_setup_component( @@ -849,9 +1284,8 @@ async def test_automation_with_sub_condition(hass, calls, enable_custom_integrat hass.bus.async_fire("test_event1") await hass.async_block_till_done() assert len(calls) == 4 - assert _same_lists( - [calls[2].data["some"], calls[3].data["some"]], - ["or event - test_event1", "and event - test_event1"], + assert [calls[2].data["some"], calls[3].data["some"]] == unordered( + ["or event - test_event1", "and event - test_event1"] ) @@ -905,3 +1339,105 @@ async def test_websocket_device_not_found(hass, hass_ws_client): assert msg["id"] == 1 assert not msg["success"] assert msg["error"] == {"code": "not_found", "message": "Device not found"} + + +async def test_automation_with_unknown_device(hass, caplog, fake_integration): + """Test device automation with a trigger with an unknown device.""" + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_trigger"] + module.async_validate_trigger_config = AsyncMock() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": { + "platform": "device", + "device_id": "no_such_device", + "domain": "fake_integration", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + module.async_validate_trigger_config.assert_not_awaited() + assert ( + "Automation with alias 'hello' failed to setup triggers and has been disabled: " + "Unknown device 'no_such_device'" in caplog.text + ) + + +async def test_automation_with_device_wrong_domain( + hass, caplog, device_reg, fake_integration +): + """Test device automation where the device doesn't have the right config entry.""" + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_trigger"] + module.async_validate_trigger_config = AsyncMock() + + device_entry = device_reg.async_get_or_create( + config_entry_id="not_fake_integration_config_entry", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": { + "platform": "device", + "device_id": device_entry.id, + "domain": "fake_integration", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + module.async_validate_trigger_config.assert_not_awaited() + assert ( + "Automation with alias 'hello' failed to setup triggers and has been disabled: " + f"Device '{device_entry.id}' has no config entry from domain 'fake_integration'" + in caplog.text + ) + + +async def test_automation_with_device_component_not_loaded( + hass, caplog, device_reg, fake_integration +): + """Test device automation where the device's config entry is not loaded.""" + + module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + module = module_cache["fake_integration.device_trigger"] + module.async_validate_trigger_config = AsyncMock() + module.async_attach_trigger = AsyncMock() + + config_entry = MockConfigEntry(domain="fake_integration", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": { + "platform": "device", + "device_id": device_entry.id, + "domain": "fake_integration", + }, + "action": {"service": "test.automation", "entity_id": "hello.world"}, + } + }, + ) + + module.async_validate_trigger_config.assert_not_awaited() diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index 976a07cbfb3..e18276378c2 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -1,5 +1,5 @@ """The tests device sun light trigger component.""" -# pylint: disable=protected-access + from datetime import datetime from unittest.mock import patch diff --git a/tests/components/device_tracker/test_device_condition.py b/tests/components/device_tracker/test_device_condition.py index 9c97304b701..726c7479b7d 100644 --- a/tests/components/device_tracker/test_device_condition.py +++ b/tests/components/device_tracker/test_device_condition.py @@ -136,7 +136,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_home - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_home " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -154,7 +158,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_not_home - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_not_home " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, diff --git a/tests/components/device_tracker/test_device_trigger.py b/tests/components/device_tracker/test_device_trigger.py index d28a2b62519..00e39fdc3fe 100644 --- a/tests/components/device_tracker/test_device_trigger.py +++ b/tests/components/device_tracker/test_device_trigger.py @@ -165,9 +165,13 @@ async def test_if_fires_on_zone_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "enter - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.attributes.longitude|round(3)}} - " - "{{ trigger.to_state.attributes.longitude|round(3)}}" + "enter " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ " + " trigger.from_state.attributes.longitude|round(3) " + " }} " + "- {{ trigger.to_state.attributes.longitude|round(3) }}" ) }, }, @@ -185,9 +189,13 @@ async def test_if_fires_on_zone_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "leave - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.attributes.longitude|round(3)}} - " - "{{ trigger.to_state.attributes.longitude|round(3)}}" + "leave " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ " + " trigger.from_state.attributes.longitude|round(3) " + " }} " + "- {{ trigger.to_state.attributes.longitude|round(3)}}" ) }, }, diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 9bb93a49637..b099824f250 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -75,7 +75,7 @@ async def test_reading_broken_yaml_config(hass): "badkey.yaml": "@:\n name: Device", "noname.yaml": "my_device:\n", "allok.yaml": "My Device:\n name: Device", - "oneok.yaml": ("My Device!:\n name: Device\nbad_device:\n nme: Device"), + "oneok.yaml": "My Device!:\n name: Device\nbad_device:\n nme: Device", } args = {"hass": hass, "consider_home": timedelta(seconds=60)} with patch_yaml_files(files): diff --git a/tests/components/devolo_home_network/__init__.py b/tests/components/devolo_home_network/__init__.py index f42abef20ec..9340f7d2283 100644 --- a/tests/components/devolo_home_network/__init__.py +++ b/tests/components/devolo_home_network/__init__.py @@ -12,7 +12,7 @@ def configure_integration(hass: HomeAssistant) -> MockConfigEntry: """Configure the integration.""" config = { CONF_IP_ADDRESS: IP, - CONF_PASSWORD: "", + CONF_PASSWORD: "test", } entry = MockConfigEntry(domain=DOMAIN, data=config) entry.add_to_hass(hass) diff --git a/tests/components/devolo_home_network/const.py b/tests/components/devolo_home_network/const.py index aec27ce6a8b..75e6a57e1d4 100644 --- a/tests/components/devolo_home_network/const.py +++ b/tests/components/devolo_home_network/const.py @@ -1,26 +1,33 @@ """Constants used for mocking data.""" -from homeassistant.components import zeroconf +from devolo_plc_api.device_api import ( + WIFI_BAND_2G, + WIFI_BAND_5G, + WIFI_VAP_MAIN_AP, + ConnectedStationInfo, + NeighborAPInfo, + WifiGuestAccessGet, +) +from devolo_plc_api.plcnet_api import LogicalNetwork -IP = "1.1.1.1" +from homeassistant.components.zeroconf import ZeroconfServiceInfo -CONNECTED_STATIONS = { - "connected_stations": [ - { - "mac_address": "AA:BB:CC:DD:EE:FF", - "vap_type": "WIFI_VAP_MAIN_AP", - "band": "WIFI_BAND_5G", - "rx_rate": 87800, - "tx_rate": 87800, - } - ], -} +IP = "192.0.2.1" +IP_ALT = "192.0.2.2" -NO_CONNECTED_STATIONS = { - "connected_stations": [], -} +CONNECTED_STATIONS = [ + ConnectedStationInfo( + mac_address="AA:BB:CC:DD:EE:FF", + vap_type=WIFI_VAP_MAIN_AP, + band=WIFI_BAND_5G, + rx_rate=87800, + tx_rate=87800, + ) +] -DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( +NO_CONNECTED_STATIONS = [] + +DISCOVERY_INFO = ZeroconfServiceInfo( host=IP, addresses=[IP], port=14791, @@ -41,7 +48,28 @@ DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( }, ) -DISCOVERY_INFO_WRONG_DEVICE = zeroconf.ZeroconfServiceInfo( +DISCOVERY_INFO_CHANGED = ZeroconfServiceInfo( + host=IP_ALT, + addresses=[IP_ALT], + port=14791, + hostname="test.local.", + type="_dvl-deviceapi._tcp.local.", + name="dLAN pro 1200+ WiFi ac._dvl-deviceapi._tcp.local.", + properties={ + "Path": "abcdefghijkl/deviceapi", + "Version": "v0", + "Product": "dLAN pro 1200+ WiFi ac", + "Features": "reset,update,led,intmtg,wifi1", + "MT": "2730", + "SN": "1234567890", + "FirmwareVersion": "5.6.1", + "FirmwareDate": "2020-10-23", + "PS": "", + "PlcMacAddress": "AA:BB:CC:DD:EE:FF", + }, +) + +DISCOVERY_INFO_WRONG_DEVICE = ZeroconfServiceInfo( host="mock_host", addresses=["mock_host"], hostname="mock_hostname", @@ -51,46 +79,47 @@ DISCOVERY_INFO_WRONG_DEVICE = zeroconf.ZeroconfServiceInfo( type="mock_type", ) -NEIGHBOR_ACCESS_POINTS = { - "neighbor_aps": [ +GUEST_WIFI = WifiGuestAccessGet( + ssid="devolo-guest-930", + key="HMANPGBA", + enabled=False, + remaining_duration=0, +) + +NEIGHBOR_ACCESS_POINTS = [ + NeighborAPInfo( + mac_address="AA:BB:CC:DD:EE:FF", + ssid="wifi", + band=WIFI_BAND_2G, + channel=1, + signal=-73, + signal_bars=1, + ) +] + +PLCNET = LogicalNetwork( + devices=[ { "mac_address": "AA:BB:CC:DD:EE:FF", - "ssid": "wifi", - "band": "WIFI_BAND_2G", - "channel": 1, - "signal": -73, - "signal_bars": 1, + "attached_to_router": False, } - ] -} + ], + data_rates=[ + { + "mac_address_from": "AA:BB:CC:DD:EE:FF", + "mac_address_to": "11:22:33:44:55:66", + "rx_rate": 0.0, + "tx_rate": 0.0, + }, + ], +) -PLCNET = { - "network": { - "devices": [ - { - "mac_address": "AA:BB:CC:DD:EE:FF", - "attached_to_router": False, - } - ], - "data_rates": [ - { - "mac_address_from": "AA:BB:CC:DD:EE:FF", - "mac_address_to": "11:22:33:44:55:66", - "rx_rate": 0.0, - "tx_rate": 0.0, - }, - ], - } -} - -PLCNET_ATTACHED = { - "network": { - "devices": [ - { - "mac_address": "AA:BB:CC:DD:EE:FF", - "attached_to_router": True, - } - ], - "data_rates": [], - } -} +PLCNET_ATTACHED = LogicalNetwork( + devices=[ + { + "mac_address": "AA:BB:CC:DD:EE:FF", + "attached_to_router": True, + } + ], + data_rates=[], +) diff --git a/tests/components/devolo_home_network/mock.py b/tests/components/devolo_home_network/mock.py index 660cc19f78c..0ea985a48c7 100644 --- a/tests/components/devolo_home_network/mock.py +++ b/tests/components/devolo_home_network/mock.py @@ -1,8 +1,6 @@ """Mock of a devolo Home Network device.""" from __future__ import annotations -import dataclasses -from typing import Any from unittest.mock import AsyncMock from devolo_plc_api.device import Device @@ -15,6 +13,7 @@ from zeroconf.asyncio import AsyncZeroconf from .const import ( CONNECTED_STATIONS, DISCOVERY_INFO, + GUEST_WIFI, IP, NEIGHBOR_ACCESS_POINTS, PLCNET, @@ -27,31 +26,37 @@ class MockDevice(Device): def __init__( self, ip: str, - plcnetapi: dict[str, Any] | None = None, - deviceapi: dict[str, Any] | None = None, zeroconf_instance: AsyncZeroconf | Zeroconf | None = None, ) -> None: """Bring mock in a well defined state.""" - super().__init__(ip, plcnetapi, deviceapi, zeroconf_instance) + super().__init__(ip, zeroconf_instance) self.reset() + @property + def firmware_version(self) -> str: + """Mock firmware version currently installed.""" + return DISCOVERY_INFO.properties["FirmwareVersion"] + async def async_connect( self, session_instance: httpx.AsyncClient | None = None ) -> None: """Give a mocked device the needed properties.""" self.mac = DISCOVERY_INFO.properties["PlcMacAddress"] + self.mt_number = DISCOVERY_INFO.properties["MT"] self.product = DISCOVERY_INFO.properties["Product"] self.serial_number = DISCOVERY_INFO.properties["SN"] def reset(self): """Reset mock to starting point.""" self.async_disconnect = AsyncMock() - self.device = DeviceApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) + self.device = DeviceApi(IP, None, DISCOVERY_INFO) + self.device.async_get_led_setting = AsyncMock(return_value=False) self.device.async_get_wifi_connected_station = AsyncMock( return_value=CONNECTED_STATIONS ) + self.device.async_get_wifi_guest_access = AsyncMock(return_value=GUEST_WIFI) self.device.async_get_wifi_neighbor_access_points = AsyncMock( return_value=NEIGHBOR_ACCESS_POINTS ) - self.plcnet = PlcNetApi(IP, None, dataclasses.asdict(DISCOVERY_INFO)) + self.plcnet = PlcNetApi(IP, None, DISCOVERY_INFO) self.plcnet.async_get_network_overview = AsyncMock(return_value=PLCNET) diff --git a/tests/components/devolo_home_network/test_config_flow.py b/tests/components/devolo_home_network/test_config_flow.py index 0d35630407e..223f0a84204 100644 --- a/tests/components/devolo_home_network/test_config_flow.py +++ b/tests/components/devolo_home_network/test_config_flow.py @@ -19,9 +19,17 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from . import configure_integration -from .const import DISCOVERY_INFO, DISCOVERY_INFO_WRONG_DEVICE, IP +from .const import ( + DISCOVERY_INFO, + DISCOVERY_INFO_CHANGED, + DISCOVERY_INFO_WRONG_DEVICE, + IP, + IP_ALT, +) from .mock import MockDevice +from tests.common import MockConfigEntry + async def test_form(hass: HomeAssistant, info: dict[str, Any]): """Test we get the form.""" @@ -132,20 +140,11 @@ async def test_abort_zeroconf_wrong_device(hass: HomeAssistant): @pytest.mark.usefixtures("info") async def test_abort_if_configued(hass: HomeAssistant): """Test we abort config flow if already configured.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + serial_number = DISCOVERY_INFO.properties["SN"] + entry = MockConfigEntry( + domain=DOMAIN, unique_id=serial_number, data={CONF_IP_ADDRESS: IP} ) - with patch( - "homeassistant.components.devolo_home_network.async_setup_entry", - return_value=True, - ): - await hass.config_entries.flow.async_configure( - result["flow_id"], - { - CONF_IP_ADDRESS: IP, - }, - ) - await hass.async_block_till_done() + entry.add_to_hass(hass) # Abort on concurrent user flow result = await hass.config_entries.flow.async_init( @@ -165,10 +164,11 @@ async def test_abort_if_configued(hass: HomeAssistant): result3 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, - data=DISCOVERY_INFO, + data=DISCOVERY_INFO_CHANGED, ) assert result3["type"] == FlowResultType.ABORT assert result3["reason"] == "already_configured" + assert entry.data[CONF_IP_ADDRESS] == IP_ALT @pytest.mark.usefixtures("mock_device") diff --git a/tests/components/devolo_home_network/test_device_tracker.py b/tests/components/devolo_home_network/test_device_tracker.py index 2f8fea3e749..e2cabc9a18c 100644 --- a/tests/components/devolo_home_network/test_device_tracker.py +++ b/tests/components/devolo_home_network/test_device_tracker.py @@ -11,10 +11,10 @@ from homeassistant.components.devolo_home_network.const import ( WIFI_BANDS, ) from homeassistant.const import ( - FREQUENCY_GIGAHERTZ, STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE, + UnitOfFrequency, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry @@ -26,13 +26,15 @@ from .mock import MockDevice from tests.common import async_fire_time_changed -STATION = CONNECTED_STATIONS["connected_stations"][0] +STATION = CONNECTED_STATIONS[0] SERIAL = DISCOVERY_INFO.properties["SN"] async def test_device_tracker(hass: HomeAssistant, mock_device: MockDevice): """Test device tracker states.""" - state_key = f"{PLATFORM}.{DOMAIN}_{SERIAL}_{STATION['mac_address'].lower().replace(':', '_')}" + state_key = ( + f"{PLATFORM}.{DOMAIN}_{SERIAL}_{STATION.mac_address.lower().replace(':', '_')}" + ) entry = configure_integration(hass) er = entity_registry.async_get(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -49,10 +51,10 @@ async def test_device_tracker(hass: HomeAssistant, mock_device: MockDevice): state = hass.states.get(state_key) assert state is not None assert state.state == STATE_HOME - assert state.attributes["wifi"] == WIFI_APTYPE[STATION["vap_type"]] + assert state.attributes["wifi"] == WIFI_APTYPE[STATION.vap_type] assert ( state.attributes["band"] - == f"{WIFI_BANDS[STATION['band']]} {FREQUENCY_GIGAHERTZ}" + == f"{WIFI_BANDS[STATION.band]} {UnitOfFrequency.GIGAHERTZ}" ) # Emulate state change @@ -82,13 +84,15 @@ async def test_device_tracker(hass: HomeAssistant, mock_device: MockDevice): async def test_restoring_clients(hass: HomeAssistant, mock_device: MockDevice): """Test restoring existing device_tracker entities.""" - state_key = f"{PLATFORM}.{DOMAIN}_{SERIAL}_{STATION['mac_address'].lower().replace(':', '_')}" + state_key = ( + f"{PLATFORM}.{DOMAIN}_{SERIAL}_{STATION.mac_address.lower().replace(':', '_')}" + ) entry = configure_integration(hass) er = entity_registry.async_get(hass) er.async_get_or_create( PLATFORM, DOMAIN, - f"{SERIAL}_{STATION['mac_address']}", + f"{SERIAL}_{STATION.mac_address}", config_entry=entry, ) diff --git a/tests/components/devolo_home_network/test_diagnostics.py b/tests/components/devolo_home_network/test_diagnostics.py new file mode 100644 index 00000000000..fe9c6c1f106 --- /dev/null +++ b/tests/components/devolo_home_network/test_diagnostics.py @@ -0,0 +1,45 @@ +"""Tests for the devolo Home Network diagnostics.""" +from __future__ import annotations + +from aiohttp import ClientSession +import pytest + +from homeassistant.components.devolo_home_network.diagnostics import TO_REDACT +from homeassistant.components.diagnostics import REDACTED +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import configure_integration +from .const import DISCOVERY_INFO + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +@pytest.mark.usefixtures("mock_device") +async def test_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, +): + """Test config entry diagnostics.""" + entry = configure_integration(hass) + await hass.config_entries.async_setup(entry.entry_id) + 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) + assert result == { + "entry": entry_dict, + "device_info": { + "mt_number": DISCOVERY_INFO.properties["MT"], + "product": DISCOVERY_INFO.properties["Product"], + "firmware": DISCOVERY_INFO.properties["FirmwareVersion"], + "device_api": True, + "plcnet_api": True, + "features": DISCOVERY_INFO.properties["Features"].split(","), + }, + } diff --git a/tests/components/devolo_home_network/test_switch.py b/tests/components/devolo_home_network/test_switch.py new file mode 100644 index 00000000000..47924f4aa85 --- /dev/null +++ b/tests/components/devolo_home_network/test_switch.py @@ -0,0 +1,326 @@ +"""Tests for the devolo Home Network switch.""" +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +from devolo_plc_api.device_api import WifiGuestAccessGet +from devolo_plc_api.exceptions.device import DevicePasswordProtected, DeviceUnavailable +import pytest + +from homeassistant.components.devolo_home_network.const import ( + DOMAIN, + SHORT_UPDATE_INTERVAL, +) +from homeassistant.components.switch import DOMAIN as PLATFORM +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import ( + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.update_coordinator import REQUEST_REFRESH_DEFAULT_COOLDOWN +from homeassistant.util import dt + +from . import configure_integration +from .mock import MockDevice + +from tests.common import async_fire_time_changed + + +@pytest.mark.usefixtures("mock_device") +async def test_switch_setup(hass: HomeAssistant): + """Test default setup of the switch component.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(f"{PLATFORM}.{device_name}_enable_guest_wifi") is not None + assert hass.states.get(f"{PLATFORM}.{device_name}_enable_leds") is not None + + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_update_guest_wifi_status_auth_failed( + hass: HomeAssistant, mock_device: MockDevice +): + """Test getting the wifi_status with wrong password triggers the reauth flow.""" + entry = configure_integration(hass) + mock_device.device.async_get_wifi_guest_access.side_effect = DevicePasswordProtected + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + + assert "context" in flow + assert flow["context"]["source"] == SOURCE_REAUTH + assert flow["context"]["entry_id"] == entry.entry_id + + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_update_enable_guest_wifi(hass: HomeAssistant, mock_device: MockDevice): + """Test state change of a enable_guest_wifi switch device.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{PLATFORM}.{device_name}_enable_guest_wifi" + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_OFF + + # Emulate state change + mock_device.device.async_get_wifi_guest_access.return_value = WifiGuestAccessGet( + enabled=True + ) + async_fire_time_changed(hass, dt.utcnow() + SHORT_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_ON + + # Switch off + mock_device.device.async_get_wifi_guest_access.return_value = WifiGuestAccessGet( + enabled=False + ) + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_set_wifi_guest_access", + new=AsyncMock(), + ) as turn_off: + await hass.services.async_call( + PLATFORM, SERVICE_TURN_OFF, {"entity_id": state_key}, blocking=True + ) + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_OFF + turn_off.assert_called_once_with(False) + + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=REQUEST_REFRESH_DEFAULT_COOLDOWN) + ) + await hass.async_block_till_done() + + # Switch on + mock_device.device.async_get_wifi_guest_access.return_value = WifiGuestAccessGet( + enabled=True + ) + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_set_wifi_guest_access", + new=AsyncMock(), + ) as turn_on: + await hass.services.async_call( + PLATFORM, SERVICE_TURN_ON, {"entity_id": state_key}, blocking=True + ) + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_ON + turn_on.assert_called_once_with(True) + + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=REQUEST_REFRESH_DEFAULT_COOLDOWN) + ) + await hass.async_block_till_done() + + # Device unavailable + mock_device.device.async_get_wifi_guest_access.side_effect = DeviceUnavailable() + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_set_wifi_guest_access", + side_effect=DeviceUnavailable, + ): + await hass.services.async_call( + PLATFORM, SERVICE_TURN_ON, {"entity_id": state_key}, blocking=True + ) + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_unload(entry.entry_id) + + +async def test_update_enable_leds(hass: HomeAssistant, mock_device: MockDevice): + """Test state change of a enable_leds switch device.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{PLATFORM}.{device_name}_enable_leds" + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_OFF + + er = entity_registry.async_get(hass) + assert er.async_get(state_key).entity_category == EntityCategory.CONFIG + + # Emulate state change + mock_device.device.async_get_led_setting.return_value = True + async_fire_time_changed(hass, dt.utcnow() + SHORT_UPDATE_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_ON + + # Switch off + mock_device.device.async_get_led_setting.return_value = False + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_set_led_setting", + new=AsyncMock(), + ) as turn_off: + await hass.services.async_call( + PLATFORM, SERVICE_TURN_OFF, {"entity_id": state_key}, blocking=True + ) + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_OFF + turn_off.assert_called_once_with(False) + + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=REQUEST_REFRESH_DEFAULT_COOLDOWN) + ) + await hass.async_block_till_done() + + # Switch on + mock_device.device.async_get_led_setting.return_value = True + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_set_led_setting", + new=AsyncMock(), + ) as turn_on: + await hass.services.async_call( + PLATFORM, SERVICE_TURN_ON, {"entity_id": state_key}, blocking=True + ) + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_ON + turn_on.assert_called_once_with(True) + + async_fire_time_changed( + hass, dt.utcnow() + timedelta(seconds=REQUEST_REFRESH_DEFAULT_COOLDOWN) + ) + await hass.async_block_till_done() + + # Device unavailable + mock_device.device.async_get_led_setting.side_effect = DeviceUnavailable() + with patch( + "devolo_plc_api.device_api.deviceapi.DeviceApi.async_set_led_setting", + side_effect=DeviceUnavailable, + ): + await hass.services.async_call( + PLATFORM, SERVICE_TURN_OFF, {"entity_id": state_key}, blocking=True + ) + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_unload(entry.entry_id) + + +@pytest.mark.parametrize( + "name, get_method, update_interval", + [ + ["enable_guest_wifi", "async_get_wifi_guest_access", SHORT_UPDATE_INTERVAL], + ["enable_leds", "async_get_led_setting", SHORT_UPDATE_INTERVAL], + ], +) +async def test_device_failure( + hass: HomeAssistant, + mock_device: MockDevice, + name: str, + get_method: str, + update_interval: timedelta, +): + """Test device failure.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{PLATFORM}.{device_name}_{name}" + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + + api = getattr(mock_device.device, get_method) + api.side_effect = DeviceUnavailable + async_fire_time_changed(hass, dt.utcnow() + update_interval) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize( + "name, set_method", + [ + ["enable_guest_wifi", "async_set_wifi_guest_access"], + ["enable_leds", "async_set_led_setting"], + ], +) +async def test_auth_failed( + hass: HomeAssistant, mock_device: MockDevice, name: str, set_method: str +): + """Test setting unautherized triggers the reauth flow.""" + entry = configure_integration(hass) + device_name = entry.title.replace(" ", "_").lower() + state_key = f"{PLATFORM}.{device_name}_{name}" + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(state_key) + assert state is not None + + setattr(mock_device.device, set_method, AsyncMock()) + api = getattr(mock_device.device, set_method) + api.side_effect = DevicePasswordProtected + + await hass.services.async_call( + PLATFORM, SERVICE_TURN_ON, {"entity_id": state_key}, blocking=True + ) + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + assert "context" in flow + assert flow["context"]["source"] == SOURCE_REAUTH + assert flow["context"]["entry_id"] == entry.entry_id + + await hass.services.async_call( + PLATFORM, SERVICE_TURN_OFF, {"entity_id": state_key}, blocking=True + ) + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + assert "context" in flow + assert flow["context"]["source"] == SOURCE_REAUTH + assert flow["context"]["entry_id"] == entry.entry_id + + await hass.config_entries.async_unload(entry.entry_id) diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py index b84341f44ea..ffd58ec5ea8 100644 --- a/tests/components/dhcp/test_init.py +++ b/tests/components/dhcp/test_init.py @@ -483,7 +483,7 @@ async def test_dhcp_invalid_option(hass): ("requested_addr", "192.168.208.55"), ("server_id", "192.168.208.1"), ("param_req_list", [1, 3, 28, 6]), - ("hostname"), + "hostname", ] async_handle_dhcp_packet = await _async_get_handle_dhcp_packet( diff --git a/tests/components/directv/test_init.py b/tests/components/directv/test_init.py index cdfc9b1bcad..dd7b5b09f9f 100644 --- a/tests/components/directv/test_init.py +++ b/tests/components/directv/test_init.py @@ -7,8 +7,6 @@ from . import setup_integration from tests.test_util.aiohttp import AiohttpClientMocker -# pylint: disable=redefined-outer-name - async def test_config_entry_not_ready( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 60ccc49457c..af033ab206d 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -61,8 +61,6 @@ RESTRICTED_ENTITY_ID = f"{MP_DOMAIN}.restricted_client" STANDBY_ENTITY_ID = f"{MP_DOMAIN}.standby_client" UNAVAILABLE_ENTITY_ID = f"{MP_DOMAIN}.unavailable_client" -# pylint: disable=redefined-outer-name - @fixture def mock_now() -> datetime: diff --git a/tests/components/directv/test_remote.py b/tests/components/directv/test_remote.py index 1dcd6c88047..7a674fefa8c 100644 --- a/tests/components/directv/test_remote.py +++ b/tests/components/directv/test_remote.py @@ -19,8 +19,6 @@ CLIENT_ENTITY_ID = f"{REMOTE_DOMAIN}.client" MAIN_ENTITY_ID = f"{REMOTE_DOMAIN}.host" UNAVAILABLE_ENTITY_ID = f"{REMOTE_DOMAIN}.unavailable_client" -# pylint: disable=redefined-outer-name - async def test_setup(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) -> None: """Test setup with basic config.""" diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py index 9bc1e9a6812..df1a67245db 100644 --- a/tests/components/discovery/test_init.py +++ b/tests/components/discovery/test_init.py @@ -92,7 +92,9 @@ async def test_discover_config_flow(hass): with patch.dict( discovery.CONFIG_ENTRY_HANDLERS, {"mock-service": "mock-component"} - ), patch("homeassistant.data_entry_flow.FlowManager.async_init") as m_init: + ), patch( + "homeassistant.config_entries.ConfigEntriesFlowManager.async_init" + ) as m_init: await mock_discovery(hass, discover) assert len(m_init.mock_calls) == 1 diff --git a/tests/components/dlink/__init__.py b/tests/components/dlink/__init__.py new file mode 100644 index 00000000000..49801df4391 --- /dev/null +++ b/tests/components/dlink/__init__.py @@ -0,0 +1 @@ +"""Tests for the D-Link Smart Plug integration.""" diff --git a/tests/components/dlink/conftest.py b/tests/components/dlink/conftest.py new file mode 100644 index 00000000000..4e064b35d5f --- /dev/null +++ b/tests/components/dlink/conftest.py @@ -0,0 +1,90 @@ +"""Configure pytest for D-Link tests.""" + +from copy import deepcopy +from unittest.mock import MagicMock, patch + +import pytest + +from homeassistant.components import dhcp +from homeassistant.components.dlink.const import CONF_USE_LEGACY_PROTOCOL, DOMAIN +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import format_mac + +from tests.common import MockConfigEntry + +HOST = "1.2.3.4" +PASSWORD = "123456" +MAC = format_mac("AA:BB:CC:DD:EE:FF") +USERNAME = "admin" + +CONF_DHCP_DATA = { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_USE_LEGACY_PROTOCOL: True, +} + +CONF_DATA = CONF_DHCP_DATA | {CONF_HOST: HOST} + +CONF_IMPORT_DATA = CONF_DATA | {CONF_NAME: "Smart Plug"} + +CONF_DHCP_FLOW = dhcp.DhcpServiceInfo( + ip=HOST, + macaddress=MAC, + hostname="dsp-w215", +) + +CONF_DHCP_FLOW_NEW_IP = dhcp.DhcpServiceInfo( + ip="5.6.7.8", + macaddress=MAC, + hostname="dsp-w215", +) + + +def create_entry(hass: HomeAssistant) -> MockConfigEntry: + """Create fixture for adding config entry in Home Assistant.""" + entry = MockConfigEntry(domain=DOMAIN, data=CONF_DATA) + entry.add_to_hass(hass) + return entry + + +@pytest.fixture() +def config_entry(hass: HomeAssistant) -> MockConfigEntry: + """Add config entry in Home Assistant.""" + return create_entry(hass) + + +@pytest.fixture() +def config_entry_with_uid(hass: HomeAssistant) -> MockConfigEntry: + """Add config entry with unique ID in Home Assistant.""" + config_entry = create_entry(hass) + config_entry.unique_id = "aa:bb:cc:dd:ee:ff" + return config_entry + + +@pytest.fixture() +def mocked_plug() -> MagicMock: + """Create mocked plug device.""" + mocked_plug = MagicMock() + mocked_plug.state = "OFF" + mocked_plug.temperature = 0 + mocked_plug.current_consumption = "N/A" + mocked_plug.total_consumption = "N/A" + mocked_plug.authenticated = ("0123456789ABCDEF0123456789ABCDEF", "ABCDefGHiJ") + return mocked_plug + + +@pytest.fixture() +def mocked_plug_no_auth(mocked_plug: MagicMock) -> MagicMock: + """Create mocked unauthenticated plug device.""" + mocked_plug = deepcopy(mocked_plug) + mocked_plug.authenticated = None + return mocked_plug + + +def patch_config_flow(mocked_plug: MagicMock): + """Patch D-Link Smart Plug config flow.""" + return patch( + "homeassistant.components.dlink.config_flow.SmartPlug", + return_value=mocked_plug, + ) diff --git a/tests/components/dlink/test_config_flow.py b/tests/components/dlink/test_config_flow.py new file mode 100644 index 00000000000..3e5bdf2106a --- /dev/null +++ b/tests/components/dlink/test_config_flow.py @@ -0,0 +1,204 @@ +"""Test D-Link Smart Plug config flow.""" +from unittest.mock import MagicMock, patch + +from homeassistant import data_entry_flow +from homeassistant.components import dhcp +from homeassistant.components.dlink.const import DEFAULT_NAME, DOMAIN +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant + +from .conftest import ( + CONF_DATA, + CONF_DHCP_DATA, + CONF_DHCP_FLOW, + CONF_DHCP_FLOW_NEW_IP, + CONF_IMPORT_DATA, + patch_config_flow, +) + +from tests.common import MockConfigEntry + + +def _patch_setup_entry(): + return patch("homeassistant.components.dlink.async_setup_entry", return_value=True) + + +async def test_flow_user(hass: HomeAssistant, mocked_plug: MagicMock) -> None: + """Test user initialized flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + with patch_config_flow(mocked_plug), _patch_setup_entry(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DATA, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == CONF_DATA + + +async def test_flow_user_already_configured( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test user initialized flow with duplicate server.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_user_cannot_connect( + hass: HomeAssistant, mocked_plug: MagicMock, mocked_plug_no_auth: MagicMock +) -> None: + """Test user initialized flow with unreachable server.""" + with patch_config_flow(mocked_plug_no_auth): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "cannot_connect" + + with patch_config_flow(mocked_plug), _patch_setup_entry(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DATA, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == CONF_DATA + + +async def test_flow_user_unknown_error( + hass: HomeAssistant, mocked_plug: MagicMock +) -> None: + """Test user initialized flow with unreachable server.""" + with patch_config_flow(mocked_plug) as mock: + mock.side_effect = Exception + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "unknown" + + with patch_config_flow(mocked_plug), _patch_setup_entry(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DATA, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == CONF_DATA + + +async def test_import(hass: HomeAssistant, mocked_plug: MagicMock) -> None: + """Test import initialized flow.""" + with patch_config_flow(mocked_plug), _patch_setup_entry(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=CONF_IMPORT_DATA, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "Smart Plug" + assert result["data"] == CONF_DATA + + +async def test_dhcp(hass: HomeAssistant, mocked_plug: MagicMock) -> None: + """Test we can process the discovery from dhcp.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "confirm_discovery" + with patch_config_flow(mocked_plug), _patch_setup_entry(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DHCP_DATA, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == CONF_DATA + + +async def test_dhcp_failed_auth( + hass: HomeAssistant, mocked_plug: MagicMock, mocked_plug_no_auth: MagicMock +) -> None: + """Test we can recovery from failed authentication during dhcp flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "confirm_discovery" + with patch_config_flow(mocked_plug_no_auth): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DHCP_DATA, + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"]["base"] == "cannot_connect" + + with patch_config_flow(mocked_plug), _patch_setup_entry(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DHCP_DATA, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == DEFAULT_NAME + assert result["data"] == CONF_DATA + + +async def test_dhcp_already_configured( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> None: + """Test dhcp initialized flow with duplicate server.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert config_entry.unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_dhcp_unique_id_assignment( + hass: HomeAssistant, mocked_plug: MagicMock +) -> None: + """Test dhcp initialized flow with no unique id for matching entry.""" + dhcp_data = dhcp.DhcpServiceInfo( + ip="2.3.4.5", + macaddress="11:22:33:44:55:66", + hostname="dsp-w215", + ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "confirm_discovery" + with patch_config_flow(mocked_plug), _patch_setup_entry(): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_DHCP_DATA, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == CONF_DATA | {CONF_HOST: "2.3.4.5"} + assert result["result"].unique_id == "11:22:33:44:55:66" + + +async def test_dhcp_changed_ip( + hass: HomeAssistant, config_entry_with_uid: MockConfigEntry +) -> None: + """Test that we successfully change IP address for device with known mac address.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=CONF_DHCP_FLOW_NEW_IP + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + assert config_entry_with_uid.data[CONF_HOST] == "5.6.7.8" diff --git a/tests/components/dlna_dmr/test_media_player.py b/tests/components/dlna_dmr/test_media_player.py index e9c6bbfda15..889cc92c969 100644 --- a/tests/components/dlna_dmr/test_media_player.py +++ b/tests/components/dlna_dmr/test_media_player.py @@ -674,7 +674,9 @@ async def test_play_media_stopped( { ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, - mp_const.ATTR_MEDIA_CONTENT_ID: "http://198.51.100.20:8200/MediaItems/17621.mp3", + mp_const.ATTR_MEDIA_CONTENT_ID: ( + "http://198.51.100.20:8200/MediaItems/17621.mp3" + ), mp_const.ATTR_MEDIA_ENQUEUE: False, }, blocking=True, @@ -706,7 +708,9 @@ async def test_play_media_playing( { ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, - mp_const.ATTR_MEDIA_CONTENT_ID: "http://198.51.100.20:8200/MediaItems/17621.mp3", + mp_const.ATTR_MEDIA_CONTENT_ID: ( + "http://198.51.100.20:8200/MediaItems/17621.mp3" + ), mp_const.ATTR_MEDIA_ENQUEUE: False, }, blocking=True, @@ -739,7 +743,9 @@ async def test_play_media_no_autoplay( { ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, - mp_const.ATTR_MEDIA_CONTENT_ID: "http://198.51.100.20:8200/MediaItems/17621.mp3", + mp_const.ATTR_MEDIA_CONTENT_ID: ( + "http://198.51.100.20:8200/MediaItems/17621.mp3" + ), mp_const.ATTR_MEDIA_ENQUEUE: False, mp_const.ATTR_MEDIA_EXTRA: {"autoplay": False}, }, @@ -770,7 +776,9 @@ async def test_play_media_metadata( { ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, - mp_const.ATTR_MEDIA_CONTENT_ID: "http://198.51.100.20:8200/MediaItems/17621.mp3", + mp_const.ATTR_MEDIA_CONTENT_ID: ( + "http://198.51.100.20:8200/MediaItems/17621.mp3" + ), mp_const.ATTR_MEDIA_ENQUEUE: False, mp_const.ATTR_MEDIA_EXTRA: { "title": "Mock song", @@ -800,7 +808,9 @@ async def test_play_media_metadata( { ATTR_ENTITY_ID: mock_entity_id, mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.TVSHOW, - mp_const.ATTR_MEDIA_CONTENT_ID: "http://198.51.100.20:8200/MediaItems/123.mkv", + mp_const.ATTR_MEDIA_CONTENT_ID: ( + "http://198.51.100.20:8200/MediaItems/123.mkv" + ), mp_const.ATTR_MEDIA_ENQUEUE: False, mp_const.ATTR_MEDIA_EXTRA: { "title": "Mock show", @@ -833,7 +843,9 @@ async def test_play_media_local_source( { 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", + mp_const.ATTR_MEDIA_CONTENT_ID: ( + "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4" + ), }, blocking=True, ) @@ -888,7 +900,9 @@ async def test_play_media_didl_metadata( { 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", + mp_const.ATTR_MEDIA_CONTENT_ID: ( + "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4" + ), }, blocking=True, ) @@ -1011,7 +1025,9 @@ async def test_browse_media( "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", + "media_content_id": ( + "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4" + ), "can_play": True, "can_expand": False, "thumbnail": None, @@ -1104,7 +1120,9 @@ async def test_browse_media_unfiltered( "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", + "media_content_id": ( + "media-source://media_source/local/Epic Sax Guy 10 Hours.mp4" + ), "can_play": True, "can_expand": False, "thumbnail": None, @@ -1280,7 +1298,9 @@ async def test_unavailable_device( mp_const.SERVICE_PLAY_MEDIA, { mp_const.ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC, - mp_const.ATTR_MEDIA_CONTENT_ID: "http://198.51.100.20:8200/MediaItems/17621.mp3", + mp_const.ATTR_MEDIA_CONTENT_ID: ( + "http://198.51.100.20:8200/MediaItems/17621.mp3" + ), mp_const.ATTR_MEDIA_ENQUEUE: False, }, ), diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index ac4b9587ec7..01d45287c28 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -22,12 +22,11 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - ENERGY_KILO_WATT_HOUR, STATE_UNAVAILABLE, STATE_UNKNOWN, - VOLUME_CUBIC_METERS, UnitOfEnergy, UnitOfPower, + UnitOfVolume, ) from homeassistant.helpers import entity_registry as er @@ -65,7 +64,7 @@ async def test_default_setup(hass, dsmr_connection_fixture): GAS_METER_READING: MBusObject( [ {"value": datetime.datetime.fromtimestamp(1551642213)}, - {"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, + {"value": Decimal(745.695), "unit": UnitOfVolume.CUBIC_METERS}, ] ), } @@ -133,7 +132,8 @@ async def test_default_setup(hass, dsmr_connection_fixture): == SensorStateClass.TOTAL_INCREASING ) assert ( - gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS ) @@ -228,13 +228,17 @@ async def test_v4_meter(hass, dsmr_connection_fixture): gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") assert gas_consumption.state == "745.695" assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS - assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS + assert ( + gas_consumption.attributes.get("unit_of_measurement") + == UnitOfVolume.CUBIC_METERS + ) assert ( gas_consumption.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING ) assert ( - gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS ) @@ -305,7 +309,8 @@ async def test_v5_meter(hass, dsmr_connection_fixture): == SensorStateClass.TOTAL_INCREASING ) assert ( - gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS ) @@ -340,10 +345,10 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): ] ), ELECTRICITY_IMPORTED_TOTAL: CosemObject( - [{"value": Decimal(123.456), "unit": ENERGY_KILO_WATT_HOUR}] + [{"value": Decimal(123.456), "unit": UnitOfEnergy.KILO_WATT_HOUR}] ), ELECTRICITY_EXPORTED_TOTAL: CosemObject( - [{"value": Decimal(654.321), "unit": ENERGY_KILO_WATT_HOUR}] + [{"value": Decimal(654.321), "unit": UnitOfEnergy.KILO_WATT_HOUR}] ), } @@ -373,12 +378,16 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): == SensorStateClass.TOTAL_INCREASING ) assert ( - active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfEnergy.KILO_WATT_HOUR ) active_tariff = hass.states.get("sensor.electricity_meter_energy_production_total") assert active_tariff.state == "654.321" - assert active_tariff.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert ( + active_tariff.attributes.get("unit_of_measurement") + == UnitOfEnergy.KILO_WATT_HOUR + ) # check if gas consumption is parsed correctly gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption") @@ -389,7 +398,8 @@ async def test_luxembourg_meter(hass, dsmr_connection_fixture): == SensorStateClass.TOTAL_INCREASING ) assert ( - gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS ) @@ -460,7 +470,8 @@ async def test_belgian_meter(hass, dsmr_connection_fixture): == SensorStateClass.TOTAL_INCREASING ) assert ( - gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS ) @@ -536,10 +547,10 @@ async def test_swedish_meter(hass, dsmr_connection_fixture): telegram = { ELECTRICITY_IMPORTED_TOTAL: CosemObject( - [{"value": Decimal(123.456), "unit": ENERGY_KILO_WATT_HOUR}] + [{"value": Decimal(123.456), "unit": UnitOfEnergy.KILO_WATT_HOUR}] ), ELECTRICITY_EXPORTED_TOTAL: CosemObject( - [{"value": Decimal(654.321), "unit": ENERGY_KILO_WATT_HOUR}] + [{"value": Decimal(654.321), "unit": UnitOfEnergy.KILO_WATT_HOUR}] ), } @@ -569,7 +580,8 @@ async def test_swedish_meter(hass, dsmr_connection_fixture): == SensorStateClass.TOTAL_INCREASING ) assert ( - active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfEnergy.KILO_WATT_HOUR ) active_tariff = hass.states.get("sensor.electricity_meter_energy_production_total") @@ -579,7 +591,8 @@ async def test_swedish_meter(hass, dsmr_connection_fixture): == SensorStateClass.TOTAL_INCREASING ) assert ( - active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfEnergy.KILO_WATT_HOUR ) @@ -607,10 +620,10 @@ async def test_easymeter(hass, dsmr_connection_fixture): telegram = { ELECTRICITY_IMPORTED_TOTAL: CosemObject( - [{"value": Decimal(54184.6316), "unit": ENERGY_KILO_WATT_HOUR}] + [{"value": Decimal(54184.6316), "unit": UnitOfEnergy.KILO_WATT_HOUR}] ), ELECTRICITY_EXPORTED_TOTAL: CosemObject( - [{"value": Decimal(19981.1069), "unit": ENERGY_KILO_WATT_HOUR}] + [{"value": Decimal(19981.1069), "unit": UnitOfEnergy.KILO_WATT_HOUR}] ), } @@ -643,7 +656,8 @@ async def test_easymeter(hass, dsmr_connection_fixture): == SensorStateClass.TOTAL_INCREASING ) assert ( - active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfEnergy.KILO_WATT_HOUR ) active_tariff = hass.states.get("sensor.electricity_meter_energy_production_total") @@ -653,7 +667,8 @@ async def test_easymeter(hass, dsmr_connection_fixture): == SensorStateClass.TOTAL_INCREASING ) assert ( - active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + active_tariff.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfEnergy.KILO_WATT_HOUR ) diff --git a/tests/components/ecobee/test_config_flow.py b/tests/components/ecobee/test_config_flow.py index 0c80f20859f..ca94d05f743 100644 --- a/tests/components/ecobee/test_config_flow.py +++ b/tests/components/ecobee/test_config_flow.py @@ -86,9 +86,8 @@ async def test_token_request_succeeds(hass): mock_ecobee.request_tokens.return_value = True mock_ecobee.api_key = "test-api-key" mock_ecobee.refresh_token = "test-token" - # pylint: disable=protected-access + flow._ecobee = mock_ecobee - # pylint: enable=protected-access result = await flow.async_step_authorize(user_input={}) @@ -110,9 +109,8 @@ async def test_token_request_fails(hass): mock_ecobee = mock_ecobee.return_value mock_ecobee.request_tokens.return_value = False mock_ecobee.pin = "test-pin" - # pylint: disable=protected-access + flow._ecobee = mock_ecobee - # pylint: enable=protected-access result = await flow.async_step_authorize(user_input={}) diff --git a/tests/components/efergy/test_sensor.py b/tests/components/efergy/test_sensor.py index 4df383a5487..6692b320b4d 100644 --- a/tests/components/efergy/test_sensor.py +++ b/tests/components/efergy/test_sensor.py @@ -11,9 +11,9 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, STATE_UNAVAILABLE, + UnitOfEnergy, + UnitOfPower, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -38,7 +38,7 @@ async def test_sensor_readings( state = hass.states.get("sensor.power_usage") assert state.state == "1580" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get("sensor.energy_budget") assert state.state == "ok" @@ -48,22 +48,22 @@ async def test_sensor_readings( state = hass.states.get("sensor.daily_consumption") assert state.state == "38.21" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING state = hass.states.get("sensor.weekly_consumption") assert state.state == "267.47" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING state = hass.states.get("sensor.monthly_consumption") assert state.state == "1069.88" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING state = hass.states.get("sensor.yearly_consumption") assert state.state == "13373.50" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING state = hass.states.get("sensor.daily_energy_cost") assert state.state == "5.27" @@ -93,7 +93,7 @@ async def test_sensor_readings( state = hass.states.get("sensor.power_usage_728386") assert state.state == "1628" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT @@ -107,17 +107,17 @@ async def test_multi_sensor_readings( state = hass.states.get("sensor.power_usage_728386") assert state.state == "218" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get("sensor.power_usage_0") assert state.state == "1808" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get("sensor.power_usage_728387") assert state.state == "312" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT diff --git a/tests/components/elgato/test_config_flow.py b/tests/components/elgato/test_config_flow.py index dbdfbfed1d0..bb5a2375f80 100644 --- a/tests/components/elgato/test_config_flow.py +++ b/tests/components/elgato/test_config_flow.py @@ -26,7 +26,6 @@ async def test_full_user_flow_implementation( assert result.get("type") == FlowResultType.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_HOST: "127.0.0.1", CONF_PORT: 9123} @@ -69,7 +68,6 @@ async def test_full_zeroconf_flow_implementation( assert result.get("description_placeholders") == {"serial_number": "CN11A1A00001"} assert result.get("step_id") == "zeroconf_confirm" assert result.get("type") == FlowResultType.FORM - assert "flow_id" in result progress = hass.config_entries.flow.async_progress() assert len(progress) == 1 diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index a58528b700a..9bd3f949d7f 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -1587,9 +1587,6 @@ async def test_multiple_instances_with_tls_v12(hass): ) await hass.async_block_till_done() - import pprint - - pprint.pprint(result2) assert result2["type"] == "create_entry" assert result2["title"] == "guest_house" assert result2["data"] == { diff --git a/tests/components/energy/test_sensor.py b/tests/components/energy/test_sensor.py index 636eb74e4d3..6ea606a4344 100644 --- a/tests/components/energy/test_sensor.py +++ b/tests/components/energy/test_sensor.py @@ -1,6 +1,7 @@ """Test the Energy sensors.""" import copy from datetime import timedelta +import gc from unittest.mock import patch from freezegun import freeze_time @@ -18,10 +19,8 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, - VOLUME_CUBIC_FEET, - VOLUME_CUBIC_METERS, - VOLUME_GALLONS, UnitOfEnergy, + UnitOfVolume, ) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -31,6 +30,18 @@ from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM from tests.components.recorder.common import async_wait_recording_done +@pytest.fixture(autouse=True) +def garbage_collection(): + """Make sure garbage collection is run between all tests. + + There are unknown issues with GC triggering during a test + case, leading to the test breaking down. Make sure we + clean up between each testcase to avoid this issue. + """ + yield + gc.collect() + + @pytest.fixture async def setup_integration(recorder_mock): """Set up the integration.""" @@ -844,7 +855,7 @@ async def test_cost_sensor_handle_price_units( @pytest.mark.parametrize( "unit", - (VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS), + (UnitOfVolume.CUBIC_FEET, UnitOfVolume.CUBIC_METERS), ) async def test_cost_sensor_handle_gas( setup_integration, hass, hass_storage, unit @@ -950,9 +961,9 @@ async def test_cost_sensor_handle_gas_kwh( "unit_system,usage_unit,growth", ( # 1 cubic foot = 7.47 gl, 100 ft3 growth @ 0.5/ft3: - (US_CUSTOMARY_SYSTEM, VOLUME_CUBIC_FEET, 374.025974025974), - (US_CUSTOMARY_SYSTEM, VOLUME_GALLONS, 50.0), - (METRIC_SYSTEM, VOLUME_CUBIC_METERS, 50.0), + (US_CUSTOMARY_SYSTEM, UnitOfVolume.CUBIC_FEET, 374.025974025974), + (US_CUSTOMARY_SYSTEM, UnitOfVolume.GALLONS, 50.0), + (METRIC_SYSTEM, UnitOfVolume.CUBIC_METERS, 50.0), ), ) async def test_cost_sensor_handle_water( @@ -1152,7 +1163,7 @@ async def test_inherit_source_unique_id(setup_integration, hass, hass_storage): "sensor.gas_consumption", 100, { - ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfVolume.CUBIC_METERS, ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, }, ) diff --git a/tests/components/energy/test_validate.py b/tests/components/energy/test_validate.py index f1e626c24d5..924a63dc12c 100644 --- a/tests/components/energy/test_validate.py +++ b/tests/components/energy/test_validate.py @@ -118,13 +118,13 @@ async def test_validation_device_consumption_entity_missing(hass, mock_energy_ma [ { "type": "statistics_not_defined", - "identifier": "sensor.not_exist", - "value": None, + "affected_entities": {("sensor.not_exist", None)}, + "translation_placeholders": None, }, { "type": "entity_not_defined", - "identifier": "sensor.not_exist", - "value": None, + "affected_entities": {("sensor.not_exist", None)}, + "translation_placeholders": None, }, ] ], @@ -142,8 +142,8 @@ async def test_validation_device_consumption_stat_missing(hass, mock_energy_mana [ { "type": "statistics_not_defined", - "identifier": "external:not_exist", - "value": None, + "affected_entities": {("external:not_exist", None)}, + "translation_placeholders": None, } ] ], @@ -165,8 +165,8 @@ async def test_validation_device_consumption_entity_unavailable( [ { "type": "entity_unavailable", - "identifier": "sensor.unavailable", - "value": "unavailable", + "affected_entities": {("sensor.unavailable", "unavailable")}, + "translation_placeholders": None, } ] ], @@ -188,8 +188,8 @@ async def test_validation_device_consumption_entity_non_numeric( [ { "type": "entity_state_non_numeric", - "identifier": "sensor.non_numeric", - "value": "123,123.10", + "affected_entities": {("sensor.non_numeric", "123,123.10")}, + "translation_placeholders": None, }, ] ], @@ -219,8 +219,10 @@ async def test_validation_device_consumption_entity_unexpected_unit( [ { "type": "entity_unexpected_unit_energy", - "identifier": "sensor.unexpected_unit", - "value": "beers", + "affected_entities": {("sensor.unexpected_unit", "beers")}, + "translation_placeholders": { + "energy_units": "GJ, kWh, MJ, MWh, Wh" + }, } ] ], @@ -242,8 +244,8 @@ async def test_validation_device_consumption_recorder_not_tracked( [ { "type": "recorder_untracked", - "identifier": "sensor.not_recorded", - "value": None, + "affected_entities": {("sensor.not_recorded", None)}, + "translation_placeholders": None, } ] ], @@ -273,8 +275,8 @@ async def test_validation_device_consumption_no_last_reset( [ { "type": "entity_state_class_measurement_no_last_reset", - "identifier": "sensor.no_last_reset", - "value": None, + "affected_entities": {("sensor.no_last_reset", None)}, + "translation_placeholders": None, } ] ], @@ -305,8 +307,10 @@ async def test_validation_solar(hass, mock_energy_manager, mock_get_metadata): [ { "type": "entity_unexpected_unit_energy", - "identifier": "sensor.solar_production", - "value": "beers", + "affected_entities": {("sensor.solar_production", "beers")}, + "translation_placeholders": { + "energy_units": "GJ, kWh, MJ, MWh, Wh" + }, } ] ], @@ -351,13 +355,13 @@ async def test_validation_battery(hass, mock_energy_manager, mock_get_metadata): [ { "type": "entity_unexpected_unit_energy", - "identifier": "sensor.battery_import", - "value": "beers", - }, - { - "type": "entity_unexpected_unit_energy", - "identifier": "sensor.battery_export", - "value": "beers", + "affected_entities": { + ("sensor.battery_import", "beers"), + ("sensor.battery_export", "beers"), + }, + "translation_placeholders": { + "energy_units": "GJ, kWh, MJ, MWh, Wh" + }, }, ] ], @@ -422,43 +426,37 @@ async def test_validation_grid( [ { "type": "entity_unexpected_unit_energy", - "identifier": "sensor.grid_consumption_1", - "value": "beers", + "affected_entities": { + ("sensor.grid_consumption_1", "beers"), + ("sensor.grid_production_1", "beers"), + }, + "translation_placeholders": { + "energy_units": "GJ, kWh, MJ, MWh, Wh" + }, }, { "type": "statistics_not_defined", - "identifier": "sensor.grid_cost_1", - "value": None, + "affected_entities": { + ("sensor.grid_cost_1", None), + ("sensor.grid_compensation_1", None), + }, + "translation_placeholders": None, }, { "type": "recorder_untracked", - "identifier": "sensor.grid_cost_1", - "value": None, + "affected_entities": { + ("sensor.grid_cost_1", None), + ("sensor.grid_compensation_1", None), + }, + "translation_placeholders": None, }, { "type": "entity_not_defined", - "identifier": "sensor.grid_cost_1", - "value": None, - }, - { - "type": "entity_unexpected_unit_energy", - "identifier": "sensor.grid_production_1", - "value": "beers", - }, - { - "type": "statistics_not_defined", - "identifier": "sensor.grid_compensation_1", - "value": None, - }, - { - "type": "recorder_untracked", - "identifier": "sensor.grid_compensation_1", - "value": None, - }, - { - "type": "entity_not_defined", - "identifier": "sensor.grid_compensation_1", - "value": None, + "affected_entities": { + ("sensor.grid_cost_1", None), + ("sensor.grid_compensation_1", None), + }, + "translation_placeholders": None, }, ] ], @@ -517,23 +515,21 @@ async def test_validation_grid_external_cost_compensation( [ { "type": "entity_unexpected_unit_energy", - "identifier": "sensor.grid_consumption_1", - "value": "beers", + "affected_entities": { + ("sensor.grid_consumption_1", "beers"), + ("sensor.grid_production_1", "beers"), + }, + "translation_placeholders": { + "energy_units": "GJ, kWh, MJ, MWh, Wh" + }, }, { "type": "statistics_not_defined", - "identifier": "external:grid_cost_1", - "value": None, - }, - { - "type": "entity_unexpected_unit_energy", - "identifier": "sensor.grid_production_1", - "value": "beers", - }, - { - "type": "statistics_not_defined", - "identifier": "external:grid_compensation_1", - "value": None, + "affected_entities": { + ("external:grid_cost_1", None), + ("external:grid_compensation_1", None), + }, + "translation_placeholders": None, }, ] ], @@ -599,18 +595,16 @@ async def test_validation_grid_price_not_exist( [ { "type": "entity_not_defined", - "identifier": "sensor.grid_price_1", - "value": None, + "affected_entities": {("sensor.grid_price_1", None)}, + "translation_placeholders": None, }, { "type": "recorder_untracked", - "identifier": "sensor.grid_consumption_1_cost", - "value": None, - }, - { - "type": "recorder_untracked", - "identifier": "sensor.grid_production_1_compensation", - "value": None, + "affected_entities": { + ("sensor.grid_consumption_1_cost", None), + ("sensor.grid_production_1_compensation", None), + }, + "translation_placeholders": None, }, ] ], @@ -683,8 +677,8 @@ async def test_validation_grid_auto_cost_entity_errors( "$/kWh", { "type": "entity_state_non_numeric", - "identifier": "sensor.grid_price_1", - "value": "123,123.12", + "affected_entities": {("sensor.grid_price_1", "123,123.12")}, + "translation_placeholders": None, }, ), ( @@ -692,8 +686,10 @@ async def test_validation_grid_auto_cost_entity_errors( "$/Ws", { "type": "entity_unexpected_unit_energy_price", - "identifier": "sensor.grid_price_1", - "value": "$/Ws", + "affected_entities": {("sensor.grid_price_1", "$/Ws")}, + "translation_placeholders": { + "price_units": "EUR/GJ, EUR/kWh, EUR/MJ, EUR/MWh, EUR/Wh" + }, }, ), ), @@ -834,18 +830,21 @@ async def test_validation_gas( [ { "type": "entity_unexpected_unit_gas", - "identifier": "sensor.gas_consumption_1", - "value": "beers", + "affected_entities": {("sensor.gas_consumption_1", "beers")}, + "translation_placeholders": { + "energy_units": "GJ, kWh, MJ, MWh, Wh", + "gas_units": "CCF, ft³, m³", + }, }, { "type": "recorder_untracked", - "identifier": "sensor.gas_cost_1", - "value": None, + "affected_entities": {("sensor.gas_cost_1", None)}, + "translation_placeholders": None, }, { "type": "entity_not_defined", - "identifier": "sensor.gas_cost_1", - "value": None, + "affected_entities": {("sensor.gas_cost_1", None)}, + "translation_placeholders": None, }, ], [], @@ -853,15 +852,19 @@ async def test_validation_gas( [ { "type": "entity_unexpected_device_class", - "identifier": "sensor.gas_consumption_4", - "value": None, + "affected_entities": {("sensor.gas_consumption_4", None)}, + "translation_placeholders": None, }, ], [ { "type": "entity_unexpected_unit_gas_price", - "identifier": "sensor.gas_price_2", - "value": "EUR/invalid", + "affected_entities": {("sensor.gas_price_2", "EUR/invalid")}, + "translation_placeholders": { + "price_units": ( + "EUR/GJ, EUR/kWh, EUR/MJ, EUR/MWh, EUR/Wh, EUR/CCF, EUR/ft³, EUR/m³" + ) + }, }, ], ], @@ -1039,18 +1042,18 @@ async def test_validation_water( [ { "type": "entity_unexpected_unit_water", - "identifier": "sensor.water_consumption_1", - "value": "beers", + "affected_entities": {("sensor.water_consumption_1", "beers")}, + "translation_placeholders": {"water_units": "CCF, ft³, m³, gal, L"}, }, { "type": "recorder_untracked", - "identifier": "sensor.water_cost_1", - "value": None, + "affected_entities": {("sensor.water_cost_1", None)}, + "translation_placeholders": None, }, { "type": "entity_not_defined", - "identifier": "sensor.water_cost_1", - "value": None, + "affected_entities": {("sensor.water_cost_1", None)}, + "translation_placeholders": None, }, ], [], @@ -1058,15 +1061,17 @@ async def test_validation_water( [ { "type": "entity_unexpected_device_class", - "identifier": "sensor.water_consumption_4", - "value": None, + "affected_entities": {("sensor.water_consumption_4", None)}, + "translation_placeholders": None, }, ], [ { "type": "entity_unexpected_unit_water_price", - "identifier": "sensor.water_price_2", - "value": "EUR/invalid", + "affected_entities": {("sensor.water_price_2", "EUR/invalid")}, + "translation_placeholders": { + "price_units": "EUR/CCF, EUR/ft³, EUR/m³, EUR/gal, EUR/L" + }, }, ], ], diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index 536077d6b15..354f1eef077 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -163,7 +163,9 @@ async def test_save_preferences( assert msg["result"] == { "cost_sensors": { "sensor.heat_pump_meter_2": "sensor.heat_pump_meter_2_cost", - "sensor.return_to_grid_offpeak": "sensor.return_to_grid_offpeak_compensation", + "sensor.return_to_grid_offpeak": ( + "sensor.return_to_grid_offpeak_compensation" + ), }, "solar_forecast_domains": ["some_domain"], } diff --git a/tests/components/energyzero/__init__.py b/tests/components/energyzero/__init__.py new file mode 100644 index 00000000000..287bdf6a2f4 --- /dev/null +++ b/tests/components/energyzero/__init__.py @@ -0,0 +1 @@ +"""Tests for the EnergyZero integration.""" diff --git a/tests/components/energyzero/conftest.py b/tests/components/energyzero/conftest.py new file mode 100644 index 00000000000..42b05eff444 --- /dev/null +++ b/tests/components/energyzero/conftest.py @@ -0,0 +1,61 @@ +"""Fixtures for EnergyZero integration tests.""" +from collections.abc import Generator +import json +from unittest.mock import AsyncMock, MagicMock, patch + +from energyzero import Electricity, Gas +import pytest + +from homeassistant.components.energyzero.const import DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.energyzero.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="energy", + domain=DOMAIN, + data={}, + unique_id="unique_thingy", + ) + + +@pytest.fixture +def mock_energyzero() -> Generator[MagicMock, None, None]: + """Return a mocked EnergyZero client.""" + with patch( + "homeassistant.components.energyzero.coordinator.EnergyZero", autospec=True + ) as energyzero_mock: + client = energyzero_mock.return_value + client.energy_prices.return_value = Electricity.from_dict( + json.loads(load_fixture("today_energy.json", DOMAIN)) + ) + client.gas_prices.return_value = Gas.from_dict( + json.loads(load_fixture("today_gas.json", DOMAIN)) + ) + yield client + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_energyzero: MagicMock +) -> MockConfigEntry: + """Set up the EnergyZero 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/energyzero/fixtures/today_energy.json b/tests/components/energyzero/fixtures/today_energy.json new file mode 100644 index 00000000000..a2139bef0bd --- /dev/null +++ b/tests/components/energyzero/fixtures/today_energy.json @@ -0,0 +1,104 @@ +{ + "Prices": [ + { + "price": 0.35, + "readingDate": "2022-12-06T23:00:00Z" + }, + { + "price": 0.32, + "readingDate": "2022-12-07T00:00:00Z" + }, + { + "price": 0.28, + "readingDate": "2022-12-07T01:00:00Z" + }, + { + "price": 0.26, + "readingDate": "2022-12-07T02:00:00Z" + }, + { + "price": 0.27, + "readingDate": "2022-12-07T03:00:00Z" + }, + { + "price": 0.28, + "readingDate": "2022-12-07T04:00:00Z" + }, + { + "price": 0.28, + "readingDate": "2022-12-07T05:00:00Z" + }, + { + "price": 0.38, + "readingDate": "2022-12-07T06:00:00Z" + }, + { + "price": 0.41, + "readingDate": "2022-12-07T07:00:00Z" + }, + { + "price": 0.46, + "readingDate": "2022-12-07T08:00:00Z" + }, + { + "price": 0.44, + "readingDate": "2022-12-07T09:00:00Z" + }, + { + "price": 0.39, + "readingDate": "2022-12-07T10:00:00Z" + }, + { + "price": 0.33, + "readingDate": "2022-12-07T11:00:00Z" + }, + { + "price": 0.37, + "readingDate": "2022-12-07T12:00:00Z" + }, + { + "price": 0.44, + "readingDate": "2022-12-07T13:00:00Z" + }, + { + "price": 0.48, + "readingDate": "2022-12-07T14:00:00Z" + }, + { + "price": 0.49, + "readingDate": "2022-12-07T15:00:00Z" + }, + { + "price": 0.55, + "readingDate": "2022-12-07T16:00:00Z" + }, + { + "price": 0.37, + "readingDate": "2022-12-07T17:00:00Z" + }, + { + "price": 0.4, + "readingDate": "2022-12-07T18:00:00Z" + }, + { + "price": 0.4, + "readingDate": "2022-12-07T19:00:00Z" + }, + { + "price": 0.32, + "readingDate": "2022-12-07T20:00:00Z" + }, + { + "price": 0.33, + "readingDate": "2022-12-07T21:00:00Z" + }, + { + "price": 0.31, + "readingDate": "2022-12-07T22:00:00Z" + } + ], + "intervalType": 4, + "average": 0.37, + "fromDate": "2022-12-06T23:00:00Z", + "tillDate": "2022-12-07T22:59:59.999Z" +} diff --git a/tests/components/energyzero/fixtures/today_gas.json b/tests/components/energyzero/fixtures/today_gas.json new file mode 100644 index 00000000000..20bd40220aa --- /dev/null +++ b/tests/components/energyzero/fixtures/today_gas.json @@ -0,0 +1,200 @@ +{ + "Prices": [ + { + "price": 1.43, + "readingDate": "2022-12-05T23:00:00Z" + }, + { + "price": 1.43, + "readingDate": "2022-12-06T00:00:00Z" + }, + { + "price": 1.43, + "readingDate": "2022-12-06T01:00:00Z" + }, + { + "price": 1.43, + "readingDate": "2022-12-06T02:00:00Z" + }, + { + "price": 1.43, + "readingDate": "2022-12-06T03:00:00Z" + }, + { + "price": 1.43, + "readingDate": "2022-12-06T04:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T05:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T06:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T07:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T08:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T09:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T10:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T11:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T12:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T13:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T14:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T15:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T16:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T17:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T18:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T19:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T20:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T21:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T22:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-06T23:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-07T00:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-07T01:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-07T02:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-07T03:00:00Z" + }, + { + "price": 1.45, + "readingDate": "2022-12-07T04:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T05:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T06:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T07:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T08:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T09:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T10:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T11:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T12:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T13:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T14:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T15:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T16:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T17:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T18:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T19:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T20:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T21:00:00Z" + }, + { + "price": 1.47, + "readingDate": "2022-12-07T22:00:00Z" + } + ], + "intervalType": 4, + "average": 1.46, + "fromDate": "2022-12-06T23:00:00Z", + "tillDate": "2022-12-07T22:59:59.999Z" +} diff --git a/tests/components/energyzero/test_config_flow.py b/tests/components/energyzero/test_config_flow.py new file mode 100644 index 00000000000..b75b0c00dab --- /dev/null +++ b/tests/components/energyzero/test_config_flow.py @@ -0,0 +1,32 @@ +"""Test the EnergyZero config flow.""" +from unittest.mock import MagicMock + +from homeassistant.components.energyzero.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def test_full_user_flow( + hass: HomeAssistant, + mock_setup_entry: MagicMock, +) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == FlowResultType.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={}, + ) + + assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("title") == "EnergyZero" + assert result2.get("data") == {} + + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/energyzero/test_diagnostics.py b/tests/components/energyzero/test_diagnostics.py new file mode 100644 index 00000000000..e58ca9bc0bf --- /dev/null +++ b/tests/components/energyzero/test_diagnostics.py @@ -0,0 +1,86 @@ +"""Tests for the diagnostics data provided by the EnergyZero integration.""" +from unittest.mock import MagicMock + +from aiohttp import ClientSession +from energyzero import EnergyZeroNoDataError +import pytest + +from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +@pytest.mark.freeze_time("2022-12-07 15:00:00") +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSession, + init_integration: MockConfigEntry, +) -> None: + """Test diagnostics.""" + assert await get_diagnostics_for_config_entry( + hass, hass_client, init_integration + ) == { + "entry": { + "title": "energy", + }, + "energy": { + "current_hour_price": 0.49, + "next_hour_price": 0.55, + "average_price": 0.37, + "max_price": 0.55, + "min_price": 0.26, + "highest_price_time": "2022-12-07T16:00:00+00:00", + "lowest_price_time": "2022-12-07T02:00:00+00:00", + "percentage_of_max": 89.09, + }, + "gas": { + "current_hour_price": 1.47, + "next_hour_price": 1.47, + }, + } + + +@pytest.mark.freeze_time("2022-12-07 15:00:00") +async def test_diagnostics_no_gas_today( + hass: HomeAssistant, + hass_client: ClientSession, + mock_energyzero: MagicMock, + init_integration: MockConfigEntry, +) -> None: + """Test diagnostics, no gas sensors available.""" + await async_setup_component(hass, "homeassistant", {}) + mock_energyzero.gas_prices.side_effect = EnergyZeroNoDataError + + await hass.services.async_call( + "homeassistant", + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: ["sensor.energyzero_today_gas_current_hour_price"]}, + blocking=True, + ) + await hass.async_block_till_done() + + assert await get_diagnostics_for_config_entry( + hass, hass_client, init_integration + ) == { + "entry": { + "title": "energy", + }, + "energy": { + "current_hour_price": 0.49, + "next_hour_price": 0.55, + "average_price": 0.37, + "max_price": 0.55, + "min_price": 0.26, + "highest_price_time": "2022-12-07T16:00:00+00:00", + "lowest_price_time": "2022-12-07T02:00:00+00:00", + "percentage_of_max": 89.09, + }, + "gas": { + "current_hour_price": None, + "next_hour_price": None, + }, + } diff --git a/tests/components/energyzero/test_init.py b/tests/components/energyzero/test_init.py new file mode 100644 index 00000000000..489e4346e25 --- /dev/null +++ b/tests/components/energyzero/test_init.py @@ -0,0 +1,45 @@ +"""Tests for the EnergyZero integration.""" +from unittest.mock import MagicMock, patch + +from energyzero import EnergyZeroConnectionError + +from homeassistant.components.energyzero.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_energyzero: MagicMock +) -> None: + """Test the EnergyZero 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 + + +@patch( + "homeassistant.components.energyzero.coordinator.EnergyZero._request", + side_effect=EnergyZeroConnectionError, +) +async def test_config_flow_entry_not_ready( + mock_request: MagicMock, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the EnergyZero 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_request.call_count == 1 + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/energyzero/test_sensor.py b/tests/components/energyzero/test_sensor.py new file mode 100644 index 00000000000..e1773e5349c --- /dev/null +++ b/tests/components/energyzero/test_sensor.py @@ -0,0 +1,180 @@ +"""Tests for the sensors provided by the EnergyZero integration.""" + +from unittest.mock import MagicMock + +from energyzero import EnergyZeroNoDataError +import pytest + +from homeassistant.components.energyzero.const import DOMAIN +from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + CURRENCY_EURO, + STATE_UNKNOWN, + UnitOfEnergy, + UnitOfVolume, +) +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 tests.common import MockConfigEntry + + +@pytest.mark.freeze_time("2022-12-07 15:00:00") +async def test_energy_today( + hass: HomeAssistant, init_integration: MockConfigEntry +) -> None: + """Test the EnergyZero - Energy sensors.""" + entry_id = init_integration.entry_id + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + # Current energy price sensor + state = hass.states.get("sensor.energyzero_today_energy_current_hour_price") + entry = entity_registry.async_get( + "sensor.energyzero_today_energy_current_hour_price" + ) + assert entry + assert state + assert entry.unique_id == f"{entry_id}_today_energy_current_hour_price" + assert state.state == "0.49" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy market price Current hour" + ) + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert ATTR_DEVICE_CLASS not in state.attributes + assert ATTR_ICON not in state.attributes + + # Average price sensor + state = hass.states.get("sensor.energyzero_today_energy_average_price") + entry = entity_registry.async_get("sensor.energyzero_today_energy_average_price") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_today_energy_average_price" + assert state.state == "0.37" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Energy market price Average - today" + ) + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}" + ) + assert ATTR_DEVICE_CLASS not in state.attributes + assert ATTR_ICON not in state.attributes + + # Highest price sensor + state = hass.states.get("sensor.energyzero_today_energy_max_price") + entry = entity_registry.async_get("sensor.energyzero_today_energy_max_price") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_today_energy_max_price" + assert state.state == "0.55" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Energy market price Highest price - today" + ) + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}" + ) + assert ATTR_DEVICE_CLASS not in state.attributes + assert ATTR_ICON not in state.attributes + + # Highest price time sensor + state = hass.states.get("sensor.energyzero_today_energy_highest_price_time") + entry = entity_registry.async_get( + "sensor.energyzero_today_energy_highest_price_time" + ) + assert entry + assert state + assert entry.unique_id == f"{entry_id}_today_energy_highest_price_time" + assert state.state == "2022-12-07T16:00:00+00:00" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Energy market price Time of highest price - today" + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP + 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, f"{entry_id}_today_energy")} + assert device_entry.manufacturer == "EnergyZero" + assert device_entry.name == "Energy market price" + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE + assert not device_entry.model + assert not device_entry.sw_version + + +@pytest.mark.freeze_time("2022-12-07 15:00:00") +async def test_gas_today( + hass: HomeAssistant, init_integration: MockConfigEntry +) -> None: + """Test the EnergyZero - Gas sensors.""" + entry_id = init_integration.entry_id + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + # Current gas price sensor + state = hass.states.get("sensor.energyzero_today_gas_current_hour_price") + entry = entity_registry.async_get("sensor.energyzero_today_gas_current_hour_price") + assert entry + assert state + assert entry.unique_id == f"{entry_id}_today_gas_current_hour_price" + assert state.state == "1.47" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Gas market price Current hour" + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == f"{CURRENCY_EURO}/{UnitOfVolume.CUBIC_METERS}" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert ATTR_DEVICE_CLASS not in state.attributes + 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, f"{entry_id}_today_gas")} + assert device_entry.manufacturer == "EnergyZero" + assert device_entry.name == "Gas market price" + assert device_entry.entry_type is dr.DeviceEntryType.SERVICE + assert not device_entry.model + assert not device_entry.sw_version + + +@pytest.mark.freeze_time("2022-12-07 15:00:00") +async def test_no_gas_today( + hass: HomeAssistant, mock_energyzero: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test the EnergyZero - No gas sensors available.""" + await async_setup_component(hass, "homeassistant", {}) + + mock_energyzero.gas_prices.side_effect = EnergyZeroNoDataError + + await hass.services.async_call( + "homeassistant", + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: ["sensor.energyzero_today_gas_current_hour_price"]}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energyzero_today_gas_current_hour_price") + assert state + assert state.state == STATE_UNKNOWN diff --git a/tests/components/esphome/__init__.py b/tests/components/esphome/__init__.py index a3e4985a2d8..a44db03f841 100644 --- a/tests/components/esphome/__init__.py +++ b/tests/components/esphome/__init__.py @@ -1 +1,6 @@ """Tests for esphome.""" + +DASHBOARD_SLUG = "mock-slug" +DASHBOARD_HOST = "mock-host" +DASHBOARD_PORT = 1234 +VALID_NOISE_PSK = "bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU=" diff --git a/tests/components/esphome/conftest.py b/tests/components/esphome/conftest.py index 3382e978a19..f53e513e6bb 100644 --- a/tests/components/esphome/conftest.py +++ b/tests/components/esphome/conftest.py @@ -3,14 +3,21 @@ from __future__ import annotations from unittest.mock import AsyncMock, Mock, patch -from aioesphomeapi import APIClient +from aioesphomeapi import APIClient, DeviceInfo import pytest from zeroconf import Zeroconf -from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN +from homeassistant.components.esphome import ( + CONF_DEVICE_NAME, + CONF_NOISE_PSK, + DOMAIN, + dashboard, +) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant +from . import DASHBOARD_HOST, DASHBOARD_PORT, DASHBOARD_SLUG + from tests.common import MockConfigEntry @@ -25,9 +32,9 @@ def esphome_mock_async_zeroconf(mock_async_zeroconf): @pytest.fixture -def mock_config_entry() -> MockConfigEntry: +def mock_config_entry(hass) -> MockConfigEntry: """Return the default mocked config entry.""" - return MockConfigEntry( + config_entry = MockConfigEntry( title="ESPHome Device", domain=DOMAIN, data={ @@ -35,9 +42,24 @@ def mock_config_entry() -> MockConfigEntry: CONF_PORT: 6053, CONF_PASSWORD: "pwd", CONF_NOISE_PSK: "12345678123456781234567812345678", + CONF_DEVICE_NAME: "test", }, unique_id="11:22:33:44:55:aa", ) + config_entry.add_to_hass(hass) + return config_entry + + +@pytest.fixture +def mock_device_info() -> DeviceInfo: + """Return the default mocked device info.""" + return DeviceInfo( + uses_password=False, + name="test", + bluetooth_proxy_version=0, + mac_address="11:22:33:44:55:aa", + esphome_version="1.0.0", + ) @pytest.fixture @@ -45,8 +67,6 @@ async def init_integration( hass: HomeAssistant, mock_config_entry: MockConfigEntry ) -> MockConfigEntry: """Set up the ESPHome 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() @@ -54,7 +74,7 @@ async def init_integration( @pytest.fixture -def mock_client(): +def mock_client(mock_device_info): """Mock APIClient.""" mock_client = Mock(spec=APIClient) @@ -78,6 +98,7 @@ def mock_client(): return mock_client mock_client.side_effect = mock_constructor + mock_client.device_info = AsyncMock(return_value=mock_device_info) mock_client.connect = AsyncMock() mock_client.disconnect = AsyncMock() @@ -85,3 +106,17 @@ def mock_client(): "homeassistant.components.esphome.config_flow.APIClient", mock_client ): yield mock_client + + +@pytest.fixture +async def mock_dashboard(hass): + """Mock dashboard.""" + data = {"configured": [], "importable": []} + with patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.get_devices", + return_value=data, + ): + await dashboard.async_set_dashboard_info( + hass, DASHBOARD_SLUG, DASHBOARD_HOST, DASHBOARD_PORT + ) + yield data diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 4a40f4ec4d7..b629e604410 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -11,15 +11,23 @@ from aioesphomeapi import ( ) import pytest -from homeassistant import config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components import dhcp, zeroconf -from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN, DomainData +from homeassistant.components.esphome import ( + CONF_DEVICE_NAME, + CONF_NOISE_PSK, + DOMAIN, + DomainData, + dashboard, +) +from homeassistant.components.hassio import HassioServiceInfo from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.data_entry_flow import FlowResultType +from . import VALID_NOISE_PSK + from tests.common import MockConfigEntry -VALID_NOISE_PSK = "bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU=" INVALID_NOISE_PSK = "lSYBYEjQI1bVL8s2Vask4YytGMj1f1epNtmoim2yuTM=" @@ -41,12 +49,6 @@ async def test_user_connection_works(hass, mock_client, mock_zeroconf): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - mock_client.device_info = AsyncMock( - return_value=DeviceInfo( - uses_password=False, name="test", mac_address="mock-mac" - ) - ) - result = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_USER}, @@ -59,9 +61,10 @@ async def test_user_connection_works(hass, mock_client, mock_zeroconf): CONF_PORT: 80, CONF_PASSWORD: "", CONF_NOISE_PSK: "", + CONF_DEVICE_NAME: "test", } assert result["title"] == "test" - assert result["result"].unique_id == "mock-mac" + assert result["result"].unique_id == "11:22:33:44:55:aa" assert len(mock_client.connect.mock_calls) == 1 assert len(mock_client.device_info.mock_calls) == 1 @@ -77,7 +80,7 @@ async def test_user_connection_updates_host(hass, mock_client, mock_zeroconf): entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "test.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, - unique_id="mock-mac", + unique_id="11:22:33:44:55:aa", ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -89,12 +92,6 @@ async def test_user_connection_updates_host(hass, mock_client, mock_zeroconf): assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - mock_client.device_info = AsyncMock( - return_value=DeviceInfo( - uses_password=False, name="test", mac_address="mock-mac" - ) - ) - result = await hass.config_entries.flow.async_init( "esphome", context={"source": config_entries.SOURCE_USER}, @@ -149,9 +146,7 @@ async def test_user_connection_error(hass, mock_client, mock_zeroconf): async def test_user_with_password(hass, mock_client, mock_zeroconf): """Test user step with password.""" - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(uses_password=True, name="test") - ) + mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test") result = await hass.config_entries.flow.async_init( "esphome", @@ -172,15 +167,14 @@ async def test_user_with_password(hass, mock_client, mock_zeroconf): CONF_PORT: 6053, CONF_PASSWORD: "password1", CONF_NOISE_PSK: "", + CONF_DEVICE_NAME: "test", } assert mock_client.password == "password1" async def test_user_invalid_password(hass, mock_client, mock_zeroconf): """Test user step with invalid password.""" - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(uses_password=True, name="test") - ) + mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test") result = await hass.config_entries.flow.async_init( "esphome", @@ -204,9 +198,7 @@ async def test_user_invalid_password(hass, mock_client, mock_zeroconf): async def test_login_connection_error(hass, mock_client, mock_zeroconf): """Test user step with connection error on login attempt.""" - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(uses_password=True, name="test") - ) + mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test") result = await hass.config_entries.flow.async_init( "esphome", @@ -230,16 +222,10 @@ async def test_login_connection_error(hass, mock_client, mock_zeroconf): async def test_discovery_initiation(hass, mock_client, mock_zeroconf): """Test discovery importing works.""" - mock_client.device_info = AsyncMock( - return_value=DeviceInfo( - uses_password=False, name="test8266", mac_address="11:22:33:44:55:aa" - ) - ) - service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", addresses=["192.168.43.183"], - hostname="test8266.local.", + hostname="test.local.", name="mock_name", port=6053, properties={ @@ -256,7 +242,7 @@ async def test_discovery_initiation(hass, mock_client, mock_zeroconf): ) assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "test8266" + assert result["title"] == "test" assert result["data"][CONF_HOST] == "192.168.43.183" assert result["data"][CONF_PORT] == 6053 @@ -314,17 +300,13 @@ async def test_discovery_duplicate_data(hass, mock_client): service_info = zeroconf.ZeroconfServiceInfo( host="192.168.43.183", addresses=["192.168.43.183"], - hostname="test8266.local.", + hostname="test.local.", name="mock_name", port=6053, - properties={"address": "test8266.local", "mac": "1122334455aa"}, + properties={"address": "test.local", "mac": "1122334455aa"}, type="mock_type", ) - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(uses_password=False, name="test8266") - ) - result = await hass.config_entries.flow.async_init( "esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF} ) @@ -413,6 +395,7 @@ async def test_encryption_key_valid_psk(hass, mock_client, mock_zeroconf): CONF_PORT: 6053, CONF_PASSWORD: "", CONF_NOISE_PSK: VALID_NOISE_PSK, + CONF_DEVICE_NAME: "test", } assert mock_client.noise_psk == VALID_NOISE_PSK @@ -479,9 +462,7 @@ async def test_reauth_confirm_valid(hass, mock_client, mock_zeroconf): }, ) - mock_client.device_info = AsyncMock( - return_value=DeviceInfo(uses_password=False, name="test") - ) + mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_NOISE_PSK: VALID_NOISE_PSK} ) @@ -491,6 +472,163 @@ async def test_reauth_confirm_valid(hass, mock_client, mock_zeroconf): assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK +async def test_reauth_fixed_via_dashboard( + hass, mock_client, mock_zeroconf, mock_dashboard +): + """Test reauth fixed automatically via dashboard.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test", + }, + ) + entry.add_to_hass(hass) + + mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") + + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + } + ) + + await dashboard.async_get_dashboard(hass).async_refresh() + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ) as mock_get_encryption_key: + result = await hass.config_entries.flow.async_init( + "esphome", + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + ) + + assert result["type"] == FlowResultType.ABORT, result + assert result["reason"] == "reauth_successful" + assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK + + assert len(mock_get_encryption_key.mock_calls) == 1 + + +async def test_reauth_fixed_via_dashboard_add_encryption_remove_password( + hass, mock_client, mock_zeroconf, mock_dashboard, mock_config_entry +): + """Test reauth fixed automatically via dashboard with password removed.""" + mock_client.device_info.side_effect = ( + InvalidAuthAPIError, + DeviceInfo(uses_password=False, name="test"), + ) + + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + } + ) + + await dashboard.async_get_dashboard(hass).async_refresh() + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ) as mock_get_encryption_key: + result = await hass.config_entries.flow.async_init( + "esphome", + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + "unique_id": mock_config_entry.unique_id, + }, + ) + + assert result["type"] == FlowResultType.ABORT, result + assert result["reason"] == "reauth_successful" + assert mock_config_entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK + assert mock_config_entry.data[CONF_PASSWORD] == "" + + assert len(mock_get_encryption_key.mock_calls) == 1 + + +async def test_reauth_fixed_via_remove_password(hass, mock_client, mock_config_entry): + """Test reauth fixed automatically by seeing password removed.""" + mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") + + result = await hass.config_entries.flow.async_init( + "esphome", + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + "unique_id": mock_config_entry.unique_id, + }, + ) + + assert result["type"] == FlowResultType.ABORT, result + assert result["reason"] == "reauth_successful" + assert mock_config_entry.data[CONF_PASSWORD] == "" + + +async def test_reauth_fixed_via_dashboard_at_confirm( + hass, mock_client, mock_zeroconf, mock_dashboard +): + """Test reauth fixed automatically via dashboard at confirm step.""" + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "127.0.0.1", + CONF_PORT: 6053, + CONF_PASSWORD: "", + CONF_DEVICE_NAME: "test", + }, + ) + entry.add_to_hass(hass) + + mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") + + result = await hass.config_entries.flow.async_init( + "esphome", + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + ) + + assert result["type"] == FlowResultType.FORM, result + assert result["step_id"] == "reauth_confirm" + + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + } + ) + + await dashboard.async_get_dashboard(hass).async_refresh() + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ) as mock_get_encryption_key: + # We just fetch the form + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == FlowResultType.ABORT, result + assert result["reason"] == "reauth_successful" + assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK + + assert len(mock_get_encryption_key.mock_calls) == 1 + + async def test_reauth_confirm_invalid(hass, mock_client, mock_zeroconf): """Test reauth initiation with invalid PSK.""" entry = MockConfigEntry( @@ -620,3 +758,122 @@ async def test_discovery_dhcp_no_changes(hass, mock_client): assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "192.168.43.183" + + +async def test_discovery_hassio(hass): + """Test dashboard discovery.""" + result = await hass.config_entries.flow.async_init( + "esphome", + data=HassioServiceInfo( + config={ + "host": "mock-esphome", + "port": 6052, + }, + name="ESPHome", + slug="mock-slug", + ), + context={"source": config_entries.SOURCE_HASSIO}, + ) + assert result + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "service_received" + + dash = dashboard.async_get_dashboard(hass) + assert dash is not None + assert dash.addon_slug == "mock-slug" + + +async def test_zeroconf_encryption_key_via_dashboard( + hass, mock_client, mock_zeroconf, mock_dashboard +): + """Test encryption key retrieved from dashboard.""" + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.43.183", + addresses=["192.168.43.183"], + hostname="test8266.local.", + name="mock_name", + port=6053, + properties={ + "mac": "1122334455aa", + }, + type="mock_type", + ) + flow = await hass.config_entries.flow.async_init( + "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + ) + + assert flow["type"] == FlowResultType.FORM + assert flow["step_id"] == "discovery_confirm" + + mock_dashboard["configured"].append( + { + "name": "test8266", + "configuration": "test8266.yaml", + } + ) + + await dashboard.async_get_dashboard(hass).async_refresh() + + mock_client.device_info.side_effect = [ + RequiresEncryptionAPIError, + DeviceInfo( + uses_password=False, + name="test8266", + mac_address="11:22:33:44:55:aa", + ), + ] + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ) as mock_get_encryption_key: + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], user_input={} + ) + + assert len(mock_get_encryption_key.mock_calls) == 1 + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "test8266" + assert result["data"][CONF_HOST] == "192.168.43.183" + assert result["data"][CONF_PORT] == 6053 + assert result["data"][CONF_NOISE_PSK] == VALID_NOISE_PSK + + assert result["result"] + assert result["result"].unique_id == "11:22:33:44:55:aa" + + assert mock_client.noise_psk == VALID_NOISE_PSK + + +async def test_zeroconf_no_encryption_key_via_dashboard( + hass, mock_client, mock_zeroconf, mock_dashboard +): + """Test encryption key not retrieved from dashboard.""" + service_info = zeroconf.ZeroconfServiceInfo( + host="192.168.43.183", + addresses=["192.168.43.183"], + hostname="test8266.local.", + name="mock_name", + port=6053, + properties={ + "mac": "1122334455aa", + }, + type="mock_type", + ) + flow = await hass.config_entries.flow.async_init( + "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + ) + + assert flow["type"] == FlowResultType.FORM + assert flow["step_id"] == "discovery_confirm" + + await dashboard.async_get_dashboard(hass).async_refresh() + + mock_client.device_info.side_effect = RequiresEncryptionAPIError + + result = await hass.config_entries.flow.async_configure( + flow["flow_id"], user_input={} + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "encryption_key" diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py new file mode 100644 index 00000000000..ed2afb7e500 --- /dev/null +++ b/tests/components/esphome/test_dashboard.py @@ -0,0 +1,101 @@ +"""Test ESPHome dashboard features.""" +from unittest.mock import patch + +from aioesphomeapi import DeviceInfo, InvalidAuthAPIError + +from homeassistant.components.esphome import CONF_NOISE_PSK, dashboard +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.data_entry_flow import FlowResultType + +from . import VALID_NOISE_PSK + + +async def test_new_info_reload_config_entries(hass, init_integration, mock_dashboard): + """Test config entries are reloaded when new info is set.""" + assert init_integration.state == ConfigEntryState.LOADED + + with patch("homeassistant.components.esphome.async_setup_entry") as mock_setup: + await dashboard.async_set_dashboard_info(hass, "test-slug", "test-host", 6052) + + assert len(mock_setup.mock_calls) == 1 + assert mock_setup.mock_calls[0][1][1] == init_integration + + # Test it's a no-op when the same info is set + with patch("homeassistant.components.esphome.async_setup_entry") as mock_setup: + await dashboard.async_set_dashboard_info(hass, "test-slug", "test-host", 6052) + + assert len(mock_setup.mock_calls) == 0 + + +async def test_new_dashboard_fix_reauth( + hass, mock_client, mock_config_entry, mock_dashboard +): + """Test config entries waiting for reauth are triggered.""" + mock_client.device_info.side_effect = ( + InvalidAuthAPIError, + DeviceInfo(uses_password=False, name="test"), + ) + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ) as mock_get_encryption_key: + result = await hass.config_entries.flow.async_init( + "esphome", + context={ + "source": SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + "unique_id": mock_config_entry.unique_id, + }, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert len(mock_get_encryption_key.mock_calls) == 0 + + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + } + ) + + await dashboard.async_get_dashboard(hass).async_refresh() + + with patch( + "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", + return_value=VALID_NOISE_PSK, + ) as mock_get_encryption_key, patch( + "homeassistant.components.esphome.async_setup_entry", return_value=True + ) as mock_setup: + await dashboard.async_set_dashboard_info(hass, "test-slug", "test-host", 6052) + await hass.async_block_till_done() + + assert len(mock_get_encryption_key.mock_calls) == 1 + assert len(mock_setup.mock_calls) == 1 + assert mock_config_entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK + + +async def test_dashboard_supports_update(hass, mock_dashboard): + """Test dashboard supports update.""" + dash = dashboard.async_get_dashboard(hass) + + # No data + assert not dash.supports_update + + # supported version + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + "current_version": "2023.2.0-dev", + } + ) + await dash.async_refresh() + + assert dash.supports_update + + # unsupported version + mock_dashboard["configured"][0]["current_version"] = "2023.1.0" + await dash.async_refresh() + + assert not dash.supports_update diff --git a/tests/components/esphome/test_diagnostics.py b/tests/components/esphome/test_diagnostics.py index 9f55b83a47c..6a08a47e6eb 100644 --- a/tests/components/esphome/test_diagnostics.py +++ b/tests/components/esphome/test_diagnostics.py @@ -3,10 +3,12 @@ from aiohttp import ClientSession import pytest -from homeassistant.components.esphome import CONF_NOISE_PSK +from homeassistant.components.esphome import CONF_DEVICE_NAME, CONF_NOISE_PSK from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from homeassistant.core import HomeAssistant +from . import DASHBOARD_SLUG + from tests.common import MockConfigEntry from tests.components.diagnostics import get_diagnostics_for_config_entry @@ -16,15 +18,18 @@ async def test_diagnostics( hass_client: ClientSession, init_integration: MockConfigEntry, enable_bluetooth: pytest.fixture, + mock_dashboard, ): """Test diagnostics for config entry.""" result = await get_diagnostics_for_config_entry(hass, hass_client, init_integration) assert isinstance(result, dict) assert result["config"]["data"] == { + CONF_DEVICE_NAME: "test", CONF_HOST: "192.168.1.2", CONF_PORT: 6053, CONF_PASSWORD: "**REDACTED**", CONF_NOISE_PSK: "**REDACTED**", } assert result["config"]["unique_id"] == "11:22:33:44:55:aa" + assert result["dashboard"] == DASHBOARD_SLUG diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py new file mode 100644 index 00000000000..9cfba03be9f --- /dev/null +++ b/tests/components/esphome/test_update.py @@ -0,0 +1,147 @@ +"""Test ESPHome update entities.""" +import dataclasses +from unittest.mock import Mock, patch + +import pytest + +from homeassistant.components.esphome.dashboard import async_get_dashboard +from homeassistant.components.update import UpdateEntityFeature +from homeassistant.helpers.dispatcher import async_dispatcher_send + + +@pytest.fixture(autouse=True) +def stub_reconnect(): + """Stub reconnect.""" + with patch("homeassistant.components.esphome.ReconnectLogic.start"): + yield + + +@pytest.mark.parametrize( + "devices_payload,expected_state,expected_attributes", + [ + ( + [ + { + "name": "test", + "current_version": "2023.2.0-dev", + "configuration": "test.yaml", + } + ], + "on", + { + "latest_version": "2023.2.0-dev", + "installed_version": "1.0.0", + "supported_features": UpdateEntityFeature.INSTALL, + }, + ), + ( + [ + { + "name": "test", + "current_version": "1.0.0", + }, + ], + "off", + { + "latest_version": "1.0.0", + "installed_version": "1.0.0", + "supported_features": 0, + }, + ), + ( + [], + "unavailable", + {"supported_features": 0}, + ), + ], +) +async def test_update_entity( + hass, + mock_config_entry, + mock_device_info, + mock_dashboard, + devices_payload, + expected_state, + expected_attributes, +): + """Test ESPHome update entity.""" + mock_dashboard["configured"] = devices_payload + await async_get_dashboard(hass).async_refresh() + + with patch( + "homeassistant.components.esphome.update.DomainData.get_entry_data", + return_value=Mock(available=True, device_info=mock_device_info), + ): + assert await hass.config_entries.async_forward_entry_setup( + mock_config_entry, "update" + ) + + state = hass.states.get("update.none_firmware") + assert state is not None + assert state.state == expected_state + for key, expected_value in expected_attributes.items(): + assert state.attributes.get(key) == expected_value + + if expected_state != "on": + return + + with patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.compile", return_value=True + ) as mock_compile, patch( + "esphome_dashboard_api.ESPHomeDashboardAPI.upload", return_value=True + ) as mock_upload: + await hass.services.async_call( + "update", + "install", + {"entity_id": "update.none_firmware"}, + blocking=True, + ) + + assert len(mock_compile.mock_calls) == 1 + assert mock_compile.mock_calls[0][1][0] == "test.yaml" + + assert len(mock_upload.mock_calls) == 1 + assert mock_upload.mock_calls[0][1][0] == "test.yaml" + + +async def test_update_static_info( + hass, + mock_config_entry, + mock_device_info, + mock_dashboard, +): + """Test ESPHome update entity.""" + mock_dashboard["configured"] = [ + { + "name": "test", + "current_version": "1.2.3", + }, + ] + await async_get_dashboard(hass).async_refresh() + + signal_static_info_updated = f"esphome_{mock_config_entry.entry_id}_on_list" + runtime_data = Mock( + available=True, + device_info=mock_device_info, + signal_static_info_updated=signal_static_info_updated, + ) + + with patch( + "homeassistant.components.esphome.update.DomainData.get_entry_data", + return_value=runtime_data, + ): + assert await hass.config_entries.async_forward_entry_setup( + mock_config_entry, "update" + ) + + state = hass.states.get("update.none_firmware") + assert state is not None + assert state.state == "on" + + runtime_data.device_info = dataclasses.replace( + runtime_data.device_info, esphome_version="1.2.3" + ) + async_dispatcher_send(hass, signal_static_info_updated, []) + + state = hass.states.get("update.none_firmware") + assert state.state == "off" diff --git a/tests/components/eufylife_ble/__init__.py b/tests/components/eufylife_ble/__init__.py new file mode 100644 index 00000000000..7fbeed9d798 --- /dev/null +++ b/tests/components/eufylife_ble/__init__.py @@ -0,0 +1,24 @@ +"""Tests for the EufyLife integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_EUFYLIFE_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="11:22:33:44:55:66", + rssi=-60, + manufacturer_data={}, + service_data={}, + service_uuids=[], + source="local", +) + +T9146_SERVICE_INFO = BluetoothServiceInfo( + name="eufy T9146", + address="11:22:33:44:55:66", + rssi=-60, + manufacturer_data={}, + service_uuids=["0000fff0-0000-1000-8000-00805f9b34fb"], + service_data={}, + source="local", +) diff --git a/tests/components/eufylife_ble/conftest.py b/tests/components/eufylife_ble/conftest.py new file mode 100644 index 00000000000..18f5a0ec3a1 --- /dev/null +++ b/tests/components/eufylife_ble/conftest.py @@ -0,0 +1,8 @@ +"""EufyLife session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/eufylife_ble/test_config_flow.py b/tests/components/eufylife_ble/test_config_flow.py new file mode 100644 index 00000000000..c62883e8858 --- /dev/null +++ b/tests/components/eufylife_ble/test_config_flow.py @@ -0,0 +1,200 @@ +"""Test the EufyLife config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.eufylife_ble.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import NOT_EUFYLIFE_SERVICE_INFO, T9146_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=T9146_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch( + "homeassistant.components.eufylife_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Smart Scale C1" + assert result2["data"] == {"model": "eufy T9146"} + assert result2["result"].unique_id == "11:22:33:44:55:66" + + +async def test_async_step_bluetooth_not_eufylife(hass): + """Test discovery via bluetooth with an invalid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_EUFYLIFE_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.eufylife_ble.config_flow.async_discovered_service_info", + return_value=[T9146_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.eufylife_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "11:22:33:44:55:66"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Smart Scale C1" + assert result2["data"] == {"model": "eufy T9146"} + assert result2["result"].unique_id == "11:22:33:44:55:66" + + +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.eufylife_ble.config_flow.async_discovered_service_info", + return_value=[T9146_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="11:22:33:44:55:66", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.eufylife_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "11:22:33:44:55:66"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="11:22:33:44:55:66", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.eufylife_ble.config_flow.async_discovered_service_info", + return_value=[T9146_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="11:22:33:44:55:66", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=T9146_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=T9146_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=T9146_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=T9146_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.eufylife_ble.config_flow.async_discovered_service_info", + return_value=[T9146_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch( + "homeassistant.components.eufylife_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "11:22:33:44:55:66"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Smart Scale C1" + assert result2["data"] == {"model": "eufy T9146"} + assert result2["result"].unique_id == "11:22:33:44:55:66" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/fan/test_device_condition.py b/tests/components/fan/test_device_condition.py index 8a00fd445e0..241c9b129a3 100644 --- a/tests/components/fan/test_device_condition.py +++ b/tests/components/fan/test_device_condition.py @@ -136,7 +136,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_on - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_on " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, @@ -154,7 +158,11 @@ async def test_if_state(hass, calls): "action": { "service": "test.automation", "data_template": { - "some": "is_off - {{ trigger.platform }} - {{ trigger.event.event_type }}" + "some": ( + "is_off " + "- {{ trigger.platform }} " + "- {{ trigger.event.event_type }}" + ) }, }, }, diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py index 68a855f93b0..4255c592792 100644 --- a/tests/components/fan/test_device_trigger.py +++ b/tests/components/fan/test_device_trigger.py @@ -163,9 +163,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "turn_on - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "turn_on " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -182,9 +185,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "turn_off - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "turn_off " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, @@ -201,9 +207,12 @@ async def test_if_fires_on_state_change(hass, calls): "service": "test.automation", "data_template": { "some": ( - "turn_on_or_off - {{ trigger.platform}} - " - "{{ trigger.entity_id}} - {{ trigger.from_state.state}} - " - "{{ trigger.to_state.state}} - {{ trigger.for }}" + "turn_on_or_off " + "- {{ trigger.platform }} " + "- {{ trigger.entity_id }} " + "- {{ trigger.from_state.state }} " + "- {{ trigger.to_state.state }} " + "- {{ trigger.for }}" ) }, }, diff --git a/tests/components/filesize/test_config_flow.py b/tests/components/filesize/test_config_flow.py index ed9d4004b1a..2bdc1ed82a0 100644 --- a/tests/components/filesize/test_config_flow.py +++ b/tests/components/filesize/test_config_flow.py @@ -22,7 +22,6 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index bb01b5b4c5c..054b4cbc0dc 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -14,6 +14,7 @@ from homeassistant.components.filter.sensor import ( TimeSMAFilter, TimeThrottleFilter, ) +from homeassistant.components.recorder import Recorder from homeassistant.components.sensor import ( ATTR_STATE_CLASS, SensorDeviceClass, @@ -27,7 +28,7 @@ from homeassistant.const import ( STATE_UNKNOWN, UnitOfTemperature, ) -import homeassistant.core as ha +from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -35,19 +36,19 @@ import homeassistant.util.dt as dt_util from tests.common import assert_setup_component, get_fixture_path -@pytest.fixture -def values(): +@pytest.fixture(name="values") +def values_fixture() -> list[State]: """Fixture for a list of test States.""" values = [] raw_values = [20, 19, 18, 21, 22, 0] timestamp = dt_util.utcnow() for val in raw_values: - values.append(ha.State("sensor.test_monitored", val, last_updated=timestamp)) + values.append(State("sensor.test_monitored", str(val), last_updated=timestamp)) timestamp += timedelta(minutes=1) return values -async def test_setup_fail(hass): +async def test_setup_fail(hass: HomeAssistant) -> None: """Test if filter doesn't exist.""" config = { "sensor": { @@ -61,7 +62,9 @@ async def test_setup_fail(hass): await hass.async_block_till_done() -async def test_chain(recorder_mock, hass, values): +async def test_chain( + recorder_mock: Recorder, hass: HomeAssistant, values: list[State] +) -> None: """Test if filter chaining works.""" config = { "sensor": { @@ -89,7 +92,12 @@ async def test_chain(recorder_mock, hass, values): @pytest.mark.parametrize("missing", (True, False)) -async def test_chain_history(recorder_mock, hass, values, missing): +async def test_chain_history( + recorder_mock: Recorder, + hass: HomeAssistant, + values: list[State], + missing: bool, +) -> None: """Test if filter chaining works, when a source is and isn't recorded.""" config = { "sensor": { @@ -114,10 +122,10 @@ async def test_chain_history(recorder_mock, hass, values, missing): else: fake_states = { "sensor.test_monitored": [ - ha.State("sensor.test_monitored", 18.0, last_changed=t_0), - ha.State("sensor.test_monitored", "unknown", last_changed=t_1), - ha.State("sensor.test_monitored", 19.0, last_changed=t_2), - ha.State("sensor.test_monitored", 18.2, last_changed=t_3), + State("sensor.test_monitored", "18.0", last_changed=t_0), + State("sensor.test_monitored", "unknown", last_changed=t_1), + State("sensor.test_monitored", "19.0", last_changed=t_2), + State("sensor.test_monitored", "18.2", last_changed=t_3), ] } @@ -143,7 +151,7 @@ async def test_chain_history(recorder_mock, hass, values, missing): assert state.state == "17.05" -async def test_source_state_none(recorder_mock, hass, values): +async def test_source_state_none(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test is source sensor state is null and sets state to STATE_UNKNOWN.""" config = { @@ -203,7 +211,7 @@ async def test_source_state_none(recorder_mock, hass, values): assert state.state == STATE_UNKNOWN -async def test_history_time(recorder_mock, hass): +async def test_history_time(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test loading from history based on a time window.""" config = { "sensor": { @@ -220,9 +228,9 @@ async def test_history_time(recorder_mock, hass): fake_states = { "sensor.test_monitored": [ - ha.State("sensor.test_monitored", 18.0, last_changed=t_0), - ha.State("sensor.test_monitored", 19.0, last_changed=t_1), - ha.State("sensor.test_monitored", 18.2, last_changed=t_2), + State("sensor.test_monitored", "18.0", last_changed=t_0), + State("sensor.test_monitored", "19.0", last_changed=t_1), + State("sensor.test_monitored", "18.2", last_changed=t_2), ] } with patch( @@ -241,7 +249,7 @@ async def test_history_time(recorder_mock, hass): assert state.state == "18.0" -async def test_setup(recorder_mock, hass): +async def test_setup(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test if filter attributes are inherited.""" config = { "sensor": { @@ -283,7 +291,7 @@ async def test_setup(recorder_mock, hass): assert entity_id == "sensor.test" -async def test_invalid_state(recorder_mock, hass): +async def test_invalid_state(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test if filter attributes are inherited.""" config = { "sensor": { @@ -313,7 +321,7 @@ async def test_invalid_state(recorder_mock, hass): assert state.state == STATE_UNAVAILABLE -async def test_timestamp_state(recorder_mock, hass): +async def test_timestamp_state(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test if filter state is a datetime.""" config = { "sensor": { @@ -342,7 +350,7 @@ async def test_timestamp_state(recorder_mock, hass): assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP -async def test_outlier(values): +async def test_outlier(values: list[State]) -> None: """Test if outlier filter works.""" filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) for state in values: @@ -350,7 +358,7 @@ async def test_outlier(values): assert filtered.state == 21 -def test_outlier_step(values): +def test_outlier_step(values: list[State]) -> None: """ Test step-change handling in outlier. @@ -365,19 +373,19 @@ def test_outlier_step(values): assert filtered.state == 22 -def test_initial_outlier(values): +def test_initial_outlier(values: list[State]) -> None: """Test issue #13363.""" filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) - out = ha.State("sensor.test_monitored", 4000) + out = State("sensor.test_monitored", "4000") for state in [out] + values: filtered = filt.filter_state(state) assert filtered.state == 21 -def test_unknown_state_outlier(values): +def test_unknown_state_outlier(values: list[State]) -> None: """Test issue #32395.""" filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) - out = ha.State("sensor.test_monitored", "unknown") + out = State("sensor.test_monitored", "unknown") for state in [out] + values + [out]: try: filtered = filt.filter_state(state) @@ -386,7 +394,7 @@ def test_unknown_state_outlier(values): assert filtered.state == 21 -def test_precision_zero(values): +def test_precision_zero(values: list[State]) -> None: """Test if precision of zero returns an integer.""" filt = LowPassFilter(window_size=10, precision=0, entity=None, time_constant=10) for state in values: @@ -394,10 +402,10 @@ def test_precision_zero(values): assert isinstance(filtered.state, int) -def test_lowpass(values): +def test_lowpass(values: list[State]) -> None: """Test if lowpass filter works.""" filt = LowPassFilter(window_size=10, precision=2, entity=None, time_constant=10) - out = ha.State("sensor.test_monitored", "unknown") + out = State("sensor.test_monitored", "unknown") for state in [out] + values + [out]: try: filtered = filt.filter_state(state) @@ -406,7 +414,7 @@ def test_lowpass(values): assert filtered.state == 18.05 -def test_range(values): +def test_range(values: list[State]) -> None: """Test if range filter works.""" lower = 10 upper = 20 @@ -422,7 +430,7 @@ def test_range(values): assert unf == filtered.state -def test_range_zero(values): +def test_range_zero(values: list[State]) -> None: """Test if range filter works with zeroes as bounds.""" lower = 0 upper = 0 @@ -438,7 +446,7 @@ def test_range_zero(values): assert unf == filtered.state -def test_throttle(values): +def test_throttle(values: list[State]) -> None: """Test if lowpass filter works.""" filt = ThrottleFilter(window_size=3, precision=2, entity=None) filtered = [] @@ -449,7 +457,7 @@ def test_throttle(values): assert [20, 21] == [f.state for f in filtered] -def test_time_throttle(values): +def test_time_throttle(values: list[State]) -> None: """Test if lowpass filter works.""" filt = TimeThrottleFilter( window_size=timedelta(minutes=2), precision=2, entity=None @@ -462,7 +470,7 @@ def test_time_throttle(values): assert [20, 18, 22] == [f.state for f in filtered] -def test_time_sma(values): +def test_time_sma(values: list[State]) -> None: """Test if time_sma filter works.""" filt = TimeSMAFilter( window_size=timedelta(minutes=2), precision=2, entity=None, type="last" @@ -472,7 +480,7 @@ def test_time_sma(values): assert filtered.state == 21.5 -async def test_reload(recorder_mock, hass): +async def test_reload(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Verify we can reload filter sensors.""" hass.states.async_set("sensor.test_monitored", 12345) await async_setup_component( diff --git a/tests/components/flipr/test_sensor.py b/tests/components/flipr/test_sensor.py index 51fbf2941f8..75ab6ffd0bd 100644 --- a/tests/components/flipr/test_sensor.py +++ b/tests/components/flipr/test_sensor.py @@ -12,7 +12,7 @@ from homeassistant.const import ( CONF_EMAIL, CONF_PASSWORD, PERCENTAGE, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as entity_reg @@ -71,7 +71,7 @@ async def test_sensors(hass: HomeAssistant) -> None: state = hass.states.get("sensor.flipr_myfliprid_water_temp") assert state assert state.attributes.get(ATTR_ICON) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.state == "10.5" diff --git a/tests/components/foobot/test_sensor.py b/tests/components/foobot/test_sensor.py index 93908715888..3b02df55bf4 100644 --- a/tests/components/foobot/test_sensor.py +++ b/tests/components/foobot/test_sensor.py @@ -14,7 +14,7 @@ from homeassistant.const import ( CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.exceptions import PlatformNotReady from homeassistant.setup import async_setup_component @@ -43,7 +43,7 @@ async def test_default_setup(hass, aioclient_mock): metrics = { "co2": ["1232.0", CONCENTRATION_PARTS_PER_MILLION], - "temperature": ["21.1", TEMP_CELSIUS], + "temperature": ["21.1", UnitOfTemperature.CELSIUS], "humidity": ["49.5", PERCENTAGE], "pm2_5": ["144.8", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER], "voc": ["340.7", CONCENTRATION_PARTS_PER_BILLION], diff --git a/tests/components/forecast_solar/test_config_flow.py b/tests/components/forecast_solar/test_config_flow.py index 616dcba4a36..41cfb4d839e 100644 --- a/tests/components/forecast_solar/test_config_flow.py +++ b/tests/components/forecast_solar/test_config_flow.py @@ -25,7 +25,6 @@ async def test_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> No assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -68,7 +67,6 @@ async def test_options_flow_invalid_api( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" - assert "flow_id" in result result2 = await hass.config_entries.options.async_configure( result["flow_id"], @@ -101,7 +99,6 @@ async def test_options_flow( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" - assert "flow_id" in result # With the API key result2 = await hass.config_entries.options.async_configure( @@ -142,7 +139,6 @@ async def test_options_flow_without_key( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" - assert "flow_id" in result # Without the API key result2 = await hass.config_entries.options.async_configure( diff --git a/tests/components/forecast_solar/test_sensor.py b/tests/components/forecast_solar/test_sensor.py index 893730c722e..3735b992a04 100644 --- a/tests/components/forecast_solar/test_sensor.py +++ b/tests/components/forecast_solar/test_sensor.py @@ -15,8 +15,8 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, + UnitOfEnergy, + UnitOfPower, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -44,7 +44,7 @@ async def test_sensors( == "Solar production forecast Estimated energy production - today" ) assert state.attributes.get(ATTR_STATE_CLASS) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -59,7 +59,7 @@ async def test_sensors( == "Solar production forecast Estimated energy production - tomorrow" ) assert state.attributes.get(ATTR_STATE_CLASS) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -104,7 +104,7 @@ async def test_sensors( == "Solar production forecast Estimated power production - now" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes @@ -119,7 +119,7 @@ async def test_sensors( == "Solar production forecast Estimated energy production - this hour" ) assert state.attributes.get(ATTR_STATE_CLASS) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -134,7 +134,7 @@ async def test_sensors( == "Solar production forecast Estimated energy production - next hour" ) assert state.attributes.get(ATTR_STATE_CLASS) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -228,6 +228,6 @@ async def test_enabling_disable_by_default( state.attributes.get(ATTR_FRIENDLY_NAME) == f"Solar production forecast {name}" ) assert state.attributes.get(ATTR_STATE_CLASS) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index fe7d11068fd..34311c0aa55 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -52,6 +52,8 @@ class FritzEntityBaseMock(Mock): manufacturer = CONF_FAKE_MANUFACTURER name = CONF_FAKE_NAME productname = CONF_FAKE_PRODUCTNAME + rel_humidity = None + battery_level = None class FritzDeviceBinarySensorMock(FritzEntityBaseMock): diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index faed4389310..0a1feb377e0 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -36,7 +36,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, PERCENTAGE, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util @@ -87,14 +87,14 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): assert ( state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Comfort Temperature" ) - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert ATTR_STATE_CLASS not in state.attributes state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_eco_temperature") assert state assert state.state == "16.0" assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Eco Temperature" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert ATTR_STATE_CLASS not in state.attributes state = hass.states.get( @@ -106,7 +106,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Next Scheduled Temperature" ) - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert ATTR_STATE_CLASS not in state.attributes state = hass.states.get( diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index dbec8b4ec14..41725a6a6e2 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -19,7 +19,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, STATE_UNAVAILABLE, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -53,7 +53,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): "domain": SENSOR_DOMAIN, "platform": FB_DOMAIN, "unique_id": CONF_FAKE_AIN, - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, }, CONF_FAKE_AIN, f"{CONF_FAKE_AIN}_temperature", @@ -106,7 +106,7 @@ async def test_update_unique_id( "domain": SENSOR_DOMAIN, "platform": FB_DOMAIN, "unique_id": f"{CONF_FAKE_AIN}_temperature", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, }, f"{CONF_FAKE_AIN}_temperature", ), diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index dfb480459da..0077f878b72 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -11,7 +11,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, PERCENTAGE, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util @@ -36,7 +36,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): assert state assert state.state == "1.23" assert state.attributes[ATTR_FRIENDLY_NAME] == f"{CONF_FAKE_NAME} Temperature" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT state = hass.states.get(f"{ENTITY_ID}_humidity") diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 852b41512f5..01b80872b66 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -16,15 +16,15 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, - ELECTRIC_CURRENT_AMPERE, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, STATE_UNAVAILABLE, - TEMP_CELSIUS, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util @@ -58,35 +58,35 @@ async def test_setup(hass: HomeAssistant, fritz: Mock): f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_temperature", "1.23", f"{CONF_FAKE_NAME} Temperature", - TEMP_CELSIUS, + UnitOfTemperature.CELSIUS, SensorStateClass.MEASUREMENT, ], [ f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_power_consumption", "5.678", f"{CONF_FAKE_NAME} Power Consumption", - POWER_WATT, + UnitOfPower.WATT, SensorStateClass.MEASUREMENT, ], [ f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_total_energy", "1.234", f"{CONF_FAKE_NAME} Total Energy", - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, SensorStateClass.TOTAL_INCREASING, ], [ f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_voltage", "230.0", f"{CONF_FAKE_NAME} Voltage", - ELECTRIC_POTENTIAL_VOLT, + UnitOfElectricPotential.VOLT, SensorStateClass.MEASUREMENT, ], [ f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_electric_current", "0.025", f"{CONF_FAKE_NAME} Electric Current", - ELECTRIC_CURRENT_AMPERE, + UnitOfElectricCurrent.AMPERE, SensorStateClass.MEASUREMENT, ], ) diff --git a/tests/components/fully_kiosk/test_config_flow.py b/tests/components/fully_kiosk/test_config_flow.py index 19a8715b4cd..719d2442c51 100644 --- a/tests/components/fully_kiosk/test_config_flow.py +++ b/tests/components/fully_kiosk/test_config_flow.py @@ -28,7 +28,6 @@ async def test_full_flow( ) assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -72,7 +71,6 @@ async def test_errors( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert "flow_id" in result flow_id = result["flow_id"] mock_fully_kiosk_config_flow.getDeviceInfo.side_effect = side_effect @@ -119,7 +117,6 @@ async def test_duplicate_updates_existing_entry( ) assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/garages_amsterdam/test_config_flow.py b/tests/components/garages_amsterdam/test_config_flow.py index dfb4531fa1d..f39aea541aa 100644 --- a/tests/components/garages_amsterdam/test_config_flow.py +++ b/tests/components/garages_amsterdam/test_config_flow.py @@ -18,7 +18,6 @@ async def test_full_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result.get("type") == FlowResultType.FORM - assert "flow_id" in result with patch( "homeassistant.components.garages_amsterdam.async_setup_entry", diff --git a/tests/components/gdacs/test_geo_location.py b/tests/components/gdacs/test_geo_location.py index 83b9efde1e8..b7678e557b5 100644 --- a/tests/components/gdacs/test_geo_location.py +++ b/tests/components/gdacs/test_geo_location.py @@ -29,7 +29,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_RADIUS, EVENT_HOMEASSISTANT_START, - LENGTH_KILOMETERS, + UnitOfLength, ) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -131,7 +131,7 @@ async def test_setup(hass): ATTR_EVENT_TYPE: "Drought", ATTR_SEVERITY: "Severity 1", ATTR_VULNERABILITY: "Vulnerability 1", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "gdacs", ATTR_ICON: "mdi:water-off", } @@ -147,7 +147,7 @@ async def test_setup(hass): ATTR_FRIENDLY_NAME: "Tropical Cyclone: Name 2", ATTR_DESCRIPTION: "Description 2", ATTR_EVENT_TYPE: "Tropical Cyclone", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "gdacs", ATTR_ICON: "mdi:weather-hurricane", } @@ -164,7 +164,7 @@ async def test_setup(hass): ATTR_DESCRIPTION: "Description 3", ATTR_EVENT_TYPE: "Tropical Cyclone", ATTR_COUNTRY: "Country 2", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "gdacs", ATTR_ICON: "mdi:weather-hurricane", } diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 52714ab15a2..4c1ed8ad8c2 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -30,7 +30,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfTemperature, ) import homeassistant.core as ha from homeassistant.core import DOMAIN as HASS_DOMAIN, CoreState, State, callback @@ -572,7 +572,7 @@ def _setup_switch(hass, is_on): @pytest.fixture async def setup_comp_3(hass): """Initialize components.""" - hass.config.temperature_unit = TEMP_CELSIUS + hass.config.temperature_unit = UnitOfTemperature.CELSIUS assert await async_setup_component( hass, DOMAIN, @@ -717,7 +717,7 @@ async def test_no_state_change_when_operation_mode_off_2(hass, setup_comp_3): @pytest.fixture async def setup_comp_4(hass): """Initialize components.""" - hass.config.temperature_unit = TEMP_CELSIUS + hass.config.temperature_unit = UnitOfTemperature.CELSIUS assert await async_setup_component( hass, DOMAIN, @@ -823,7 +823,7 @@ async def test_mode_change_ac_trigger_on_not_long_enough(hass, setup_comp_4): @pytest.fixture async def setup_comp_5(hass): """Initialize components.""" - hass.config.temperature_unit = TEMP_CELSIUS + hass.config.temperature_unit = UnitOfTemperature.CELSIUS assert await async_setup_component( hass, DOMAIN, @@ -929,7 +929,7 @@ async def test_mode_change_ac_trigger_on_not_long_enough_2(hass, setup_comp_5): @pytest.fixture async def setup_comp_6(hass): """Initialize components.""" - hass.config.temperature_unit = TEMP_CELSIUS + hass.config.temperature_unit = UnitOfTemperature.CELSIUS assert await async_setup_component( hass, DOMAIN, @@ -1034,7 +1034,7 @@ async def test_mode_change_heater_trigger_on_not_long_enough(hass, setup_comp_6) @pytest.fixture async def setup_comp_7(hass): """Initialize components.""" - hass.config.temperature_unit = TEMP_CELSIUS + hass.config.temperature_unit = UnitOfTemperature.CELSIUS assert await async_setup_component( hass, DOMAIN, @@ -1107,7 +1107,7 @@ async def test_temp_change_ac_trigger_off_long_enough_3(hass, setup_comp_7): @pytest.fixture async def setup_comp_8(hass): """Initialize components.""" - hass.config.temperature_unit = TEMP_CELSIUS + hass.config.temperature_unit = UnitOfTemperature.CELSIUS assert await async_setup_component( hass, DOMAIN, diff --git a/tests/components/geo_json_events/test_geo_location.py b/tests/components/geo_json_events/test_geo_location.py index 66f5f06ffe9..193c9bfa090 100644 --- a/tests/components/geo_json_events/test_geo_location.py +++ b/tests/components/geo_json_events/test_geo_location.py @@ -20,7 +20,7 @@ from homeassistant.const import ( CONF_RADIUS, CONF_URL, EVENT_HOMEASSISTANT_START, - LENGTH_KILOMETERS, + UnitOfLength, ) from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component @@ -94,7 +94,7 @@ async def test_setup(hass): ATTR_LATITUDE: -31.0, ATTR_LONGITUDE: 150.0, ATTR_FRIENDLY_NAME: "Title 1", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "geo_json_events", } assert round(abs(float(state.state) - 15.5), 7) == 0 @@ -107,7 +107,7 @@ async def test_setup(hass): ATTR_LATITUDE: -31.1, ATTR_LONGITUDE: 150.1, ATTR_FRIENDLY_NAME: "Title 2", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "geo_json_events", } assert round(abs(float(state.state) - 20.5), 7) == 0 @@ -120,7 +120,7 @@ async def test_setup(hass): ATTR_LATITUDE: -31.2, ATTR_LONGITUDE: 150.2, ATTR_FRIENDLY_NAME: "Title 3", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "geo_json_events", } assert round(abs(float(state.state) - 25.5), 7) == 0 diff --git a/tests/components/geocaching/test_config_flow.py b/tests/components/geocaching/test_config_flow.py index a40668c627f..10408a79049 100644 --- a/tests/components/geocaching/test_config_flow.py +++ b/tests/components/geocaching/test_config_flow.py @@ -52,9 +52,7 @@ async def test_full_flow( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert "flow_id" in result - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -110,8 +108,7 @@ async def test_existing_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert "flow_id" in result - # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -151,9 +148,7 @@ async def test_oauth_error( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert "flow_id" in result - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -209,9 +204,7 @@ async def test_reauthentication( assert "flow_id" in flows[0] result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) - assert "flow_id" in result - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 9aaa356085b..eb0dc8d0d31 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -1,7 +1,5 @@ """The tests for the Geofency device tracker platform.""" from http import HTTPStatus - -# pylint: disable=redefined-outer-name from unittest.mock import patch import pytest diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py index 327829d3d4b..2f58a7fd095 100644 --- a/tests/components/geonetnz_quakes/test_geo_location.py +++ b/tests/components/geonetnz_quakes/test_geo_location.py @@ -23,7 +23,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_RADIUS, EVENT_HOMEASSISTANT_START, - LENGTH_KILOMETERS, + UnitOfLength, ) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -99,7 +99,7 @@ async def test_setup(hass): ATTR_DEPTH: 10.5, ATTR_MMI: 5, ATTR_QUALITY: "best", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "geonetnz_quakes", ATTR_ICON: "mdi:pulse", } @@ -114,7 +114,7 @@ async def test_setup(hass): ATTR_LONGITUDE: -3.1, ATTR_FRIENDLY_NAME: "Title 2", ATTR_MAGNITUDE: 4.6, - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "geonetnz_quakes", ATTR_ICON: "mdi:pulse", } @@ -129,7 +129,7 @@ async def test_setup(hass): ATTR_LONGITUDE: -3.2, ATTR_FRIENDLY_NAME: "Title 3", ATTR_LOCALITY: "Locality 3", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "geonetnz_quakes", ATTR_ICON: "mdi:pulse", } diff --git a/tests/components/gios/test_sensor.py b/tests/components/gios/test_sensor.py index f7475b1ef0f..9f88b247983 100644 --- a/tests/components/gios/test_sensor.py +++ b/tests/components/gios/test_sensor.py @@ -162,7 +162,6 @@ async def test_sensor(hass): assert state.state == "dobry" assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_STATION) == "Test Name 1" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.AQI assert state.attributes.get(ATTR_STATE_CLASS) is None assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None diff --git a/tests/components/github/test_config_flow.py b/tests/components/github/test_config_flow.py index 174d70c3ae3..ad3be582a5d 100644 --- a/tests/components/github/test_config_flow.py +++ b/tests/components/github/test_config_flow.py @@ -59,7 +59,6 @@ async def test_full_user_flow_implementation( assert result["step_id"] == "device" assert result["type"] == FlowResultType.SHOW_PROGRESS - assert "flow_id" in result result = await hass.config_entries.flow.async_configure(result["flow_id"]) diff --git a/tests/components/goalzero/test_sensor.py b/tests/components/goalzero/test_sensor.py index 402c03f2e51..58869bead65 100644 --- a/tests/components/goalzero/test_sensor.py +++ b/tests/components/goalzero/test_sensor.py @@ -10,15 +10,14 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, - ELECTRIC_CURRENT_AMPERE, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_WATT_HOUR, PERCENTAGE, - POWER_WATT, SIGNAL_STRENGTH_DECIBELS, - TEMP_CELSIUS, - TIME_MINUTES, - TIME_SECONDS, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfTemperature, + UnitOfTime, ) from homeassistant.core import HomeAssistant @@ -38,37 +37,43 @@ async def test_sensors( state = hass.states.get(f"sensor.{DEFAULT_NAME}_watts_in") assert state.state == "0.0" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get(f"sensor.{DEFAULT_NAME}_amps_in") assert state.state == "0.0" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_AMPERE + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricCurrent.AMPERE + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get(f"sensor.{DEFAULT_NAME}_watts_out") assert state.state == "50.5" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get(f"sensor.{DEFAULT_NAME}_amps_out") assert state.state == "2.1" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_AMPERE + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricCurrent.AMPERE + ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get(f"sensor.{DEFAULT_NAME}_wh_out") assert state.state == "5.23" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.WATT_HOUR assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING state = hass.states.get(f"sensor.{DEFAULT_NAME}_wh_stored") assert state.state == "1330" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.WATT_HOUR assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get(f"sensor.{DEFAULT_NAME}_volts") assert state.state == "12.0" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_POTENTIAL_VOLT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricPotential.VOLT + ) assert state.attributes.get(ATTR_STATE_CLASS) is None state = hass.states.get(f"sensor.{DEFAULT_NAME}_state_of_charge_percent") assert state.state == "95" @@ -78,12 +83,12 @@ async def test_sensors( state = hass.states.get(f"sensor.{DEFAULT_NAME}_time_to_empty_full") assert state.state == "-1" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DURATION - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_MINUTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.MINUTES assert state.attributes.get(ATTR_STATE_CLASS) is None state = hass.states.get(f"sensor.{DEFAULT_NAME}_temperature") assert state.state == "25" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_STATE_CLASS) is None state = hass.states.get(f"sensor.{DEFAULT_NAME}_wi_fi_strength") assert state.state == "-62" @@ -93,7 +98,7 @@ async def test_sensors( state = hass.states.get(f"sensor.{DEFAULT_NAME}_total_run_time") assert state.state == "1720984" assert state.attributes.get(ATTR_DEVICE_CLASS) is None - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_SECONDS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.SECONDS assert state.attributes.get(ATTR_STATE_CLASS) is None state = hass.states.get(f"sensor.{DEFAULT_NAME}_wi_fi_ssid") assert state.state == "wifi" diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 88e75499678..6e466bdcd4f 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -121,7 +121,9 @@ def mock_calendars_yaml( calendars_config: list[dict[str, Any]], ) -> Generator[Mock, None, None]: """Fixture that prepares the google_calendars.yaml mocks.""" - mocked_open_function = mock_open(read_data=yaml.dump(calendars_config)) + mocked_open_function = mock_open( + read_data=yaml.dump(calendars_config) if calendars_config else None + ) with patch("homeassistant.components.google.open", mocked_open_function): yield mocked_open_function diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 452b2300e5b..4436b9226ff 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -7,6 +7,7 @@ import http import time from typing import Any from unittest.mock import Mock, patch +import zoneinfo from aiohttp.client_exceptions import ClientError import pytest @@ -56,28 +57,36 @@ def assert_state(actual: State | None, expected: State | None) -> None: @pytest.fixture( params=[ ( + DOMAIN, SERVICE_ADD_EVENT, {"calendar_id": CALENDAR_ID}, None, ), ( + DOMAIN, + SERVICE_CREATE_EVENT, + {}, + {"entity_id": TEST_API_ENTITY}, + ), + ( + "calendar", SERVICE_CREATE_EVENT, {}, {"entity_id": TEST_API_ENTITY}, ), ], - ids=("add_event", "create_event"), + ids=("google.add_event", "google.create_event", "calendar.create_event"), ) def add_event_call_service( hass: HomeAssistant, request: Any, ) -> Callable[dict[str, Any], Awaitable[None]]: """Fixture for calling the add or create event service.""" - (service_call, data, target) = request.param + (domain, service_call, data, target) = request.param async def call_service(params: dict[str, Any]) -> None: await hass.services.async_call( - DOMAIN, + domain, service_call, { **data, @@ -192,6 +201,26 @@ async def test_calendar_yaml_error( assert hass.states.get(TEST_API_ENTITY) +@pytest.mark.parametrize("calendars_config", [None]) +async def test_empty_calendar_yaml( + hass: HomeAssistant, + component_setup: ComponentSetup, + calendars_config: list[dict[str, Any]], + mock_calendars_yaml: None, + mock_calendars_list: ApiResult, + test_api_calendar: dict[str, Any], + mock_events_list: ApiResult, +) -> None: + """Test an empty yaml file is equivalent to a missing yaml file.""" + mock_calendars_list({"items": [test_api_calendar]}) + mock_events_list({}) + + assert await component_setup() + + assert not hass.states.get(TEST_YAML_ENTITY) + assert hass.states.get(TEST_API_ENTITY) + + async def test_init_calendar( hass: HomeAssistant, component_setup: ComponentSetup, @@ -516,7 +545,7 @@ async def test_add_event_date_time( mock_events_list({}) assert await component_setup() - start_datetime = datetime.datetime.now() + start_datetime = datetime.datetime.now(tz=zoneinfo.ZoneInfo("America/Regina")) delta = datetime.timedelta(days=3, hours=3) end_datetime = start_datetime + delta diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 97484f31341..561ec9caf5f 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -1,7 +1,5 @@ """The tests for the Google Assistant component.""" from http import HTTPStatus - -# pylint: disable=protected-access import json from aiohttp.hdrs import AUTHORIZATION @@ -20,7 +18,7 @@ from homeassistant.components import ( media_player, switch, ) -from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES +from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, UnitOfTemperature from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import EntityCategory @@ -127,9 +125,6 @@ def hass_fixture(event_loop, hass): return hass -# pylint: disable=redefined-outer-name - - async def test_sync_request(hass_fixture, assistant_client, auth_header): """Test a sync request.""" @@ -293,7 +288,7 @@ async def test_query_climate_request(hass_fixture, assistant_client, auth_header async def test_query_climate_request_f(hass_fixture, assistant_client, auth_header): """Test a query request.""" # Mock demo devices as fahrenheit to see if we convert to celsius - hass_fixture.config.units.temperature_unit = const.TEMP_FAHRENHEIT + hass_fixture.config.units.temperature_unit = UnitOfTemperature.FAHRENHEIT for entity_id in ("climate.hvac", "climate.heatpump", "climate.ecobee"): state = hass_fixture.states.get(entity_id) attr = dict(state.attributes) @@ -347,7 +342,7 @@ async def test_query_climate_request_f(hass_fixture, assistant_client, auth_head "thermostatHumidityAmbient": 54, "currentFanSpeedSetting": "On High", } - hass_fixture.config.units.temperature_unit = const.TEMP_CELSIUS + hass_fixture.config.units.temperature_unit = UnitOfTemperature.CELSIUS async def test_query_humidifier_request(hass_fixture, assistant_client, auth_header): diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 26b85f19c58..31984129968 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -21,7 +21,7 @@ from homeassistant.components.google_assistant import ( trait, ) from homeassistant.config import async_process_ha_core_config -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, __version__ +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfTemperature, __version__ from homeassistant.core import EVENT_CALL_SERVICE, State from homeassistant.helpers import device_registry, entity_platform from homeassistant.setup import async_setup_component @@ -228,7 +228,6 @@ async def test_sync_message(hass, registries): assert events[0].data == {"request_id": REQ_ID, "source": "cloud"} -# pylint: disable=redefined-outer-name @pytest.mark.parametrize("area_on_device", [True, False]) async def test_sync_in_area(area_on_device, hass, registries): """Test a sync message where room hint comes from area.""" @@ -827,7 +826,11 @@ async def test_raising_error_trait(hass): hass.states.async_set( "climate.bla", HVACMode.HEAT, - {ATTR_MIN_TEMP: 15, ATTR_MAX_TEMP: 30, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + { + ATTR_MIN_TEMP: 15, + ATTR_MAX_TEMP: 30, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, + }, ) events = async_capture_events(hass, EVENT_COMMAND_RECEIVED) @@ -893,7 +896,6 @@ async def test_raising_error_trait(hass): async def test_serialize_input_boolean(hass): """Test serializing an input boolean entity.""" state = State("input_boolean.bla", "on") - # pylint: disable=protected-access entity = sh.GoogleEntity(hass, BASIC_CONFIG, state) result = entity.sync_serialize(None, "mock-uuid") assert result == { @@ -918,7 +920,7 @@ async def test_unavailable_state_does_sync(hass): ) light.hass = hass light.entity_id = "light.demo_light" - light._available = False # pylint: disable=protected-access + light._available = False await light.async_update_ha_state() events = async_capture_events(hass, EVENT_SYNC_RECEIVED) diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 9d80c7ff507..a6db99e1e38 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -52,8 +52,7 @@ from homeassistant.const import ( STATE_STANDBY, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) from homeassistant.core import DOMAIN as HA_DOMAIN, EVENT_CALL_SERVICE, State from homeassistant.util import color @@ -833,7 +832,7 @@ async def test_temperature_setting_climate_onoff(hass): assert helpers.get_google_type(climate.DOMAIN, None) is not None assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None, None) - hass.config.units.temperature_unit = TEMP_FAHRENHEIT + hass.config.units.temperature_unit = UnitOfTemperature.FAHRENHEIT trt = trait.TemperatureSettingTrait( hass, @@ -878,7 +877,7 @@ async def test_temperature_setting_climate_no_modes(hass): assert helpers.get_google_type(climate.DOMAIN, None) is not None assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None, None) - hass.config.units.temperature_unit = TEMP_CELSIUS + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS trt = trait.TemperatureSettingTrait( hass, @@ -904,7 +903,7 @@ async def test_temperature_setting_climate_range(hass): assert helpers.get_google_type(climate.DOMAIN, None) is not None assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None, None) - hass.config.units.temperature_unit = TEMP_FAHRENHEIT + hass.config.units.temperature_unit = UnitOfTemperature.FAHRENHEIT trt = trait.TemperatureSettingTrait( hass, @@ -978,7 +977,7 @@ async def test_temperature_setting_climate_range(hass): {}, ) assert err.value.code == const.ERR_VALUE_OUT_OF_RANGE - hass.config.units.temperature_unit = TEMP_CELSIUS + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS async def test_temperature_setting_climate_setpoint(hass): @@ -986,7 +985,7 @@ async def test_temperature_setting_climate_setpoint(hass): assert helpers.get_google_type(climate.DOMAIN, None) is not None assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None, None) - hass.config.units.temperature_unit = TEMP_CELSIUS + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS trt = trait.TemperatureSettingTrait( hass, @@ -1041,7 +1040,7 @@ async def test_temperature_setting_climate_setpoint_auto(hass): Setpoint in auto mode. """ - hass.config.units.temperature_unit = TEMP_CELSIUS + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS trt = trait.TemperatureSettingTrait( hass, @@ -1088,7 +1087,7 @@ async def test_temperature_setting_climate_setpoint_auto(hass): async def test_temperature_control(hass): """Test TemperatureControl trait support for sensor domain.""" - hass.config.units.temperature_unit = TEMP_CELSIUS + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS trt = trait.TemperatureControlTrait( hass, @@ -2891,10 +2890,10 @@ async def test_temperature_control_sensor(hass): @pytest.mark.parametrize( "unit_in,unit_out,state,ambient", [ - (TEMP_FAHRENHEIT, "F", "70", 21.1), - (TEMP_CELSIUS, "C", "21.1", 21.1), - (TEMP_FAHRENHEIT, "F", "unavailable", None), - (TEMP_FAHRENHEIT, "F", "unknown", None), + (UnitOfTemperature.FAHRENHEIT, "F", "70", 21.1), + (UnitOfTemperature.CELSIUS, "C", "21.1", 21.1), + (UnitOfTemperature.FAHRENHEIT, "F", "unavailable", None), + (UnitOfTemperature.FAHRENHEIT, "F", "unknown", None), ], ) async def test_temperature_control_sensor_data(hass, unit_in, unit_out, state, ambient): @@ -2924,7 +2923,7 @@ async def test_temperature_control_sensor_data(hass, unit_in, unit_out, state, a } else: assert trt.query_attributes() == {} - hass.config.units.temperature_unit = TEMP_CELSIUS + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS async def test_humidity_setting_sensor(hass): diff --git a/tests/components/google_assistant_sdk/conftest.py b/tests/components/google_assistant_sdk/conftest.py index 9730c0fef17..207ceccb342 100644 --- a/tests/components/google_assistant_sdk/conftest.py +++ b/tests/components/google_assistant_sdk/conftest.py @@ -87,6 +87,10 @@ async def mock_setup_integration( class ExpectedCredentials: """Assert credentials have the expected access token.""" + def __init__(self, expected_access_token: str = ACCESS_TOKEN) -> None: + """Initialize ExpectedCredentials.""" + self.expected_access_token = expected_access_token + def __eq__(self, other: Credentials): """Return true if credentials have the expected access token.""" - return other.token == ACCESS_TOKEN + return other.token == self.expected_access_token diff --git a/tests/components/google_assistant_sdk/test_config_flow.py b/tests/components/google_assistant_sdk/test_config_flow.py index 56386df6824..f92ef41420d 100644 --- a/tests/components/google_assistant_sdk/test_config_flow.py +++ b/tests/components/google_assistant_sdk/test_config_flow.py @@ -221,39 +221,65 @@ async def test_options_flow( assert result["type"] == "form" assert result["step_id"] == "init" data_schema = result["data_schema"].schema - assert set(data_schema) == {"language_code"} + assert set(data_schema) == {"enable_conversation_agent", "language_code"} result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={"language_code": "es-ES"}, + user_input={"enable_conversation_agent": False, "language_code": "es-ES"}, ) assert result["type"] == "create_entry" - assert config_entry.options == {"language_code": "es-ES"} + assert config_entry.options == { + "enable_conversation_agent": False, + "language_code": "es-ES", + } # Retrigger options flow, not change language result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == "form" assert result["step_id"] == "init" data_schema = result["data_schema"].schema - assert set(data_schema) == {"language_code"} + assert set(data_schema) == {"enable_conversation_agent", "language_code"} result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={"language_code": "es-ES"}, + user_input={"enable_conversation_agent": False, "language_code": "es-ES"}, ) assert result["type"] == "create_entry" - assert config_entry.options == {"language_code": "es-ES"} + assert config_entry.options == { + "enable_conversation_agent": False, + "language_code": "es-ES", + } # Retrigger options flow, change language result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == "form" assert result["step_id"] == "init" data_schema = result["data_schema"].schema - assert set(data_schema) == {"language_code"} + assert set(data_schema) == {"enable_conversation_agent", "language_code"} result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={"language_code": "en-US"}, + user_input={"enable_conversation_agent": False, "language_code": "en-US"}, ) assert result["type"] == "create_entry" - assert config_entry.options == {"language_code": "en-US"} + assert config_entry.options == { + "enable_conversation_agent": False, + "language_code": "en-US", + } + + # Retrigger options flow, enable conversation agent + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == "form" + assert result["step_id"] == "init" + data_schema = result["data_schema"].schema + assert set(data_schema) == {"enable_conversation_agent", "language_code"} + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"enable_conversation_agent": True, "language_code": "en-US"}, + ) + assert result["type"] == "create_entry" + assert config_entry.options == { + "enable_conversation_agent": True, + "language_code": "en-US", + } diff --git a/tests/components/google_assistant_sdk/test_init.py b/tests/components/google_assistant_sdk/test_init.py index afc5e77042f..e01af4cbc57 100644 --- a/tests/components/google_assistant_sdk/test_init.py +++ b/tests/components/google_assistant_sdk/test_init.py @@ -1,4 +1,5 @@ """Tests for Google Assistant SDK.""" +from datetime import timedelta import http import time from unittest.mock import call, patch @@ -9,12 +10,23 @@ import pytest from homeassistant.components.google_assistant_sdk import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow from .conftest import ComponentSetup, ExpectedCredentials +from tests.common import async_fire_time_changed, async_mock_service from tests.test_util.aiohttp import AiohttpClientMocker +async def fetch_api_url(hass_client, url): + """Fetch an API URL and return HTTP status and contents.""" + client = await hass_client() + response = await client.get(url) + contents = await response.read() + return response.status, contents + + async def test_setup_success( hass: HomeAssistant, setup_integration: ComponentSetup ) -> None: @@ -128,11 +140,40 @@ async def test_send_text_command( blocking=True, ) mock_text_assistant.assert_called_once_with( - ExpectedCredentials(), expected_language_code + ExpectedCredentials(), expected_language_code, audio_out=False ) mock_text_assistant.assert_has_calls([call().__enter__().assist(command)]) +async def test_send_text_commands( + hass: HomeAssistant, + setup_integration: ComponentSetup, +) -> None: + """Test service call send_text_command calls TextAssistant.""" + await setup_integration() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + + command1 = "open the garage door" + command2 = "1234" + with patch( + "homeassistant.components.google_assistant_sdk.helpers.TextAssistant" + ) as mock_text_assistant: + await hass.services.async_call( + DOMAIN, + "send_text_command", + {"command": [command1, command2]}, + blocking=True, + ) + mock_text_assistant.assert_called_once_with( + ExpectedCredentials(), "en-US", audio_out=False + ) + mock_text_assistant.assert_has_calls([call().__enter__().assist(command1)]) + mock_text_assistant.assert_has_calls([call().__enter__().assist(command2)]) + + @pytest.mark.parametrize( "status,requires_reauth", [ @@ -177,3 +218,189 @@ async def test_send_text_command_expired_token_refresh_failure( ) assert any(entry.async_get_active_flows(hass, {"reauth"})) == requires_reauth + + +async def test_send_text_command_media_player( + hass: HomeAssistant, setup_integration: ComponentSetup, hass_client +) -> None: + """Test send_text_command with media_player.""" + await setup_integration() + + play_media_calls = async_mock_service(hass, "media_player", "play_media") + + command = "tell me a joke" + media_player = "media_player.office_speaker" + audio_response1 = b"joke1 audio response bytes" + audio_response2 = b"joke2 audio response bytes" + with patch( + "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", + side_effect=[ + ("joke1 text", None, audio_response1), + ("joke2 text", None, audio_response2), + ], + ) as mock_assist_call: + # Run the same command twice, getting different audio response each time. + await hass.services.async_call( + DOMAIN, + "send_text_command", + { + "command": command, + "media_player": media_player, + }, + blocking=True, + ) + await hass.services.async_call( + DOMAIN, + "send_text_command", + { + "command": command, + "media_player": media_player, + }, + blocking=True, + ) + + mock_assist_call.assert_has_calls([call(command), call(command)]) + assert len(play_media_calls) == 2 + for play_media_call in play_media_calls: + assert play_media_call.data["entity_id"] == [media_player] + assert play_media_call.data["media_content_id"].startswith( + "/api/google_assistant_sdk/audio/" + ) + + audio_url1 = play_media_calls[0].data["media_content_id"] + audio_url2 = play_media_calls[1].data["media_content_id"] + assert audio_url1 != audio_url2 + + # Assert that both audio responses can be served + status, response = await fetch_api_url(hass_client, audio_url1) + assert status == http.HTTPStatus.OK + assert response == audio_response1 + status, response = await fetch_api_url(hass_client, audio_url2) + assert status == http.HTTPStatus.OK + assert response == audio_response2 + + # Assert a nonexistent URL returns 404 + status, _ = await fetch_api_url( + hass_client, "/api/google_assistant_sdk/audio/nonexistent" + ) + assert status == http.HTTPStatus.NOT_FOUND + + # Assert that both audio responses can still be served before the 5 minutes expiration + async_fire_time_changed(hass, utcnow() + timedelta(minutes=4)) + status, response = await fetch_api_url(hass_client, audio_url1) + assert status == http.HTTPStatus.OK + assert response == audio_response1 + status, response = await fetch_api_url(hass_client, audio_url2) + assert status == http.HTTPStatus.OK + assert response == audio_response2 + + # Assert that they cannot be served after the 5 minutes expiration + async_fire_time_changed(hass, utcnow() + timedelta(minutes=6)) + status, response = await fetch_api_url(hass_client, audio_url1) + assert status == http.HTTPStatus.NOT_FOUND + status, response = await fetch_api_url(hass_client, audio_url2) + assert status == http.HTTPStatus.NOT_FOUND + + +async def test_conversation_agent( + hass: HomeAssistant, + setup_integration: ComponentSetup, +) -> None: + """Test GoogleAssistantConversationAgent.""" + await setup_integration() + + assert await async_setup_component(hass, "conversation", {}) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + assert entry.state is ConfigEntryState.LOADED + hass.config_entries.async_update_entry( + entry, options={"enable_conversation_agent": True} + ) + await hass.async_block_till_done() + + text1 = "tell me a joke" + text2 = "tell me another one" + with patch( + "homeassistant.components.google_assistant_sdk.TextAssistant" + ) as mock_text_assistant: + await hass.services.async_call( + "conversation", + "process", + {"text": text1}, + blocking=True, + ) + await hass.services.async_call( + "conversation", + "process", + {"text": text2}, + blocking=True, + ) + + # Assert constructor is called only once since it's reused across requests + assert mock_text_assistant.call_count == 1 + mock_text_assistant.assert_called_once_with(ExpectedCredentials(), "en-US") + mock_text_assistant.assert_has_calls([call().assist(text1)]) + mock_text_assistant.assert_has_calls([call().assist(text2)]) + + +async def test_conversation_agent_refresh_token( + hass: HomeAssistant, + setup_integration: ComponentSetup, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test GoogleAssistantConversationAgent when token is expired.""" + await setup_integration() + + assert await async_setup_component(hass, "conversation", {}) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + assert entry.state is ConfigEntryState.LOADED + hass.config_entries.async_update_entry( + entry, options={"enable_conversation_agent": True} + ) + await hass.async_block_till_done() + + text1 = "tell me a joke" + text2 = "tell me another one" + with patch( + "homeassistant.components.google_assistant_sdk.TextAssistant" + ) as mock_text_assistant: + await hass.services.async_call( + "conversation", + "process", + {"text": text1}, + blocking=True, + ) + + # Expire the token between requests + entry.data["token"]["expires_at"] = time.time() - 3600 + updated_access_token = "updated-access-token" + aioclient_mock.post( + "https://oauth2.googleapis.com/token", + json={ + "access_token": updated_access_token, + "refresh_token": "updated-refresh-token", + "expires_at": time.time() + 3600, + "expires_in": 3600, + }, + ) + + await hass.services.async_call( + "conversation", + "process", + {"text": text2}, + blocking=True, + ) + + # Assert constructor is called twice since the token was expired + assert mock_text_assistant.call_count == 2 + mock_text_assistant.assert_has_calls([call(ExpectedCredentials(), "en-US")]) + mock_text_assistant.assert_has_calls( + [call(ExpectedCredentials(updated_access_token), "en-US")] + ) + mock_text_assistant.assert_has_calls([call().assist(text1)]) + mock_text_assistant.assert_has_calls([call().assist(text2)]) diff --git a/tests/components/google_assistant_sdk/test_notify.py b/tests/components/google_assistant_sdk/test_notify.py index 5a2d11b861b..95d5720cc7e 100644 --- a/tests/components/google_assistant_sdk/test_notify.py +++ b/tests/components/google_assistant_sdk/test_notify.py @@ -1,6 +1,8 @@ """Tests for the Google Assistant notify.""" from unittest.mock import call, patch +import pytest + from homeassistant.components import notify from homeassistant.components.google_assistant_sdk import DOMAIN from homeassistant.components.google_assistant_sdk.const import SUPPORTED_LANGUAGE_CODES @@ -10,14 +12,29 @@ from homeassistant.core import HomeAssistant from .conftest import ComponentSetup, ExpectedCredentials +@pytest.mark.parametrize( + "language_code,message,expected_command", + [ + ("en-US", "Dinner is served", "broadcast Dinner is served"), + ("es-ES", "La cena está en la mesa", "Anuncia La cena está en la mesa"), + ("ko-KR", "저녁 식사가 준비됐어요", "저녁 식사가 준비됐어요 라고 방송해 줘"), + ("ja-JP", "晩ご飯できたよ", "晩ご飯できたよとブロードキャストして"), + ], + ids=["english", "spanish", "korean", "japanese"], +) async def test_broadcast_no_targets( - hass: HomeAssistant, setup_integration: ComponentSetup + hass: HomeAssistant, + setup_integration: ComponentSetup, + language_code: str, + message: str, + expected_command: str, ) -> None: """Test broadcast to all.""" await setup_integration() - message = "time for dinner" - expected_command = "broadcast time for dinner" + entry = hass.config_entries.async_entries(DOMAIN)[0] + entry.options = {"language_code": language_code} + with patch( "homeassistant.components.google_assistant_sdk.helpers.TextAssistant" ) as mock_text_assistant: @@ -27,22 +44,49 @@ async def test_broadcast_no_targets( {notify.ATTR_MESSAGE: message}, ) await hass.async_block_till_done() - mock_text_assistant.assert_called_once_with(ExpectedCredentials(), "en-US") + mock_text_assistant.assert_called_once_with( + ExpectedCredentials(), language_code, audio_out=False + ) mock_text_assistant.assert_has_calls([call().__enter__().assist(expected_command)]) +@pytest.mark.parametrize( + "language_code,message,target,expected_command", + [ + ( + "en-US", + "it's time for homework", + "living room", + "broadcast to living room it's time for homework", + ), + ( + "es-ES", + "Es hora de hacer los deberes", + "el salón", + "Anuncia en el salón Es hora de hacer los deberes", + ), + ("ko-KR", "숙제할 시간이야", "거실", "숙제할 시간이야 라고 거실에 방송해 줘"), + ("ja-JP", "宿題の時間だよ", "リビング", "宿題の時間だよとリビングにブロードキャストして"), + ], + ids=["english", "spanish", "korean", "japanese"], +) async def test_broadcast_one_target( - hass: HomeAssistant, setup_integration: ComponentSetup + hass: HomeAssistant, + setup_integration: ComponentSetup, + language_code: str, + message: str, + target: str, + expected_command: str, ) -> None: """Test broadcast to one target.""" await setup_integration() - message = "time for dinner" - target = "basement" - expected_command = "broadcast to basement time for dinner" + entry = hass.config_entries.async_entries(DOMAIN)[0] + entry.options = {"language_code": language_code} + with patch( "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", - return_value=["text_response", None], + return_value=("text_response", None, b""), ) as mock_assist_call: await hass.services.async_call( notify.DOMAIN, @@ -66,7 +110,7 @@ async def test_broadcast_two_targets( expected_command2 = "broadcast to master bedroom time for dinner" with patch( "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", - return_value=["text_response", None], + return_value=("text_response", None, b""), ) as mock_assist_call: await hass.services.async_call( notify.DOMAIN, @@ -87,7 +131,7 @@ async def test_broadcast_empty_message( with patch( "homeassistant.components.google_assistant_sdk.helpers.TextAssistant.assist", - return_value=["text_response", None], + return_value=("text_response", None, b""), ) as mock_assist_call: await hass.services.async_call( notify.DOMAIN, @@ -98,30 +142,6 @@ async def test_broadcast_empty_message( mock_assist_call.assert_not_called() -async def test_broadcast_spanish( - hass: HomeAssistant, setup_integration: ComponentSetup -) -> None: - """Test broadcast in Spanish.""" - await setup_integration() - - entry = hass.config_entries.async_entries(DOMAIN)[0] - entry.options = {"language_code": "es-ES"} - - message = "comida" - expected_command = "Anuncia comida" - with patch( - "homeassistant.components.google_assistant_sdk.helpers.TextAssistant" - ) as mock_text_assistant: - await hass.services.async_call( - notify.DOMAIN, - DOMAIN, - {notify.ATTR_MESSAGE: message}, - ) - await hass.async_block_till_done() - mock_text_assistant.assert_called_once_with(ExpectedCredentials(), "es-ES") - mock_text_assistant.assert_has_calls([call().__enter__().assist(expected_command)]) - - def test_broadcast_language_mapping( hass: HomeAssistant, setup_integration: ComponentSetup ) -> None: @@ -131,4 +151,8 @@ def test_broadcast_language_mapping( assert cmds assert len(cmds) == 2 assert cmds[0] + assert "{0}" in cmds[0] + assert "{1}" not in cmds[0] assert cmds[1] + assert "{0}" in cmds[1] + assert "{1}" in cmds[1] diff --git a/tests/components/google_mail/__init__.py b/tests/components/google_mail/__init__.py new file mode 100644 index 00000000000..9a1a839fc59 --- /dev/null +++ b/tests/components/google_mail/__init__.py @@ -0,0 +1 @@ +"""Tests for the Google Mail integration.""" diff --git a/tests/components/google_mail/conftest.py b/tests/components/google_mail/conftest.py new file mode 100644 index 00000000000..d0b7afae786 --- /dev/null +++ b/tests/components/google_mail/conftest.py @@ -0,0 +1,119 @@ +"""Configure tests for the Google Mail integration.""" +from collections.abc import Awaitable, Callable, Generator +import time +from unittest.mock import patch + +from httplib2 import Response +from pytest import fixture + +from homeassistant.components.application_credentials import ( + ClientCredential, + async_import_client_credential, +) +from homeassistant.components.google_mail.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + +ComponentSetup = Callable[[], Awaitable[None]] + +BUILD = "homeassistant.components.google_mail.api.build" +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" +GOOGLE_AUTH_URI = "https://accounts.google.com/o/oauth2/v2/auth" +GOOGLE_TOKEN_URI = "https://oauth2.googleapis.com/token" +SCOPES = [ + "https://www.googleapis.com/auth/gmail.compose", + "https://www.googleapis.com/auth/gmail.settings.basic", +] +SENSOR = "sensor.example_gmail_com_vacation_end_date" +TITLE = "example@gmail.com" +TOKEN = "homeassistant.components.google_mail.api.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid" + + +@fixture(name="scopes") +def mock_scopes() -> list[str]: + """Fixture to set the scopes present in the OAuth token.""" + return SCOPES + + +@fixture(autouse=True) +async def setup_credentials(hass: HomeAssistant) -> None: + """Fixture to setup credentials.""" + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential(CLIENT_ID, CLIENT_SECRET), + DOMAIN, + ) + + +@fixture(name="expires_at") +def mock_expires_at() -> int: + """Fixture to set the oauth token expiration time.""" + return time.time() + 3600 + + +@fixture(name="config_entry") +def mock_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry: + """Create Google Mail entry in Home Assistant.""" + return MockConfigEntry( + domain=DOMAIN, + title=TITLE, + unique_id=TITLE, + data={ + "auth_implementation": DOMAIN, + "token": { + "access_token": "mock-access-token", + "refresh_token": "mock-refresh-token", + "expires_at": expires_at, + "scope": " ".join(scopes), + }, + }, + ) + + +@fixture(autouse=True) +def mock_connection(aioclient_mock: AiohttpClientMocker) -> None: + """Mock Google Mail connection.""" + aioclient_mock.post( + GOOGLE_TOKEN_URI, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + +@fixture(name="setup_integration") +async def mock_setup_integration( + hass: HomeAssistant, config_entry: MockConfigEntry +) -> Generator[ComponentSetup, None, None]: + """Fixture for setting up the component.""" + config_entry.add_to_hass(hass) + + assert await async_setup_component(hass, "application_credentials", {}) + await async_import_client_credential( + hass, + DOMAIN, + ClientCredential(CLIENT_ID, CLIENT_SECRET), + DOMAIN, + ) + + async def func() -> None: + with patch( + "httplib2.Http.request", + return_value=( + Response({}), + bytes(load_fixture("google_mail/get_vacation.json"), encoding="UTF-8"), + ), + ): + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + + yield func diff --git a/tests/components/google_mail/fixtures/get_profile.json b/tests/components/google_mail/fixtures/get_profile.json new file mode 100644 index 00000000000..20e58b2a518 --- /dev/null +++ b/tests/components/google_mail/fixtures/get_profile.json @@ -0,0 +1,6 @@ +{ + "emailAddress": "example@gmail.com", + "messagesTotal": 35308, + "threadsTotal": 33901, + "historyId": "4178212" +} diff --git a/tests/components/google_mail/fixtures/get_profile_2.json b/tests/components/google_mail/fixtures/get_profile_2.json new file mode 100644 index 00000000000..3b36d576183 --- /dev/null +++ b/tests/components/google_mail/fixtures/get_profile_2.json @@ -0,0 +1,6 @@ +{ + "emailAddress": "example2@gmail.com", + "messagesTotal": 35308, + "threadsTotal": 33901, + "historyId": "4178212" +} diff --git a/tests/components/google_mail/fixtures/get_vacation.json b/tests/components/google_mail/fixtures/get_vacation.json new file mode 100644 index 00000000000..734e108b1ae --- /dev/null +++ b/tests/components/google_mail/fixtures/get_vacation.json @@ -0,0 +1,8 @@ +{ + "enableAutoReply": true, + "responseSubject": "Vacation", + "responseBodyPlainText": "I am on vacation.", + "restrictToContacts": false, + "startTime": "1668402000000", + "endTime": "1668747600000" +} diff --git a/tests/components/google_mail/fixtures/get_vacation_off.json b/tests/components/google_mail/fixtures/get_vacation_off.json new file mode 100644 index 00000000000..cd3e4c9b96c --- /dev/null +++ b/tests/components/google_mail/fixtures/get_vacation_off.json @@ -0,0 +1,8 @@ +{ + "enableAutoReply": false, + "responseSubject": "Vacation", + "responseBodyPlainText": "I am on vacation.", + "restrictToContacts": false, + "startTime": "1668402000000", + "endTime": "1668747600000" +} diff --git a/tests/components/google_mail/test_config_flow.py b/tests/components/google_mail/test_config_flow.py new file mode 100644 index 00000000000..08f71368cc2 --- /dev/null +++ b/tests/components/google_mail/test_config_flow.py @@ -0,0 +1,208 @@ +"""Test the Google Mail config flow.""" +from unittest.mock import patch + +from httplib2 import Response +import pytest + +from homeassistant import config_entries +from homeassistant.components.google_mail.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow + +from .conftest import CLIENT_ID, GOOGLE_AUTH_URI, GOOGLE_TOKEN_URI, SCOPES, TITLE + +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_full_flow( + hass: HomeAssistant, + hass_client_no_auth, + current_request_with_host, +) -> None: + """Check full flow.""" + result = await hass.config_entries.flow.async_init( + "google_mail", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope={'+'.join(SCOPES)}" + "&access_type=offline&prompt=consent" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + with patch( + "homeassistant.components.google_mail.async_setup_entry", return_value=True + ) as mock_setup, patch( + "httplib2.Http.request", + return_value=( + Response({}), + bytes(load_fixture("google_mail/get_profile.json"), encoding="UTF-8"), + ), + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 + + assert result.get("type") == "create_entry" + assert result.get("title") == TITLE + assert "result" in result + assert result.get("result").unique_id == TITLE + assert "token" in result.get("result").data + assert result.get("result").data["token"].get("access_token") == "mock-access-token" + assert ( + result.get("result").data["token"].get("refresh_token") == "mock-refresh-token" + ) + + +@pytest.mark.parametrize( + "fixture,abort_reason,placeholders,calls,access_token", + [ + ("get_profile", "reauth_successful", None, 1, "updated-access-token"), + ( + "get_profile_2", + "wrong_account", + {"email": "example@gmail.com"}, + 0, + "mock-access-token", + ), + ], +) +async def test_reauth( + hass: HomeAssistant, + hass_client_no_auth, + aioclient_mock: AiohttpClientMocker, + current_request_with_host, + config_entry: MockConfigEntry, + fixture: str, + abort_reason: str, + placeholders: dict[str, str], + calls: int, + access_token: str, +) -> None: + """Test the re-authentication case updates the correct config entry. + + Make sure we abort if the user selects the + wrong account on the consent screen. + """ + config_entry.add_to_hass(hass) + + config_entry.async_start_reauth(hass) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + result = flows[0] + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + assert result["url"] == ( + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope={'+'.join(SCOPES)}" + "&access_type=offline&prompt=consent" + ) + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.clear_requests() + aioclient_mock.post( + GOOGLE_TOKEN_URI, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "updated-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.google_mail.async_setup_entry", return_value=True + ) as mock_setup, patch( + "httplib2.Http.request", + return_value=( + Response({}), + bytes(load_fixture(f"google_mail/{fixture}.json"), encoding="UTF-8"), + ), + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + assert result.get("type") == "abort" + assert result["reason"] == abort_reason + assert result["description_placeholders"] == placeholders + assert len(mock_setup.mock_calls) == calls + + assert config_entry.unique_id == TITLE + assert "token" in config_entry.data + # Verify access token is refreshed + assert config_entry.data["token"].get("access_token") == access_token + assert config_entry.data["token"].get("refresh_token") == "mock-refresh-token" + + +async def test_already_configured( + hass: HomeAssistant, + hass_client_no_auth, + current_request_with_host, + config_entry: MockConfigEntry, +) -> None: + """Test case where config flow discovers unique id was already configured.""" + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + "google_mail", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope={'+'.join(SCOPES)}" + "&access_type=offline&prompt=consent" + ) + + client = await hass_client_no_auth() + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + with patch( + "httplib2.Http.request", + return_value=( + Response({}), + bytes(load_fixture("google_mail/get_profile.json"), encoding="UTF-8"), + ), + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result.get("type") == "abort" + assert result.get("reason") == "already_configured" diff --git a/tests/components/google_mail/test_init.py b/tests/components/google_mail/test_init.py new file mode 100644 index 00000000000..239c7f9d51f --- /dev/null +++ b/tests/components/google_mail/test_init.py @@ -0,0 +1,131 @@ +"""Tests for Google Mail.""" +import http +import time +from unittest.mock import patch + +from aiohttp.client_exceptions import ClientError +import pytest + +from homeassistant.components.google_mail import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .conftest import GOOGLE_TOKEN_URI, ComponentSetup + +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_setup_success( + hass: HomeAssistant, setup_integration: ComponentSetup +) -> None: + """Test successful setup and unload.""" + await setup_integration() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(entries[0].entry_id) + await hass.async_block_till_done() + + assert not hass.services.async_services().get(DOMAIN) + + +@pytest.mark.parametrize("expires_at", [time.time() - 3600], ids=["expired"]) +async def test_expired_token_refresh_success( + hass: HomeAssistant, + setup_integration: ComponentSetup, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test expired token is refreshed.""" + + aioclient_mock.clear_requests() + aioclient_mock.post( + GOOGLE_TOKEN_URI, + json={ + "access_token": "updated-access-token", + "refresh_token": "updated-refresh-token", + "expires_at": time.time() + 3600, + "expires_in": 3600, + }, + ) + + await setup_integration() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + assert entries[0].data["token"]["access_token"] == "updated-access-token" + assert entries[0].data["token"]["expires_in"] == 3600 + + +@pytest.mark.parametrize( + "expires_at,status,expected_state", + [ + ( + time.time() - 3600, + http.HTTPStatus.UNAUTHORIZED, + ConfigEntryState.SETUP_ERROR, + ), + ( + time.time() - 3600, + http.HTTPStatus.INTERNAL_SERVER_ERROR, + ConfigEntryState.SETUP_RETRY, + ), + ], + ids=["failure_requires_reauth", "transient_failure"], +) +async def test_expired_token_refresh_failure( + hass: HomeAssistant, + setup_integration: ComponentSetup, + aioclient_mock: AiohttpClientMocker, + status: http.HTTPStatus, + expected_state: ConfigEntryState, +) -> None: + """Test failure while refreshing token with a transient error.""" + + aioclient_mock.clear_requests() + aioclient_mock.post( + GOOGLE_TOKEN_URI, + status=status, + ) + + await setup_integration() + + # Verify a transient failure has occurred + entries = hass.config_entries.async_entries(DOMAIN) + assert entries[0].state is expected_state + + +async def test_expired_token_refresh_client_error( + hass: HomeAssistant, + setup_integration: ComponentSetup, +) -> None: + """Test failure while refreshing token with a client error.""" + + with patch( + "homeassistant.components.google_mail.OAuth2Session.async_ensure_token_valid", + side_effect=ClientError, + ): + await setup_integration() + + # Verify a transient failure has occurred + entries = hass.config_entries.async_entries(DOMAIN) + assert entries[0].state is ConfigEntryState.SETUP_RETRY + + +async def test_device_info( + hass: HomeAssistant, setup_integration: ComponentSetup +) -> None: + """Test device info.""" + await setup_integration() + device_registry = dr.async_get(hass) + + entry = hass.config_entries.async_entries(DOMAIN)[0] + device = device_registry.async_get_device({(DOMAIN, entry.entry_id)}) + + assert device.entry_type is dr.DeviceEntryType.SERVICE + assert device.identifiers == {(DOMAIN, entry.entry_id)} + assert device.manufacturer == "Google, Inc." + assert device.name == "example@gmail.com" diff --git a/tests/components/google_mail/test_notify.py b/tests/components/google_mail/test_notify.py new file mode 100644 index 00000000000..1e9a174d81f --- /dev/null +++ b/tests/components/google_mail/test_notify.py @@ -0,0 +1,76 @@ +"""Notify tests for the Google Mail integration.""" +from unittest.mock import patch + +import pytest +from voluptuous.error import Invalid + +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.core import HomeAssistant + +from .conftest import BUILD, ComponentSetup + + +async def test_notify( + hass: HomeAssistant, + setup_integration: ComponentSetup, +) -> None: + """Test service call draft email.""" + await setup_integration() + + with patch(BUILD) as mock_client: + await hass.services.async_call( + NOTIFY_DOMAIN, + "example_gmail_com", + { + "title": "Test", + "message": "test email", + "target": "text@example.com", + }, + blocking=True, + ) + assert len(mock_client.mock_calls) == 5 + + with patch(BUILD) as mock_client: + await hass.services.async_call( + NOTIFY_DOMAIN, + "example_gmail_com", + { + "title": "Test", + "message": "test email", + "target": "text@example.com", + "data": {"send": False}, + }, + blocking=True, + ) + assert len(mock_client.mock_calls) == 5 + + +async def test_notify_voluptuous_error( + hass: HomeAssistant, + setup_integration: ComponentSetup, +) -> None: + """Test voluptuous error thrown when drafting email.""" + await setup_integration() + + with pytest.raises(ValueError) as ex: + await hass.services.async_call( + NOTIFY_DOMAIN, + "example_gmail_com", + { + "title": "Test", + "message": "test email", + }, + blocking=True, + ) + assert ex.match("recipient address required") + + with pytest.raises(Invalid) as ex: + await hass.services.async_call( + NOTIFY_DOMAIN, + "example_gmail_com", + { + "title": "Test", + }, + blocking=True, + ) + assert ex.getrepr("required key not provided") diff --git a/tests/components/google_mail/test_sensor.py b/tests/components/google_mail/test_sensor.py new file mode 100644 index 00000000000..369557ad3e9 --- /dev/null +++ b/tests/components/google_mail/test_sensor.py @@ -0,0 +1,60 @@ +"""Sensor tests for the Google Mail integration.""" +from datetime import timedelta +from unittest.mock import patch + +from google.auth.exceptions import RefreshError +from httplib2 import Response + +from homeassistant import config_entries +from homeassistant.components.google_mail.const import DOMAIN +from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +import homeassistant.util.dt as dt_util + +from .conftest import SENSOR, TOKEN, ComponentSetup + +from tests.common import async_fire_time_changed, load_fixture + + +async def test_sensors(hass: HomeAssistant, setup_integration: ComponentSetup) -> None: + """Test we get sensor data.""" + await setup_integration() + + state = hass.states.get(SENSOR) + assert state.state == "2022-11-18T05:00:00+00:00" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP + + with patch( + "httplib2.Http.request", + return_value=( + Response({}), + bytes(load_fixture("google_mail/get_vacation_off.json"), encoding="UTF-8"), + ), + ): + next_update = dt_util.utcnow() + timedelta(minutes=15) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(SENSOR) + assert state.state == STATE_UNKNOWN + + +async def test_sensor_reauth_trigger( + hass: HomeAssistant, setup_integration: ComponentSetup +) -> None: + """Test reauth is triggered after a refresh error.""" + await setup_integration() + + with patch(TOKEN, side_effect=RefreshError): + next_update = dt_util.utcnow() + timedelta(minutes=15) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + flows = hass.config_entries.flow.async_progress() + + assert len(flows) == 1 + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + assert flow["context"]["source"] == config_entries.SOURCE_REAUTH diff --git a/tests/components/google_mail/test_services.py b/tests/components/google_mail/test_services.py new file mode 100644 index 00000000000..2523e7a9591 --- /dev/null +++ b/tests/components/google_mail/test_services.py @@ -0,0 +1,90 @@ +"""Services tests for the Google Mail integration.""" +from unittest.mock import patch + +from google.auth.exceptions import RefreshError +import pytest + +from homeassistant import config_entries +from homeassistant.components.google_mail import DOMAIN +from homeassistant.core import HomeAssistant + +from .conftest import BUILD, SENSOR, TOKEN, ComponentSetup + + +async def test_set_vacation( + hass: HomeAssistant, + setup_integration: ComponentSetup, +) -> None: + """Test service call set vacation.""" + await setup_integration() + + with patch(BUILD) as mock_client: + await hass.services.async_call( + DOMAIN, + "set_vacation", + { + "entity_id": SENSOR, + "enabled": True, + "title": "Vacation", + "message": "Vacation message", + "plain_text": False, + "restrict_contacts": True, + "restrict_domain": True, + "start": "2022-11-20", + "end": "2022-11-26", + }, + blocking=True, + ) + assert len(mock_client.mock_calls) == 5 + + with patch(BUILD) as mock_client: + await hass.services.async_call( + DOMAIN, + "set_vacation", + { + "entity_id": SENSOR, + "enabled": True, + "title": "Vacation", + "message": "Vacation message", + "plain_text": True, + "restrict_contacts": True, + "restrict_domain": True, + "start": "2022-11-20", + "end": "2022-11-26", + }, + blocking=True, + ) + assert len(mock_client.mock_calls) == 5 + + +async def test_reauth_trigger( + hass: HomeAssistant, setup_integration: ComponentSetup +) -> None: + """Test reauth is triggered after a refresh error during service call.""" + await setup_integration() + + with patch(TOKEN, side_effect=RefreshError), pytest.raises(RefreshError): + await hass.services.async_call( + DOMAIN, + "set_vacation", + { + "entity_id": SENSOR, + "enabled": True, + "title": "Vacation", + "message": "Vacation message", + "plain_text": True, + "restrict_contacts": True, + "restrict_domain": True, + "start": "2022-11-20", + "end": "2022-11-26", + }, + blocking=True, + ) + + flows = hass.config_entries.flow.async_progress() + + assert len(flows) == 1 + flow = flows[0] + assert flow["step_id"] == "reauth_confirm" + assert flow["handler"] == DOMAIN + assert flow["context"]["source"] == config_entries.SOURCE_REAUTH diff --git a/tests/components/google_wifi/test_sensor.py b/tests/components/google_wifi/test_sensor.py index ae0715c640b..30493a6011b 100644 --- a/tests/components/google_wifi/test_sensor.py +++ b/tests/components/google_wifi/test_sensor.py @@ -4,7 +4,6 @@ from http import HTTPStatus from unittest.mock import Mock, patch import homeassistant.components.google_wifi.sensor as google_wifi -from homeassistant.const import STATE_UNKNOWN from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -171,7 +170,7 @@ def test_update_when_value_changed(hass, requests_mock): elif name == google_wifi.ATTR_NEW_VERSION: assert sensor.state == "Latest" elif name == google_wifi.ATTR_LOCAL_IP: - assert sensor.state == STATE_UNKNOWN + assert sensor.state is None else: assert sensor.state == "next" @@ -185,7 +184,7 @@ def test_when_api_data_missing(hass, requests_mock): sensor = sensor_dict[name]["sensor"] fake_delay(hass, 2) sensor.update() - assert sensor.state == STATE_UNKNOWN + assert sensor.state is None def test_update_when_unavailable(hass, requests_mock): diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index 77357065a1a..8e6ec12c464 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -17,8 +17,6 @@ from homeassistant.setup import async_setup_component HOME_LATITUDE = 37.239622 HOME_LONGITUDE = -115.815811 -# pylint: disable=redefined-outer-name - @pytest.fixture(autouse=True) def mock_dev_track(mock_device_tracker_conf): diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index de873726c44..d3cf418600c 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -44,8 +44,7 @@ from homeassistant.const import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_UNAVAILABLE, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) import homeassistant.util.dt as dt_util @@ -360,14 +359,15 @@ async def test_send_power_off_device_timeout(hass, discovery, device, mock_now): @pytest.mark.parametrize( - "units,temperature", [(TEMP_CELSIUS, 26), (TEMP_FAHRENHEIT, 74)] + "units,temperature", + [(UnitOfTemperature.CELSIUS, 26), (UnitOfTemperature.FAHRENHEIT, 74)], ) async def test_send_target_temperature(hass, discovery, device, units, temperature): """Test for sending target temperature command to the device.""" hass.config.units.temperature_unit = units fake_device = device() - if units == TEMP_FAHRENHEIT: + if units == UnitOfTemperature.FAHRENHEIT: fake_device.temperature_units = 1 await async_setup_gree(hass) @@ -392,18 +392,19 @@ async def test_send_target_temperature(hass, discovery, device, units, temperatu # Reset config temperature_unit back to CELSIUS, required for # additional tests outside this component. - hass.config.units.temperature_unit = TEMP_CELSIUS + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS @pytest.mark.parametrize( - "units,temperature", [(TEMP_CELSIUS, 25), (TEMP_FAHRENHEIT, 74)] + "units,temperature", + [(UnitOfTemperature.CELSIUS, 25), (UnitOfTemperature.FAHRENHEIT, 74)], ) async def test_send_target_temperature_device_timeout( hass, discovery, device, units, temperature ): """Test for sending target temperature command to the device with a device timeout.""" hass.config.units.temperature_unit = units - if units == TEMP_FAHRENHEIT: + if units == UnitOfTemperature.FAHRENHEIT: device().temperature_units = 1 device().push_state_update.side_effect = DeviceTimeoutError @@ -421,16 +422,17 @@ async def test_send_target_temperature_device_timeout( assert state.attributes.get(ATTR_TEMPERATURE) == temperature # Reset config temperature_unit back to CELSIUS, required for additional tests outside this component. - hass.config.units.temperature_unit = TEMP_CELSIUS + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS @pytest.mark.parametrize( - "units,temperature", [(TEMP_CELSIUS, 25), (TEMP_FAHRENHEIT, 74)] + "units,temperature", + [(UnitOfTemperature.CELSIUS, 25), (UnitOfTemperature.FAHRENHEIT, 74)], ) async def test_update_target_temperature(hass, discovery, device, units, temperature): """Test for updating target temperature from the device.""" hass.config.units.temperature_unit = units - if units == TEMP_FAHRENHEIT: + if units == UnitOfTemperature.FAHRENHEIT: device().temperature_units = 1 device().target_temperature = temperature @@ -441,7 +443,7 @@ async def test_update_target_temperature(hass, discovery, device, units, tempera assert state.attributes.get(ATTR_TEMPERATURE) == temperature # Reset config temperature_unit back to CELSIUS, required for additional tests outside this component. - hass.config.units.temperature_unit = TEMP_CELSIUS + hass.config.units.temperature_unit = UnitOfTemperature.CELSIUS @pytest.mark.parametrize( diff --git a/tests/components/greeneye_monitor/conftest.py b/tests/components/greeneye_monitor/conftest.py index 00b534bb06d..eabac97d1cc 100644 --- a/tests/components/greeneye_monitor/conftest.py +++ b/tests/components/greeneye_monitor/conftest.py @@ -6,7 +6,7 @@ import pytest from homeassistant.components.greeneye_monitor import DOMAIN from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.const import ELECTRIC_POTENTIAL_VOLT, POWER_WATT +from homeassistant.const import UnitOfElectricPotential, UnitOfPower from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import ( RegistryEntry, @@ -61,7 +61,7 @@ def assert_power_sensor_registered( ) -> None: """Assert that a power sensor entity was registered properly.""" sensor = assert_sensor_registered(hass, serial_number, "current", number, name) - assert sensor.unit_of_measurement == POWER_WATT + assert sensor.unit_of_measurement == UnitOfPower.WATT assert sensor.original_device_class is SensorDeviceClass.POWER @@ -70,7 +70,7 @@ def assert_voltage_sensor_registered( ) -> None: """Assert that a voltage sensor entity was registered properly.""" sensor = assert_sensor_registered(hass, serial_number, "volts", number, name) - assert sensor.unit_of_measurement == ELECTRIC_POTENTIAL_VOLT + assert sensor.unit_of_measurement == UnitOfElectricPotential.VOLT assert sensor.original_device_class is SensorDeviceClass.VOLTAGE diff --git a/tests/components/group/fixtures/sensor_configuration.yaml b/tests/components/group/fixtures/sensor_configuration.yaml new file mode 100644 index 00000000000..1415124f275 --- /dev/null +++ b/tests/components/group/fixtures/sensor_configuration.yaml @@ -0,0 +1,7 @@ +sensor: + - platform: group + entities: + - sensor.test_1 + - sensor.test_2 + name: second_test + type: mean diff --git a/tests/components/group/test_config_flow.py b/tests/components/group/test_config_flow.py index 4c73e1d5add..7fbd22b2dc9 100644 --- a/tests/components/group/test_config_flow.py +++ b/tests/components/group/test_config_flow.py @@ -22,6 +22,15 @@ from tests.common import MockConfigEntry ("light", "on", "on", {}, {}, {}, {}), ("lock", "locked", "locked", {}, {}, {}, {}), ("media_player", "on", "on", {}, {}, {}, {}), + ( + "sensor", + "20.0", + "10", + {}, + {"type": "sum"}, + {"type": "sum"}, + {}, + ), ("switch", "on", "on", {}, {}, {}, {}), ), ) @@ -171,19 +180,25 @@ def get_suggested(schema, key): @pytest.mark.parametrize( - "group_type,member_state,extra_options", + "group_type,member_state,extra_options,options_options", ( - ("binary_sensor", "on", {"all": False}), - ("cover", "open", {}), - ("fan", "on", {}), - ("light", "on", {"all": False}), - ("lock", "locked", {}), - ("media_player", "on", {}), - ("switch", "on", {"all": False}), + ("binary_sensor", "on", {"all": False}, {}), + ("cover", "open", {}, {}), + ("fan", "on", {}, {}), + ("light", "on", {"all": False}, {}), + ("lock", "locked", {}, {}), + ("media_player", "on", {}, {}), + ( + "sensor", + "10", + {"ignore_non_numeric": False, "type": "sum"}, + {"ignore_non_numeric": False, "type": "sum"}, + ), + ("switch", "on", {"all": False}, {}), ), ) async def test_options( - hass: HomeAssistant, group_type, member_state, extra_options + hass: HomeAssistant, group_type, member_state, extra_options, options_options ) -> None: """Test reconfiguring.""" members1 = [f"{group_type}.one", f"{group_type}.two"] @@ -226,9 +241,7 @@ async def test_options( result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={ - "entities": members2, - }, + user_input={"entities": members2, **options_options}, ) assert result["type"] == FlowResultType.CREATE_ENTRY assert result["data"] == { diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index ec5732503e8..594a20a8154 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -1,5 +1,5 @@ """The tests for the Group components.""" -# pylint: disable=protected-access + from __future__ import annotations from collections import OrderedDict @@ -549,6 +549,62 @@ async def test_service_group_services(hass): assert hass.services.has_service("group", group.SERVICE_REMOVE) +async def test_service_group_services_add_remove_entities(hass: HomeAssistant) -> None: + """Check if we can add and remove entities from group.""" + + hass.states.async_set("person.one", "Work") + hass.states.async_set("person.two", "Work") + hass.states.async_set("person.three", "home") + + assert await async_setup_component(hass, "person", {}) + with assert_setup_component(0, "group"): + await async_setup_component(hass, "group", {"group": {}}) + + assert hass.services.has_service("group", group.SERVICE_SET) + + await hass.services.async_call( + group.DOMAIN, + group.SERVICE_SET, + { + "object_id": "new_group", + "name": "New Group", + "entities": ["person.one", "person.two"], + }, + ) + await hass.async_block_till_done() + + group_state = hass.states.get("group.new_group") + assert group_state.state == "not_home" + assert group_state.attributes["friendly_name"] == "New Group" + assert list(group_state.attributes["entity_id"]) == ["person.one", "person.two"] + + await hass.services.async_call( + group.DOMAIN, + group.SERVICE_SET, + { + "object_id": "new_group", + "add_entities": "person.three", + }, + ) + await hass.async_block_till_done() + group_state = hass.states.get("group.new_group") + assert group_state.state == "home" + assert "person.three" in list(group_state.attributes["entity_id"]) + + await hass.services.async_call( + group.DOMAIN, + group.SERVICE_SET, + { + "object_id": "new_group", + "remove_entities": "person.one", + }, + ) + await hass.async_block_till_done() + group_state = hass.states.get("group.new_group") + assert group_state.state == "home" + assert "person.one" not in list(group_state.attributes["entity_id"]) + + # pylint: disable=invalid-name async def test_service_group_set_group_remove_group(hass): """Check if service are available.""" @@ -1373,6 +1429,16 @@ async def test_plant_group(hass): ("fan", "on", {}), ("light", "on", {"all": False}), ("media_player", "on", {}), + ( + "sensor", + "1", + { + "all": True, + "type": "max", + "round_digits": 2.0, + "state_class": "measurement", + }, + ), ), ) async def test_setup_and_remove_config_entry( diff --git a/tests/components/group/test_lock.py b/tests/components/group/test_lock.py index 4b12bcfbd7c..16dc3797daf 100644 --- a/tests/components/group/test_lock.py +++ b/tests/components/group/test_lock.py @@ -1,6 +1,9 @@ """The tests for the Group Lock platform.""" + from unittest.mock import patch +import pytest + from homeassistant import config as hass_config from homeassistant.components.demo import lock as demo_lock from homeassistant.components.group import DOMAIN, SERVICE_RELOAD @@ -20,6 +23,8 @@ from homeassistant.const import ( STATE_UNLOCKED, STATE_UNLOCKING, ) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -167,20 +172,19 @@ async def test_state_reporting(hass): assert hass.states.get("lock.lock_group").state == STATE_UNAVAILABLE -@patch.object(demo_lock, "LOCK_UNLOCK_DELAY", 0) -async def test_service_calls(hass, enable_custom_integrations): - """Test service calls.""" +async def test_service_calls_openable(hass: HomeAssistant) -> None: + """Test service calls with open support.""" await async_setup_component( hass, LOCK_DOMAIN, { LOCK_DOMAIN: [ - {"platform": "demo"}, + {"platform": "kitchen_sink"}, { "platform": DOMAIN, "entities": [ - "lock.front_door", - "lock.kitchen_door", + "lock.openable_lock", + "lock.another_openable_lock", ], }, ] @@ -190,8 +194,8 @@ async def test_service_calls(hass, enable_custom_integrations): group_state = hass.states.get("lock.lock_group") assert group_state.state == STATE_UNLOCKED - assert hass.states.get("lock.front_door").state == STATE_LOCKED - assert hass.states.get("lock.kitchen_door").state == STATE_UNLOCKED + assert hass.states.get("lock.openable_lock").state == STATE_LOCKED + assert hass.states.get("lock.another_openable_lock").state == STATE_UNLOCKED await hass.services.async_call( LOCK_DOMAIN, @@ -199,8 +203,8 @@ async def test_service_calls(hass, enable_custom_integrations): {ATTR_ENTITY_ID: "lock.lock_group"}, blocking=True, ) - assert hass.states.get("lock.front_door").state == STATE_UNLOCKED - assert hass.states.get("lock.kitchen_door").state == STATE_UNLOCKED + assert hass.states.get("lock.openable_lock").state == STATE_UNLOCKED + assert hass.states.get("lock.another_openable_lock").state == STATE_UNLOCKED await hass.services.async_call( LOCK_DOMAIN, @@ -208,8 +212,8 @@ async def test_service_calls(hass, enable_custom_integrations): {ATTR_ENTITY_ID: "lock.lock_group"}, blocking=True, ) - assert hass.states.get("lock.front_door").state == STATE_LOCKED - assert hass.states.get("lock.kitchen_door").state == STATE_LOCKED + assert hass.states.get("lock.openable_lock").state == STATE_LOCKED + assert hass.states.get("lock.another_openable_lock").state == STATE_LOCKED await hass.services.async_call( LOCK_DOMAIN, @@ -217,8 +221,60 @@ async def test_service_calls(hass, enable_custom_integrations): {ATTR_ENTITY_ID: "lock.lock_group"}, blocking=True, ) - assert hass.states.get("lock.front_door").state == STATE_UNLOCKED - assert hass.states.get("lock.kitchen_door").state == STATE_UNLOCKED + assert hass.states.get("lock.openable_lock").state == STATE_UNLOCKED + assert hass.states.get("lock.another_openable_lock").state == STATE_UNLOCKED + + +async def test_service_calls_basic(hass: HomeAssistant) -> None: + """Test service calls without open support.""" + await async_setup_component( + hass, + LOCK_DOMAIN, + { + LOCK_DOMAIN: [ + {"platform": "kitchen_sink"}, + { + "platform": DOMAIN, + "entities": [ + "lock.basic_lock", + "lock.another_basic_lock", + ], + }, + ] + }, + ) + await hass.async_block_till_done() + + group_state = hass.states.get("lock.lock_group") + assert group_state.state == STATE_UNLOCKED + assert hass.states.get("lock.basic_lock").state == STATE_LOCKED + assert hass.states.get("lock.another_basic_lock").state == STATE_UNLOCKED + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_LOCK, + {ATTR_ENTITY_ID: "lock.lock_group"}, + blocking=True, + ) + assert hass.states.get("lock.basic_lock").state == STATE_LOCKED + assert hass.states.get("lock.another_basic_lock").state == STATE_LOCKED + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_UNLOCK, + {ATTR_ENTITY_ID: "lock.lock_group"}, + blocking=True, + ) + assert hass.states.get("lock.basic_lock").state == STATE_UNLOCKED + assert hass.states.get("lock.another_basic_lock").state == STATE_UNLOCKED + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_OPEN, + {ATTR_ENTITY_ID: "lock.lock_group"}, + blocking=True, + ) async def test_reload(hass): diff --git a/tests/components/group/test_sensor.py b/tests/components/group/test_sensor.py new file mode 100644 index 00000000000..265ee90534a --- /dev/null +++ b/tests/components/group/test_sensor.py @@ -0,0 +1,369 @@ +"""The tests for the Group Sensor platform.""" +from __future__ import annotations + +import statistics +from typing import Any +from unittest.mock import patch + +import pytest +from pytest import LogCaptureFixture + +from homeassistant import config as hass_config +from homeassistant.components.group import DOMAIN as GROUP_DOMAIN +from homeassistant.components.group.sensor import ( + ATTR_LAST_ENTITY_ID, + ATTR_MAX_ENTITY_ID, + ATTR_MIN_ENTITY_ID, + DEFAULT_NAME, +) +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_UNIT_OF_MEASUREMENT, + SERVICE_RELOAD, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant +import homeassistant.helpers.entity_registry as er +from homeassistant.setup import async_setup_component + +from tests.common import get_fixture_path + +VALUES = [17, 20, 15.3] +VALUES_ERROR = [17, "string", 15.3] +COUNT = len(VALUES) +MIN_VALUE = min(VALUES) +MAX_VALUE = max(VALUES) +MEAN = statistics.mean(VALUES) +MEDIAN = statistics.median(VALUES) +RANGE = max(VALUES) - min(VALUES) +SUM_VALUE = sum(VALUES) + + +@pytest.mark.parametrize( + "sensor_type, result, attributes", + [ + ("min", MIN_VALUE, {ATTR_MIN_ENTITY_ID: "sensor.test_3"}), + ("max", MAX_VALUE, {ATTR_MAX_ENTITY_ID: "sensor.test_2"}), + ("mean", MEAN, {}), + ("median", MEDIAN, {}), + ("last", VALUES[2], {ATTR_LAST_ENTITY_ID: "sensor.test_3"}), + ("range", RANGE, {}), + ("sum", SUM_VALUE, {}), + ], +) +async def test_sensors( + hass: HomeAssistant, + sensor_type: str, + result: str, + attributes: dict[str, Any], +) -> None: + """Test the sensors.""" + config = { + SENSOR_DOMAIN: { + "platform": GROUP_DOMAIN, + "name": DEFAULT_NAME, + "type": sensor_type, + "entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "unique_id": "very_unique_id", + } + } + + entity_ids = config["sensor"]["entities"] + + for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + hass.states.async_set( + entity_id, + value, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.VOLUME, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: "L", + }, + ) + await hass.async_block_till_done() + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + state = hass.states.get(f"sensor.sensor_group_{sensor_type}") + + assert float(state.state) == pytest.approx(float(result)) + assert state.attributes.get(ATTR_ENTITY_ID) == entity_ids + for key, value in attributes.items(): + assert state.attributes.get(key) == value + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLUME + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "L" + + entity_reg = er.async_get(hass) + entity = entity_reg.async_get(f"sensor.sensor_group_{sensor_type}") + assert entity.unique_id == "very_unique_id" + + +async def test_sensors_attributes_defined(hass: HomeAssistant) -> None: + """Test the sensors.""" + config = { + SENSOR_DOMAIN: { + "platform": GROUP_DOMAIN, + "name": DEFAULT_NAME, + "type": "sum", + "entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "unique_id": "very_unique_id", + "device_class": SensorDeviceClass.WATER, + "state_class": SensorStateClass.TOTAL_INCREASING, + "unit_of_measurement": "m³", + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entities"] + + for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + hass.states.async_set( + entity_id, + value, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.VOLUME, + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIT_OF_MEASUREMENT: "L", + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.sensor_group_sum") + + assert state.state == str(float(SUM_VALUE)) + assert state.attributes.get(ATTR_ENTITY_ID) == entity_ids + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "m³" + + +async def test_not_enough_sensor_value(hass: HomeAssistant) -> None: + """Test that there is nothing done if not enough values available.""" + config = { + SENSOR_DOMAIN: { + "platform": GROUP_DOMAIN, + "name": "test_max", + "type": "max", + "ignore_non_numeric": True, + "entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "state_class": SensorStateClass.MEASUREMENT, + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entities"] + + hass.states.async_set(entity_ids[0], STATE_UNKNOWN) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_max") + assert state.state == STATE_UNAVAILABLE + assert state.attributes.get("min_entity_id") is None + assert state.attributes.get("max_entity_id") is None + + hass.states.async_set(entity_ids[1], VALUES[1]) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_max") + assert state.state not in [STATE_UNAVAILABLE, STATE_UNKNOWN] + assert entity_ids[1] == state.attributes.get("max_entity_id") + + hass.states.async_set(entity_ids[2], STATE_UNKNOWN) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_max") + assert state.state not in [STATE_UNAVAILABLE, STATE_UNKNOWN] + assert entity_ids[1] == state.attributes.get("max_entity_id") + + hass.states.async_set(entity_ids[1], STATE_UNAVAILABLE) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_max") + assert state.state == STATE_UNAVAILABLE + assert state.attributes.get("min_entity_id") is None + assert state.attributes.get("max_entity_id") is None + + +async def test_reload(hass: HomeAssistant) -> None: + """Verify we can reload sensors.""" + hass.states.async_set("sensor.test_1", 12345) + hass.states.async_set("sensor.test_2", 45678) + + await async_setup_component( + hass, + "sensor", + { + SENSOR_DOMAIN: { + "platform": GROUP_DOMAIN, + "name": "test_sensor", + "type": "mean", + "entities": ["sensor.test_1", "sensor.test_2"], + "state_class": SensorStateClass.MEASUREMENT, + } + }, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 3 + + assert hass.states.get("sensor.test_sensor") + + yaml_path = get_fixture_path("sensor_configuration.yaml", "group") + + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + GROUP_DOMAIN, + SERVICE_RELOAD, + {}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 3 + + assert hass.states.get("sensor.test_sensor") is None + assert hass.states.get("sensor.second_test") + + +async def test_sensor_incorrect_state( + hass: HomeAssistant, caplog: LogCaptureFixture +) -> None: + """Test the min sensor.""" + config = { + SENSOR_DOMAIN: { + "platform": GROUP_DOMAIN, + "name": "test_failure", + "type": "min", + "ignore_non_numeric": True, + "entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "unique_id": "very_unique_id", + "state_class": SensorStateClass.MEASUREMENT, + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entities"] + + for entity_id, value in dict(zip(entity_ids, VALUES_ERROR)).items(): + hass.states.async_set(entity_id, value) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_failure") + + assert state.state == "15.3" + assert ( + "Unable to use state. Only numerical states are supported, entity sensor.test_2 with value string excluded from calculation" + in caplog.text + ) + + for entity_id, value in dict(zip(entity_ids, VALUES)).items(): + hass.states.async_set(entity_id, value) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_failure") + assert state.state == "15.3" + + +async def test_sensor_require_all_states(hass: HomeAssistant) -> None: + """Test the sum sensor with missing state require all.""" + config = { + SENSOR_DOMAIN: { + "platform": GROUP_DOMAIN, + "name": "test_sum", + "type": "sum", + "ignore_non_numeric": False, + "entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "unique_id": "very_unique_id_sum_sensor", + "state_class": SensorStateClass.MEASUREMENT, + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entities"] + + for entity_id, value in dict(zip(entity_ids, VALUES_ERROR)).items(): + hass.states.async_set(entity_id, value) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_sum") + + assert state.state == STATE_UNKNOWN + + +async def test_sensor_calculated_properties(hass: HomeAssistant) -> None: + """Test the sensor calculating device_class, state_class and unit of measurement.""" + config = { + SENSOR_DOMAIN: { + "platform": GROUP_DOMAIN, + "name": "test_sum", + "type": "sum", + "entities": ["sensor.test_1", "sensor.test_2", "sensor.test_3"], + "unique_id": "very_unique_id_sum_sensor", + } + } + + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + entity_ids = config["sensor"]["entities"] + + hass.states.async_set( + entity_ids[0], + VALUES[0], + { + "device_class": SensorDeviceClass.ENERGY, + "state_class": SensorStateClass.MEASUREMENT, + "unit_of_measurement": "kWh", + }, + ) + hass.states.async_set( + entity_ids[1], + VALUES[1], + { + "device_class": SensorDeviceClass.ENERGY, + "state_class": SensorStateClass.MEASUREMENT, + "unit_of_measurement": "kWh", + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_sum") + assert state.state == str(float(sum([VALUES[0], VALUES[1]]))) + assert state.attributes.get("device_class") == "energy" + assert state.attributes.get("state_class") == "measurement" + assert state.attributes.get("unit_of_measurement") == "kWh" + + hass.states.async_set( + entity_ids[2], + VALUES[2], + { + "device_class": SensorDeviceClass.BATTERY, + "state_class": SensorStateClass.TOTAL, + "unit_of_measurement": None, + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_sum") + assert state.state == str(sum(VALUES)) + assert state.attributes.get("device_class") is None + assert state.attributes.get("state_class") is None + assert state.attributes.get("unit_of_measurement") is None diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 371398e32c9..58e4fc1552d 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -202,8 +202,9 @@ async def test_setup_api_ping(hass, aioclient_mock): """Test setup with API ping.""" with patch.dict(os.environ, MOCK_ENVIRON): result = await async_setup_component(hass, "hassio", {}) - assert result + await hass.async_block_till_done() + assert result assert aioclient_mock.call_count == 16 assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0" assert hass.components.hassio.is_hassio() @@ -241,8 +242,9 @@ async def test_setup_api_push_api_data(hass, aioclient_mock): result = await async_setup_component( hass, "hassio", {"http": {"server_port": 9999}, "hassio": {}} ) - assert result + await hass.async_block_till_done() + assert result assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 @@ -257,8 +259,9 @@ async def test_setup_api_push_api_data_server_host(hass, aioclient_mock): "hassio", {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, ) - assert result + await hass.async_block_till_done() + assert result assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 @@ -269,8 +272,9 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag """Test setup with API push default data.""" with patch.dict(os.environ, MOCK_ENVIRON): result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) - assert result + await hass.async_block_till_done() + assert result assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 @@ -336,8 +340,9 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage hass_storage[STORAGE_KEY] = {"version": 1, "data": {"hassio_user": user.id}} with patch.dict(os.environ, MOCK_ENVIRON): result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) - assert result + await hass.async_block_till_done() + assert result assert aioclient_mock.call_count == 16 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 @@ -350,8 +355,9 @@ async def test_setup_core_push_timezone(hass, aioclient_mock): with patch.dict(os.environ, MOCK_ENVIRON): result = await async_setup_component(hass, "hassio", {"hassio": {}}) - assert result + await hass.async_block_till_done() + assert result assert aioclient_mock.call_count == 16 assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone" @@ -367,8 +373,9 @@ async def test_setup_hassio_no_additional_data(hass, aioclient_mock): os.environ, {"SUPERVISOR_TOKEN": "123456"} ): result = await async_setup_component(hass, "hassio", {"hassio": {}}) - assert result + await hass.async_block_till_done() + assert result assert aioclient_mock.call_count == 16 assert aioclient_mock.mock_calls[-1][3]["Authorization"] == "Bearer 123456" @@ -768,9 +775,9 @@ async def test_setup_hardware_integration(hass, aioclient_mock, integration): return_value=True, ) as mock_setup_entry: result = await async_setup_component(hass, "hassio", {"hassio": {}}) - assert result await hass.async_block_till_done() + assert result assert aioclient_mock.call_count == 16 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/hassio/test_update.py b/tests/components/hassio/test_update.py index 02d6b1dbf6b..8391ea66b5d 100644 --- a/tests/components/hassio/test_update.py +++ b/tests/components/hassio/test_update.py @@ -542,8 +542,8 @@ async def test_setting_up_core_update_when_addon_fails(hass, caplog): "hassio", {"http": {"server_port": 9999, "server_host": "127.0.0.1"}, "hassio": {}}, ) - assert result - await hass.async_block_till_done() + await hass.async_block_till_done() + assert result # Verify that the core update entity does exist state = hass.states.get("update.home_assistant_core_update") diff --git a/tests/components/hddtemp/test_sensor.py b/tests/components/hddtemp/test_sensor.py index 7c7ac5ef1e8..c95c6af50f4 100644 --- a/tests/components/hddtemp/test_sensor.py +++ b/tests/components/hddtemp/test_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import UnitOfTemperature from homeassistant.setup import async_setup_component VALID_CONFIG_MINIMAL = {"sensor": {"platform": "hddtemp"}} @@ -31,25 +31,25 @@ REFERENCE = { "/dev/sda1": { "device": "/dev/sda1", "temperature": "29", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, "model": "WDC WD30EZRX-12DC0B0", }, "/dev/sdb1": { "device": "/dev/sdb1", "temperature": "32", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, "model": "WDC WD15EADS-11P7B2", }, "/dev/sdc1": { "device": "/dev/sdc1", "temperature": "29", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, "model": "WDC WD20EARX-22MMMB0", }, "/dev/sdd1": { "device": "/dev/sdd1", "temperature": "32", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, "model": "WDC WD15EARS-00Z5B1", }, } diff --git a/tests/components/here_travel_time/conftest.py b/tests/components/here_travel_time/conftest.py index 8069583df76..dff91a4e1fb 100644 --- a/tests/components/here_travel_time/conftest.py +++ b/tests/components/here_travel_time/conftest.py @@ -13,6 +13,7 @@ TRANSIT_RESPONSE = json.loads( NO_ATTRIBUTION_TRANSIT_RESPONSE = json.loads( load_fixture("here_travel_time/no_attribution_transit_route_response.json") ) +BIKE_RESPONSE = json.loads(load_fixture("here_travel_time/bike_response.json")) @pytest.fixture(name="valid_response") @@ -27,6 +28,18 @@ def valid_response_fixture(): yield mock +@pytest.fixture(name="bike_response") +def bike_response_fixture(): + """Return valid api response.""" + with patch( + "here_transit.HERETransitApi.route", return_value=TRANSIT_RESPONSE + ), patch( + "here_routing.HERERoutingApi.route", + return_value=BIKE_RESPONSE, + ) as mock: + yield mock + + @pytest.fixture(name="no_attribution_response") def no_attribution_response_fixture(): """Return valid api response without attribution.""" diff --git a/tests/components/here_travel_time/fixtures/bike_response.json b/tests/components/here_travel_time/fixtures/bike_response.json new file mode 100644 index 00000000000..4dc9ccab03c --- /dev/null +++ b/tests/components/here_travel_time/fixtures/bike_response.json @@ -0,0 +1,214 @@ +{ + "routes": [ + { + "id": "6d8ae729-3b30-4d81-adaf-6a485b15b70a", + "sections": [ + { + "id": "c8d12b37-05e1-47f0-a5c2-43f5fb589768", + "type": "pedestrian", + "departure": { + "time": "2023-01-23T18:26:12+01:00", + "place": { + "type": "place", + "location": { + "lat": 49.1260894, + "lng": 6.1843356 + }, + "originalLocation": { + "lat": 49.1264093, + "lng": 6.1841419 + } + } + }, + "arrival": { + "time": "2023-01-23T18:29:09+01:00", + "place": { + "type": "place", + "location": { + "lat": 49.12547, + "lng": 6.18242 + } + } + }, + "summary": { + "duration": 177, + "length": 157, + "baseDuration": 177 + }, + "polyline": "BGyst29Cg5u5LpOj2BjIzZnQ_nB", + "spans": [ + { + "offset": 0, + "names": [ + { + "value": "Chemin de Halage", + "language": "fr" + } + ] + } + ], + "transport": { + "mode": "pedestrian" + } + }, + { + "id": "d37123f4-034a-4c46-8678-596f999e8eae", + "type": "vehicle", + "departure": { + "time": "2023-01-23T18:29:09+01:00", + "place": { + "type": "place", + "location": { + "lat": 49.12547, + "lng": 6.18242 + } + } + }, + "arrival": { + "time": "2023-01-23T18:44:41+01:00", + "place": { + "type": "place", + "location": { + "lat": 49.1025668, + "lng": 6.1768518 + }, + "originalLocation": { + "lat": 49.1025784, + "lng": 6.1770297 + } + } + }, + "summary": { + "duration": 932, + "length": 3426, + "baseDuration": 932 + }, + "polyline": "BG8ls29Cohr5L0UjXgFvH4DnGgFrJ7G3IvHrJrJvMnB7B_JjN_JzPvH_JjDsJjD8G7B4DrEkIvHwMzF4I7GgKjDgF7BwC7G0KjN0U3I4D7GsEvHkDzK4D7a4I3IwCzesJvH8B7GoBjDU7LsEjIwC_JkDrJkD_JsEvH4DvH4DvHsE3IgFrEkD_E4DvH0F7G0FrJ4IzFsErE4DjDgFnGsE_EwCrEoB3DUzFUnGAvHT7GTjDTvHT7GU7BArJUjD4DvCwCzFgF_EsE_EgF_JoLzF0F_EsEzFsErEkD3DwC3D8B3D8BrE8B3DoBvH8BjI8BzFoBzFUjIUnGAjSAzFTnLvC_E7BzF7B_E7B_EjD7G3D7G7B_ETnpBvCnQTzUnB3DAvMT7B7BnGvCzFrErEvCzFjDzF3DnGzF_EzF3I7LnV3cnG3IzF0K7LsTrE8G3I4NvC4DvC4D7BwC7BwCvCwCjD4DvCkDjD4D3D4DjDkDzFsEvCA3DUjDUrEUrEUrEU_ETrEvCnGrErd_nB3DnGnLnQ_JzP7Q_Y7G_JvR7ajI7Lrd3rBvH7LzZrnBnL3SnLjSjI7Lna7pBjN_T_EjIvCrE7GnL7GjNrE3IjDrEjDrEvCjDzF3IvCrE_JzU3Nrd_J_TvCrEvC3DT7BnBvC7B3D3XvvB7kBjuCoB7BoB7BUnBU7BU7BU3DAvCTjDT7BnBjDnBvC7BvCnBnB7BT7BTnBA7BU7BoBnBoBnB8BnB8BnBkDTwC_vC0PzoBwMvb8GnB0jB7B4cUgP4D0oB7foGZE", + "spans": [ + { + "offset": 0 + }, + { + "offset": 4, + "names": [ + { + "value": "Avenue de Blida", + "language": "fr" + } + ] + }, + { + "offset": 11, + "names": [ + { + "value": "Pont des Grilles", + "language": "fr" + } + ] + }, + { + "offset": 22, + "names": [ + { + "value": "Boulevard Paixhans", + "language": "fr" + } + ] + }, + { + "offset": 49, + "names": [ + { + "value": "Boulevard André Maginot", + "language": "fr" + } + ] + }, + { + "offset": 97, + "names": [ + { + "value": "Boulevard André Maginot", + "language": "fr" + }, + { + "value": "Place Jean Cocteau", + "language": "fr" + } + ] + }, + { + "offset": 98, + "names": [ + { + "value": "Place Mazelle", + "language": "fr" + } + ] + }, + { + "offset": 110, + "names": [ + { + "value": "Passage de Plantières", + "language": "fr" + } + ] + }, + { + "offset": 119 + }, + { + "offset": 127, + "names": [ + { + "value": "Avenue de lAmphithéâtre", + "language": "fr" + } + ] + }, + { + "offset": 146, + "names": [ + { + "value": "Rue aux Arènes", + "language": "fr" + } + ] + }, + { + "offset": 192, + "names": [ + { + "value": "Rue Saint-Pierre", + "language": "fr" + } + ] + }, + { + "offset": 195, + "names": [ + { + "value": "Rue Émile Boilvin", + "language": "fr" + } + ] + }, + { + "offset": 199, + "names": [ + { + "value": "Rue Charles Sadoul", + "language": "fr" + } + ] + } + ], + "transport": { + "mode": "bicycle" + } + } + ] + } + ] +} diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index f9f12504891..6d20a80bf15 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -59,8 +59,8 @@ from homeassistant.const import ( CONF_MODE, CONF_NAME, EVENT_HOMEASSISTANT_START, - TIME_MINUTES, UnitOfLength, + UnitOfTime, ) from homeassistant.core import CoreState, HomeAssistant, State from homeassistant.setup import async_setup_component @@ -146,7 +146,7 @@ async def test_sensor( await hass.async_block_till_done() duration = hass.states.get("sensor.test_duration") - assert duration.attributes.get("unit_of_measurement") == TIME_MINUTES + assert duration.attributes.get("unit_of_measurement") == UnitOfTime.MINUTES assert duration.attributes.get(ATTR_ICON) == icon assert duration.state == "26" @@ -485,13 +485,13 @@ async def test_restore_state(hass): "1234", attributes={ ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTime.MINUTES, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, ), { "native_value": 1234, - "native_unit_of_measurement": TIME_MINUTES, + "native_unit_of_measurement": UnitOfTime.MINUTES, "icon": "mdi:car", "last_reset": last_reset, }, @@ -502,13 +502,13 @@ async def test_restore_state(hass): "5678", attributes={ ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTime.MINUTES, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, }, ), { "native_value": 5678, - "native_unit_of_measurement": TIME_MINUTES, + "native_unit_of_measurement": UnitOfTime.MINUTES, "icon": "mdi:car", "last_reset": last_reset, }, @@ -581,12 +581,12 @@ async def test_restore_state(hass): # restore from cache state = hass.states.get("sensor.test_duration") assert state.state == "1234" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_MINUTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.MINUTES assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get("sensor.test_duration_in_traffic") assert state.state == "5678" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_MINUTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.MINUTES assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT state = hass.states.get("sensor.test_distance") @@ -749,3 +749,54 @@ async def test_transit_rate_limit(hass: HomeAssistant, caplog): await hass.async_block_till_done() assert hass.states.get("sensor.test_distance").state == "1.883" assert "Resetting update interval to" in caplog.text + + +@pytest.mark.usefixtures("bike_response") +async def test_multiple_sections( + hass: HomeAssistant, +): + """Test that multiple sections are handled correctly.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + data={ + CONF_ORIGIN_LATITUDE: float(ORIGIN_LATITUDE), + CONF_ORIGIN_LONGITUDE: float(ORIGIN_LONGITUDE), + CONF_DESTINATION_LATITUDE: float(DESTINATION_LATITUDE), + CONF_DESTINATION_LONGITUDE: float(DESTINATION_LONGITUDE), + CONF_API_KEY: API_KEY, + CONF_MODE: TRAVEL_MODE_BICYCLE, + CONF_NAME: "test", + }, + options=DEFAULT_OPTIONS, + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + duration = hass.states.get("sensor.test_duration") + assert duration.state == "18" + + assert float(hass.states.get("sensor.test_distance").state) == pytest.approx(3.583) + assert hass.states.get("sensor.test_duration_in_traffic").state == "18" + assert hass.states.get("sensor.test_origin").state == "Chemin de Halage" + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LATITUDE) + == "49.1260894" + ) + assert ( + hass.states.get("sensor.test_origin").attributes.get(ATTR_LONGITUDE) + == "6.1843356" + ) + + assert hass.states.get("sensor.test_destination").state == "Rue Charles Sadoul" + assert ( + hass.states.get("sensor.test_destination").attributes.get(ATTR_LATITUDE) + == "49.1025668" + ) + assert ( + hass.states.get("sensor.test_destination").attributes.get(ATTR_LONGITUDE) + == "6.1768518" + ) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index f0c4da26231..39d11f69b2e 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -1,5 +1,5 @@ """The tests the History component.""" -# pylint: disable=protected-access,invalid-name +# pylint: disable=invalid-name from datetime import timedelta from http import HTTPStatus import json @@ -10,19 +10,33 @@ import pytest from homeassistant.components import history from homeassistant.components.recorder.history import get_significant_states from homeassistant.components.recorder.models import process_timestamp -from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE +from homeassistant.const import ( + CONF_DOMAINS, + CONF_ENTITIES, + CONF_EXCLUDE, + CONF_INCLUDE, + EVENT_HOMEASSISTANT_FINAL_WRITE, +) import homeassistant.core as ha from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.components.recorder.common import ( - async_recorder_block_till_done, async_wait_recording_done, wait_recording_done, ) +def listeners_without_writes(listeners: dict[str, int]) -> dict[str, int]: + """Return listeners without final write listeners since we are not testing for these.""" + return { + key: value + for key, value in listeners.items() + if key != EVENT_HOMEASSISTANT_FINAL_WRITE + } + + @pytest.mark.usefixtures("hass_history") def test_setup(): """Test setup method of history.""" @@ -837,465 +851,3 @@ async def test_entity_ids_limit_via_api_with_skip_initial_state( assert len(response_json) == 2 assert response_json[0][0]["entity_id"] == "light.kitchen" assert response_json[1][0]["entity_id"] == "light.cow" - - -async def test_history_during_period(recorder_mock, hass, hass_ws_client): - """Test history_during_period.""" - now = dt_util.utcnow() - - await async_setup_component(hass, "history", {}) - await async_setup_component(hass, "sensor", {}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", "off", attributes={"any": "attr"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", "off", attributes={"any": "changed"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", "off", attributes={"any": "again"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) - await async_wait_recording_done(hass) - - await async_wait_recording_done(hass) - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/history_during_period", - "start_time": now.isoformat(), - "end_time": now.isoformat(), - "entity_ids": ["sensor.test"], - "include_start_time_state": True, - "significant_changes_only": False, - "no_attributes": True, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == {} - - await client.send_json( - { - "id": 2, - "type": "history/history_during_period", - "start_time": now.isoformat(), - "entity_ids": ["sensor.test"], - "include_start_time_state": True, - "significant_changes_only": False, - "no_attributes": True, - "minimal_response": True, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["id"] == 2 - - sensor_test_history = response["result"]["sensor.test"] - assert len(sensor_test_history) == 3 - - assert sensor_test_history[0]["s"] == "on" - assert sensor_test_history[0]["a"] == {} - assert isinstance(sensor_test_history[0]["lu"], float) - assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) - - assert "a" not in sensor_test_history[1] - assert sensor_test_history[1]["s"] == "off" - assert isinstance(sensor_test_history[1]["lu"], float) - assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) - - assert sensor_test_history[2]["s"] == "on" - assert "a" not in sensor_test_history[2] - - await client.send_json( - { - "id": 3, - "type": "history/history_during_period", - "start_time": now.isoformat(), - "entity_ids": ["sensor.test"], - "include_start_time_state": True, - "significant_changes_only": False, - "no_attributes": False, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["id"] == 3 - sensor_test_history = response["result"]["sensor.test"] - - assert len(sensor_test_history) == 5 - - assert sensor_test_history[0]["s"] == "on" - assert sensor_test_history[0]["a"] == {"any": "attr"} - assert isinstance(sensor_test_history[0]["lu"], float) - assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) - - assert sensor_test_history[1]["s"] == "off" - assert isinstance(sensor_test_history[1]["lu"], float) - assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) - assert sensor_test_history[1]["a"] == {"any": "attr"} - - assert sensor_test_history[4]["s"] == "on" - assert sensor_test_history[4]["a"] == {"any": "attr"} - - await client.send_json( - { - "id": 4, - "type": "history/history_during_period", - "start_time": now.isoformat(), - "entity_ids": ["sensor.test"], - "include_start_time_state": True, - "significant_changes_only": True, - "no_attributes": False, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["id"] == 4 - sensor_test_history = response["result"]["sensor.test"] - - assert len(sensor_test_history) == 3 - - assert sensor_test_history[0]["s"] == "on" - assert sensor_test_history[0]["a"] == {"any": "attr"} - assert isinstance(sensor_test_history[0]["lu"], float) - assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) - - assert sensor_test_history[1]["s"] == "off" - assert isinstance(sensor_test_history[1]["lu"], float) - assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) - assert sensor_test_history[1]["a"] == {"any": "attr"} - - assert sensor_test_history[2]["s"] == "on" - assert sensor_test_history[2]["a"] == {"any": "attr"} - - -async def test_history_during_period_impossible_conditions( - recorder_mock, hass, hass_ws_client -): - """Test history_during_period returns when condition cannot be true.""" - await async_setup_component(hass, "history", {}) - await async_setup_component(hass, "sensor", {}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", "off", attributes={"any": "attr"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", "off", attributes={"any": "changed"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", "off", attributes={"any": "again"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) - await async_wait_recording_done(hass) - - await async_wait_recording_done(hass) - - after = dt_util.utcnow() - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/history_during_period", - "start_time": after.isoformat(), - "end_time": after.isoformat(), - "entity_ids": ["sensor.test"], - "include_start_time_state": False, - "significant_changes_only": False, - "no_attributes": True, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["id"] == 1 - assert response["result"] == {} - - future = dt_util.utcnow() + timedelta(hours=10) - - await client.send_json( - { - "id": 2, - "type": "history/history_during_period", - "start_time": future.isoformat(), - "entity_ids": ["sensor.test"], - "include_start_time_state": True, - "significant_changes_only": True, - "no_attributes": True, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["id"] == 2 - assert response["result"] == {} - - -@pytest.mark.parametrize( - "time_zone", ["UTC", "Europe/Berlin", "America/Chicago", "US/Hawaii"] -) -async def test_history_during_period_significant_domain( - time_zone, recorder_mock, hass, hass_ws_client -): - """Test history_during_period with climate domain.""" - hass.config.set_time_zone(time_zone) - now = dt_util.utcnow() - - await async_setup_component(hass, "history", {}) - await async_setup_component(hass, "sensor", {}) - await async_recorder_block_till_done(hass) - hass.states.async_set("climate.test", "on", attributes={"temperature": "1"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("climate.test", "off", attributes={"temperature": "2"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("climate.test", "off", attributes={"temperature": "3"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("climate.test", "off", attributes={"temperature": "4"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("climate.test", "on", attributes={"temperature": "5"}) - await async_wait_recording_done(hass) - - await async_wait_recording_done(hass) - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/history_during_period", - "start_time": now.isoformat(), - "end_time": now.isoformat(), - "entity_ids": ["climate.test"], - "include_start_time_state": True, - "significant_changes_only": False, - "no_attributes": True, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == {} - - await client.send_json( - { - "id": 2, - "type": "history/history_during_period", - "start_time": now.isoformat(), - "entity_ids": ["climate.test"], - "include_start_time_state": True, - "significant_changes_only": False, - "no_attributes": True, - "minimal_response": True, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["id"] == 2 - - sensor_test_history = response["result"]["climate.test"] - assert len(sensor_test_history) == 5 - - assert sensor_test_history[0]["s"] == "on" - assert sensor_test_history[0]["a"] == {} - assert isinstance(sensor_test_history[0]["lu"], float) - assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) - - assert "a" in sensor_test_history[1] - assert sensor_test_history[1]["s"] == "off" - assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) - - assert sensor_test_history[4]["s"] == "on" - assert sensor_test_history[4]["a"] == {} - - await client.send_json( - { - "id": 3, - "type": "history/history_during_period", - "start_time": now.isoformat(), - "entity_ids": ["climate.test"], - "include_start_time_state": True, - "significant_changes_only": False, - "no_attributes": False, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["id"] == 3 - sensor_test_history = response["result"]["climate.test"] - - assert len(sensor_test_history) == 5 - - assert sensor_test_history[0]["s"] == "on" - assert sensor_test_history[0]["a"] == {"temperature": "1"} - assert isinstance(sensor_test_history[0]["lu"], float) - assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) - - assert sensor_test_history[1]["s"] == "off" - assert isinstance(sensor_test_history[1]["lu"], float) - assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) - assert sensor_test_history[1]["a"] == {"temperature": "2"} - - assert sensor_test_history[4]["s"] == "on" - assert sensor_test_history[4]["a"] == {"temperature": "5"} - - await client.send_json( - { - "id": 4, - "type": "history/history_during_period", - "start_time": now.isoformat(), - "entity_ids": ["climate.test"], - "include_start_time_state": True, - "significant_changes_only": True, - "no_attributes": False, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["id"] == 4 - sensor_test_history = response["result"]["climate.test"] - - assert len(sensor_test_history) == 5 - - assert sensor_test_history[0]["s"] == "on" - assert sensor_test_history[0]["a"] == {"temperature": "1"} - assert isinstance(sensor_test_history[0]["lu"], float) - assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) - - assert sensor_test_history[1]["s"] == "off" - assert isinstance(sensor_test_history[1]["lu"], float) - assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) - assert sensor_test_history[1]["a"] == {"temperature": "2"} - - assert sensor_test_history[2]["s"] == "off" - assert sensor_test_history[2]["a"] == {"temperature": "3"} - - assert sensor_test_history[3]["s"] == "off" - assert sensor_test_history[3]["a"] == {"temperature": "4"} - - assert sensor_test_history[4]["s"] == "on" - assert sensor_test_history[4]["a"] == {"temperature": "5"} - - # Test we impute the state time state - later = dt_util.utcnow() - await client.send_json( - { - "id": 5, - "type": "history/history_during_period", - "start_time": later.isoformat(), - "entity_ids": ["climate.test"], - "include_start_time_state": True, - "significant_changes_only": True, - "no_attributes": False, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["id"] == 5 - sensor_test_history = response["result"]["climate.test"] - - assert len(sensor_test_history) == 1 - - assert sensor_test_history[0]["s"] == "on" - assert sensor_test_history[0]["a"] == {"temperature": "5"} - assert sensor_test_history[0]["lu"] == later.timestamp() - assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) - - -async def test_history_during_period_bad_start_time( - recorder_mock, hass, hass_ws_client -): - """Test history_during_period bad state time.""" - await async_setup_component( - hass, - "history", - {"history": {}}, - ) - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/history_during_period", - "start_time": "cats", - } - ) - response = await client.receive_json() - assert not response["success"] - assert response["error"]["code"] == "invalid_start_time" - - -async def test_history_during_period_bad_end_time(recorder_mock, hass, hass_ws_client): - """Test history_during_period bad end time.""" - now = dt_util.utcnow() - - await async_setup_component( - hass, - "history", - {"history": {}}, - ) - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/history_during_period", - "start_time": now.isoformat(), - "end_time": "dogs", - } - ) - response = await client.receive_json() - assert not response["success"] - assert response["error"]["code"] == "invalid_end_time" - - -async def test_history_during_period_with_use_include_order( - recorder_mock, hass, hass_ws_client -): - """Test history_during_period.""" - now = dt_util.utcnow() - sort_order = ["sensor.two", "sensor.four", "sensor.one"] - await async_setup_component( - hass, - "history", - { - history.DOMAIN: { - history.CONF_ORDER: True, - CONF_INCLUDE: { - CONF_ENTITIES: sort_order, - CONF_DOMAINS: ["sensor"], - }, - } - }, - ) - await async_setup_component(hass, "sensor", {}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.three", "off", attributes={"any": "changed"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.four", "off", attributes={"any": "again"}) - await async_recorder_block_till_done(hass) - hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) - await async_wait_recording_done(hass) - - await async_wait_recording_done(hass) - - client = await hass_ws_client() - await client.send_json( - { - "id": 1, - "type": "history/history_during_period", - "start_time": now.isoformat(), - "include_start_time_state": True, - "significant_changes_only": False, - "no_attributes": True, - "minimal_response": True, - } - ) - response = await client.receive_json() - assert response["success"] - assert response["id"] == 1 - - assert list(response["result"]) == [ - *sort_order, - "sensor.three", - ] diff --git a/tests/components/history/test_init_db_schema_30.py b/tests/components/history/test_init_db_schema_30.py new file mode 100644 index 00000000000..7d1659d94f9 --- /dev/null +++ b/tests/components/history/test_init_db_schema_30.py @@ -0,0 +1,1297 @@ +"""The tests the History component.""" +from __future__ import annotations + +# pylint: disable=invalid-name +from datetime import timedelta +from http import HTTPStatus +import importlib +import json +import sys +from unittest.mock import patch, sentinel + +import pytest +from sqlalchemy import create_engine +from sqlalchemy.orm import Session + +from homeassistant.components import history, recorder +from homeassistant.components.recorder import core, statistics +from homeassistant.components.recorder.history import get_significant_states +from homeassistant.components.recorder.models import process_timestamp +from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE +import homeassistant.core as ha +from homeassistant.helpers.json import JSONEncoder +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.components.recorder.common import ( + async_recorder_block_till_done, + async_wait_recording_done, + wait_recording_done, +) + +CREATE_ENGINE_TARGET = "homeassistant.components.recorder.core.create_engine" +SCHEMA_MODULE = "tests.components.recorder.db_schema_30" + + +def _create_engine_test(*args, **kwargs): + """Test version of create_engine that initializes with old schema. + + This simulates an existing db with the old schema. + """ + importlib.import_module(SCHEMA_MODULE) + old_db_schema = sys.modules[SCHEMA_MODULE] + engine = create_engine(*args, **kwargs) + old_db_schema.Base.metadata.create_all(engine) + with Session(engine) as session: + session.add( + recorder.db_schema.StatisticsRuns(start=statistics.get_start_time()) + ) + session.add( + recorder.db_schema.SchemaChanges( + schema_version=old_db_schema.SCHEMA_VERSION + ) + ) + session.commit() + return engine + + +@pytest.fixture(autouse=True) +def db_schema_30(): + """Fixture to initialize the db with the old schema.""" + importlib.import_module(SCHEMA_MODULE) + old_db_schema = sys.modules[SCHEMA_MODULE] + + with patch.object(recorder, "db_schema", old_db_schema), patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION + ), patch.object(core, "EventData", old_db_schema.EventData), patch.object( + core, "States", old_db_schema.States + ), patch.object( + core, "Events", old_db_schema.Events + ), patch.object( + core, "StateAttributes", old_db_schema.StateAttributes + ), patch( + CREATE_ENGINE_TARGET, new=_create_engine_test + ): + yield + + +@pytest.mark.usefixtures("hass_history") +def test_setup(): + """Test setup method of history.""" + # Verification occurs in the fixture + + +def test_get_significant_states(hass_history): + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_history + zero, four, states = record_states(hass) + hist = get_significant_states(hass, zero, four, filters=history.Filters()) + assert states == hist + + +def test_get_significant_states_minimal_response(hass_history): + """Test that only significant states are returned. + + When minimal responses is set only the first and + last states return a complete state. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_history + zero, four, states = record_states(hass) + hist = get_significant_states( + hass, zero, four, filters=history.Filters(), minimal_response=True + ) + entites_with_reducable_states = [ + "media_player.test", + "media_player.test3", + ] + + # All states for media_player.test state are reduced + # down to last_changed and state when minimal_response + # is set except for the first state. + # is set. We use JSONEncoder to make sure that are + # pre-encoded last_changed is always the same as what + # will happen with encoding a native state + for entity_id in entites_with_reducable_states: + entity_states = states[entity_id] + for state_idx in range(1, len(entity_states)): + input_state = entity_states[state_idx] + orig_last_changed = orig_last_changed = json.dumps( + process_timestamp(input_state.last_changed), + cls=JSONEncoder, + ).replace('"', "") + orig_state = input_state.state + entity_states[state_idx] = { + "last_changed": orig_last_changed, + "state": orig_state, + } + assert states == hist + + +def test_get_significant_states_with_initial(hass_history): + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_history + zero, four, states = record_states(hass) + one = zero + timedelta(seconds=1) + one_and_half = zero + timedelta(seconds=1.5) + for entity_id in states: + if entity_id == "media_player.test": + states[entity_id] = states[entity_id][1:] + for state in states[entity_id]: + if state.last_changed == one: + state.last_changed = one_and_half + + hist = get_significant_states( + hass, + one_and_half, + four, + filters=history.Filters(), + include_start_time_state=True, + ) + assert states == hist + + +def test_get_significant_states_without_initial(hass_history): + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_history + zero, four, states = record_states(hass) + one = zero + timedelta(seconds=1) + one_and_half = zero + timedelta(seconds=1.5) + for entity_id in states: + states[entity_id] = list( + filter(lambda s: s.last_changed != one, states[entity_id]) + ) + del states["media_player.test2"] + + hist = get_significant_states( + hass, + one_and_half, + four, + filters=history.Filters(), + include_start_time_state=False, + ) + assert states == hist + + +def test_get_significant_states_entity_id(hass_history): + """Test that only significant states are returned for one entity.""" + hass = hass_history + zero, four, states = record_states(hass) + del states["media_player.test2"] + del states["media_player.test3"] + del states["thermostat.test"] + del states["thermostat.test2"] + del states["script.can_cancel_this_one"] + + hist = get_significant_states( + hass, zero, four, ["media_player.test"], filters=history.Filters() + ) + assert states == hist + + +def test_get_significant_states_multiple_entity_ids(hass_history): + """Test that only significant states are returned for one entity.""" + hass = hass_history + zero, four, states = record_states(hass) + del states["media_player.test2"] + del states["media_player.test3"] + del states["thermostat.test2"] + del states["script.can_cancel_this_one"] + + hist = get_significant_states( + hass, + zero, + four, + ["media_player.test", "thermostat.test"], + filters=history.Filters(), + ) + assert states == hist + + +def test_get_significant_states_exclude_domain(hass_history): + """Test if significant states are returned when excluding domains. + + We should get back every thermostat change that includes an attribute + change, but no media player changes. + """ + hass = hass_history + zero, four, states = record_states(hass) + del states["media_player.test"] + del states["media_player.test2"] + del states["media_player.test3"] + + config = history.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + history.DOMAIN: {CONF_EXCLUDE: {CONF_DOMAINS: ["media_player"]}}, + } + ) + check_significant_states(hass, zero, four, states, config) + + +def test_get_significant_states_exclude_entity(hass_history): + """Test if significant states are returned when excluding entities. + + We should get back every thermostat and script changes, but no media + player changes. + """ + hass = hass_history + zero, four, states = record_states(hass) + del states["media_player.test"] + + config = history.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + history.DOMAIN: {CONF_EXCLUDE: {CONF_ENTITIES: ["media_player.test"]}}, + } + ) + check_significant_states(hass, zero, four, states, config) + + +def test_get_significant_states_exclude(hass_history): + """Test significant states when excluding entities and domains. + + We should not get back every thermostat and media player test changes. + """ + hass = hass_history + zero, four, states = record_states(hass) + del states["media_player.test"] + del states["thermostat.test"] + del states["thermostat.test2"] + + config = history.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + history.DOMAIN: { + CONF_EXCLUDE: { + CONF_DOMAINS: ["thermostat"], + CONF_ENTITIES: ["media_player.test"], + } + }, + } + ) + check_significant_states(hass, zero, four, states, config) + + +def test_get_significant_states_exclude_include_entity(hass_history): + """Test significant states when excluding domains and include entities. + + We should not get back every thermostat change unless its specifically included + """ + hass = hass_history + zero, four, states = record_states(hass) + del states["thermostat.test2"] + + config = history.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + history.DOMAIN: { + CONF_INCLUDE: {CONF_ENTITIES: ["media_player.test", "thermostat.test"]}, + CONF_EXCLUDE: {CONF_DOMAINS: ["thermostat"]}, + }, + } + ) + check_significant_states(hass, zero, four, states, config) + + +def test_get_significant_states_include_domain(hass_history): + """Test if significant states are returned when including domains. + + We should get back every thermostat and script changes, but no media + player changes. + """ + hass = hass_history + zero, four, states = record_states(hass) + del states["media_player.test"] + del states["media_player.test2"] + del states["media_player.test3"] + + config = history.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + history.DOMAIN: {CONF_INCLUDE: {CONF_DOMAINS: ["thermostat", "script"]}}, + } + ) + check_significant_states(hass, zero, four, states, config) + + +def test_get_significant_states_include_entity(hass_history): + """Test if significant states are returned when including entities. + + We should only get back changes of the media_player.test entity. + """ + hass = hass_history + zero, four, states = record_states(hass) + del states["media_player.test2"] + del states["media_player.test3"] + del states["thermostat.test"] + del states["thermostat.test2"] + del states["script.can_cancel_this_one"] + + config = history.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + history.DOMAIN: {CONF_INCLUDE: {CONF_ENTITIES: ["media_player.test"]}}, + } + ) + check_significant_states(hass, zero, four, states, config) + + +def test_get_significant_states_include(hass_history): + """Test significant states when including domains and entities. + + We should only get back changes of the media_player.test entity and the + thermostat domain. + """ + hass = hass_history + zero, four, states = record_states(hass) + del states["media_player.test2"] + del states["media_player.test3"] + del states["script.can_cancel_this_one"] + + config = history.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + history.DOMAIN: { + CONF_INCLUDE: { + CONF_DOMAINS: ["thermostat"], + CONF_ENTITIES: ["media_player.test"], + } + }, + } + ) + check_significant_states(hass, zero, four, states, config) + + +def test_get_significant_states_include_exclude_domain(hass_history): + """Test if significant states when excluding and including domains. + + We should get back all the media_player domain changes + only since the include wins over the exclude but will + exclude everything else. + """ + hass = hass_history + zero, four, states = record_states(hass) + del states["thermostat.test"] + del states["thermostat.test2"] + del states["script.can_cancel_this_one"] + + config = history.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + history.DOMAIN: { + CONF_INCLUDE: {CONF_DOMAINS: ["media_player"]}, + CONF_EXCLUDE: {CONF_DOMAINS: ["media_player"]}, + }, + } + ) + check_significant_states(hass, zero, four, states, config) + + +def test_get_significant_states_include_exclude_entity(hass_history): + """Test if significant states when excluding and including domains. + + We should not get back any changes since we include only + media_player.test but also exclude it. + """ + hass = hass_history + zero, four, states = record_states(hass) + del states["media_player.test2"] + del states["media_player.test3"] + del states["thermostat.test"] + del states["thermostat.test2"] + del states["script.can_cancel_this_one"] + + config = history.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + history.DOMAIN: { + CONF_INCLUDE: {CONF_ENTITIES: ["media_player.test"]}, + CONF_EXCLUDE: {CONF_ENTITIES: ["media_player.test"]}, + }, + } + ) + check_significant_states(hass, zero, four, states, config) + + +def test_get_significant_states_include_exclude(hass_history): + """Test if significant states when in/excluding domains and entities. + + We should get back changes of the media_player.test2, media_player.test3, + and thermostat.test. + """ + hass = hass_history + zero, four, states = record_states(hass) + del states["media_player.test"] + del states["thermostat.test2"] + del states["script.can_cancel_this_one"] + + config = history.CONFIG_SCHEMA( + { + ha.DOMAIN: {}, + history.DOMAIN: { + CONF_INCLUDE: { + CONF_DOMAINS: ["media_player"], + CONF_ENTITIES: ["thermostat.test"], + }, + CONF_EXCLUDE: { + CONF_DOMAINS: ["thermostat"], + CONF_ENTITIES: ["media_player.test"], + }, + }, + } + ) + check_significant_states(hass, zero, four, states, config) + + +def test_get_significant_states_are_ordered(hass_history): + """Test order of results from get_significant_states. + + When entity ids are given, the results should be returned with the data + in the same order. + """ + hass = hass_history + zero, four, _states = record_states(hass) + entity_ids = ["media_player.test", "media_player.test2"] + hist = get_significant_states( + hass, zero, four, entity_ids, filters=history.Filters() + ) + assert list(hist.keys()) == entity_ids + entity_ids = ["media_player.test2", "media_player.test"] + hist = get_significant_states( + hass, zero, four, entity_ids, filters=history.Filters() + ) + assert list(hist.keys()) == entity_ids + + +def test_get_significant_states_only(hass_history): + """Test significant states when significant_states_only is set.""" + hass = hass_history + entity_id = "sensor.test" + + def set_state(state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=4) + points = [] + for i in range(1, 4): + points.append(start + timedelta(minutes=i)) + + states = [] + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start + ): + set_state("123", attributes={"attribute": 10.64}) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", + return_value=points[0], + ): + # Attributes are different, state not + states.append(set_state("123", attributes={"attribute": 21.42})) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", + return_value=points[1], + ): + # state is different, attributes not + states.append(set_state("32", attributes={"attribute": 21.42})) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", + return_value=points[2], + ): + # everything is different + states.append(set_state("412", attributes={"attribute": 54.23})) + + hist = get_significant_states(hass, start, significant_changes_only=True) + + assert len(hist[entity_id]) == 2 + assert states[0] not in hist[entity_id] + assert states[1] in hist[entity_id] + assert states[2] in hist[entity_id] + + hist = get_significant_states(hass, start, significant_changes_only=False) + + assert len(hist[entity_id]) == 3 + assert states == hist[entity_id] + + +def check_significant_states(hass, zero, four, states, config): + """Check if significant states are retrieved.""" + filters = history.Filters() + exclude = config[history.DOMAIN].get(CONF_EXCLUDE) + if exclude: + filters.excluded_entities = exclude.get(CONF_ENTITIES, []) + filters.excluded_domains = exclude.get(CONF_DOMAINS, []) + include = config[history.DOMAIN].get(CONF_INCLUDE) + if include: + filters.included_entities = include.get(CONF_ENTITIES, []) + filters.included_domains = include.get(CONF_DOMAINS, []) + + hist = get_significant_states(hass, zero, four, filters=filters) + assert states == hist + + +def record_states(hass): + """Record some test states. + + We inject a bunch of state updates from media player, zone and + thermostat. + """ + mp = "media_player.test" + mp2 = "media_player.test2" + mp3 = "media_player.test3" + therm = "thermostat.test" + therm2 = "thermostat.test2" + zone = "zone.home" + script_c = "script.can_cancel_this_one" + + def set_state(entity_id, state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + zero = dt_util.utcnow() + one = zero + timedelta(seconds=1) + two = one + timedelta(seconds=1) + three = two + timedelta(seconds=1) + four = three + timedelta(seconds=1) + + states = {therm: [], therm2: [], mp: [], mp2: [], mp3: [], script_c: []} + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=one + ): + states[mp].append( + set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[mp].append( + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[mp2].append( + set_state(mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[mp3].append( + set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[therm].append( + set_state(therm, 20, attributes={"current_temperature": 19.5}) + ) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=two + ): + # This state will be skipped only different in time + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)}) + # This state will be skipped because domain is excluded + set_state(zone, "zoning") + states[script_c].append( + set_state(script_c, "off", attributes={"can_cancel": True}) + ) + states[therm].append( + set_state(therm, 21, attributes={"current_temperature": 19.8}) + ) + states[therm2].append( + set_state(therm2, 20, attributes={"current_temperature": 19}) + ) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=three + ): + states[mp].append( + set_state(mp, "Netflix", attributes={"media_title": str(sentinel.mt4)}) + ) + states[mp3].append( + set_state(mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)}) + ) + # Attributes changed even though state is the same + states[therm].append( + set_state(therm, 21, attributes={"current_temperature": 20}) + ) + + return zero, four, states + + +async def test_fetch_period_api(recorder_mock, hass, hass_client): + """Test the fetch period view for history.""" + await async_setup_component(hass, "history", {}) + client = await hass_client() + response = await client.get(f"/api/history/period/{dt_util.utcnow().isoformat()}") + assert response.status == HTTPStatus.OK + + +async def test_fetch_period_api_with_use_include_order( + recorder_mock, hass, hass_client +): + """Test the fetch period view for history with include order.""" + await async_setup_component( + hass, "history", {history.DOMAIN: {history.CONF_ORDER: True}} + ) + client = await hass_client() + response = await client.get(f"/api/history/period/{dt_util.utcnow().isoformat()}") + assert response.status == HTTPStatus.OK + + +async def test_fetch_period_api_with_minimal_response(recorder_mock, hass, hass_client): + """Test the fetch period view for history with minimal_response.""" + now = dt_util.utcnow() + await async_setup_component(hass, "history", {}) + + hass.states.async_set("sensor.power", 0, {"attr": "any"}) + await async_wait_recording_done(hass) + hass.states.async_set("sensor.power", 50, {"attr": "any"}) + await async_wait_recording_done(hass) + hass.states.async_set("sensor.power", 23, {"attr": "any"}) + last_changed = hass.states.get("sensor.power").last_changed + await async_wait_recording_done(hass) + hass.states.async_set("sensor.power", 23, {"attr": "any"}) + await async_wait_recording_done(hass) + client = await hass_client() + response = await client.get( + f"/api/history/period/{now.isoformat()}?filter_entity_id=sensor.power&minimal_response&no_attributes" + ) + assert response.status == HTTPStatus.OK + response_json = await response.json() + assert len(response_json[0]) == 3 + state_list = response_json[0] + + assert state_list[0]["entity_id"] == "sensor.power" + assert state_list[0]["attributes"] == {} + assert state_list[0]["state"] == "0" + + assert "attributes" not in state_list[1] + assert "entity_id" not in state_list[1] + assert state_list[1]["state"] == "50" + + assert "attributes" not in state_list[2] + assert "entity_id" not in state_list[2] + assert state_list[2]["state"] == "23" + assert state_list[2]["last_changed"] == json.dumps( + process_timestamp(last_changed), + cls=JSONEncoder, + ).replace('"', "") + + +async def test_fetch_period_api_with_no_timestamp(recorder_mock, hass, hass_client): + """Test the fetch period view for history with no timestamp.""" + await async_setup_component(hass, "history", {}) + client = await hass_client() + response = await client.get("/api/history/period") + assert response.status == HTTPStatus.OK + + +async def test_fetch_period_api_with_include_order(recorder_mock, hass, hass_client): + """Test the fetch period view for history.""" + await async_setup_component( + hass, + "history", + { + "history": { + "use_include_order": True, + "include": {"entities": ["light.kitchen"]}, + } + }, + ) + client = await hass_client() + response = await client.get( + f"/api/history/period/{dt_util.utcnow().isoformat()}", + params={"filter_entity_id": "non.existing,something.else"}, + ) + assert response.status == HTTPStatus.OK + + +async def test_fetch_period_api_with_entity_glob_include( + recorder_mock, hass, hass_client +): + """Test the fetch period view for history.""" + await async_setup_component( + hass, + "history", + { + "history": { + "include": {"entity_globs": ["light.k*"]}, + } + }, + ) + hass.states.async_set("light.kitchen", "on") + hass.states.async_set("light.cow", "on") + hass.states.async_set("light.nomatch", "on") + + await async_wait_recording_done(hass) + + client = await hass_client() + response = await client.get( + f"/api/history/period/{dt_util.utcnow().isoformat()}", + ) + assert response.status == HTTPStatus.OK + response_json = await response.json() + assert response_json[0][0]["entity_id"] == "light.kitchen" + + +async def test_fetch_period_api_with_entity_glob_exclude( + recorder_mock, hass, hass_client +): + """Test the fetch period view for history.""" + await async_setup_component( + hass, + "history", + { + "history": { + "exclude": { + "entity_globs": ["light.k*", "binary_sensor.*_?"], + "domains": "switch", + "entities": "media_player.test", + }, + } + }, + ) + hass.states.async_set("light.kitchen", "on") + hass.states.async_set("light.cow", "on") + hass.states.async_set("light.match", "on") + hass.states.async_set("switch.match", "on") + hass.states.async_set("media_player.test", "on") + hass.states.async_set("binary_sensor.sensor_l", "on") + hass.states.async_set("binary_sensor.sensor_r", "on") + hass.states.async_set("binary_sensor.sensor", "on") + + await async_wait_recording_done(hass) + + client = await hass_client() + response = await client.get( + f"/api/history/period/{dt_util.utcnow().isoformat()}", + ) + assert response.status == HTTPStatus.OK + response_json = await response.json() + assert len(response_json) == 3 + assert response_json[0][0]["entity_id"] == "binary_sensor.sensor" + assert response_json[1][0]["entity_id"] == "light.cow" + assert response_json[2][0]["entity_id"] == "light.match" + + +async def test_fetch_period_api_with_entity_glob_include_and_exclude( + recorder_mock, hass, hass_client +): + """Test the fetch period view for history.""" + await async_setup_component( + hass, + "history", + { + "history": { + "exclude": { + "entity_globs": ["light.many*", "binary_sensor.*"], + }, + "include": { + "entity_globs": ["light.m*"], + "domains": "switch", + "entities": "media_player.test", + }, + } + }, + ) + hass.states.async_set("light.kitchen", "on") + hass.states.async_set("light.cow", "on") + hass.states.async_set("light.match", "on") + hass.states.async_set("light.many_state_changes", "on") + hass.states.async_set("switch.match", "on") + hass.states.async_set("media_player.test", "on") + hass.states.async_set("binary_sensor.exclude", "on") + + await async_wait_recording_done(hass) + + client = await hass_client() + response = await client.get( + f"/api/history/period/{dt_util.utcnow().isoformat()}", + ) + assert response.status == HTTPStatus.OK + response_json = await response.json() + assert len(response_json) == 4 + assert response_json[0][0]["entity_id"] == "light.many_state_changes" + assert response_json[1][0]["entity_id"] == "light.match" + assert response_json[2][0]["entity_id"] == "media_player.test" + assert response_json[3][0]["entity_id"] == "switch.match" + + +async def test_entity_ids_limit_via_api(recorder_mock, hass, hass_client): + """Test limiting history to entity_ids.""" + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + hass.states.async_set("light.kitchen", "on") + hass.states.async_set("light.cow", "on") + hass.states.async_set("light.nomatch", "on") + + await async_wait_recording_done(hass) + + client = await hass_client() + response = await client.get( + f"/api/history/period/{dt_util.utcnow().isoformat()}?filter_entity_id=light.kitchen,light.cow", + ) + assert response.status == HTTPStatus.OK + response_json = await response.json() + assert len(response_json) == 2 + assert response_json[0][0]["entity_id"] == "light.kitchen" + assert response_json[1][0]["entity_id"] == "light.cow" + + +async def test_entity_ids_limit_via_api_with_skip_initial_state( + recorder_mock, hass, hass_client +): + """Test limiting history to entity_ids with skip_initial_state.""" + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + hass.states.async_set("light.kitchen", "on") + hass.states.async_set("light.cow", "on") + hass.states.async_set("light.nomatch", "on") + + await async_wait_recording_done(hass) + + client = await hass_client() + response = await client.get( + f"/api/history/period/{dt_util.utcnow().isoformat()}?filter_entity_id=light.kitchen,light.cow&skip_initial_state", + ) + assert response.status == HTTPStatus.OK + response_json = await response.json() + assert len(response_json) == 0 + + when = dt_util.utcnow() - timedelta(minutes=1) + response = await client.get( + f"/api/history/period/{when.isoformat()}?filter_entity_id=light.kitchen,light.cow&skip_initial_state", + ) + assert response.status == HTTPStatus.OK + response_json = await response.json() + assert len(response_json) == 2 + assert response_json[0][0]["entity_id"] == "light.kitchen" + assert response_json[1][0]["entity_id"] == "light.cow" + + +async def test_history_during_period(recorder_mock, hass, hass_ws_client): + """Test history_during_period.""" + now = dt_util.utcnow() + + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "changed"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "again"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "entity_ids": ["sensor.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + await client.send_json( + { + "id": 2, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "entity_ids": ["sensor.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 2 + + sensor_test_history = response["result"]["sensor.test"] + assert len(sensor_test_history) == 3 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {} + assert isinstance(sensor_test_history[0]["lu"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + assert "a" not in sensor_test_history[1] + assert sensor_test_history[1]["s"] == "off" + assert isinstance(sensor_test_history[1]["lu"], float) + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) + + assert sensor_test_history[2]["s"] == "on" + assert "a" not in sensor_test_history[2] + + await client.send_json( + { + "id": 3, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "entity_ids": ["sensor.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 3 + sensor_test_history = response["result"]["sensor.test"] + + assert len(sensor_test_history) == 5 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {"any": "attr"} + assert isinstance(sensor_test_history[0]["lu"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + assert sensor_test_history[1]["s"] == "off" + assert isinstance(sensor_test_history[1]["lu"], float) + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) + assert sensor_test_history[1]["a"] == {"any": "attr"} + + assert sensor_test_history[4]["s"] == "on" + assert sensor_test_history[4]["a"] == {"any": "attr"} + + await client.send_json( + { + "id": 4, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "entity_ids": ["sensor.test"], + "include_start_time_state": True, + "significant_changes_only": True, + "no_attributes": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 4 + sensor_test_history = response["result"]["sensor.test"] + + assert len(sensor_test_history) == 3 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {"any": "attr"} + assert isinstance(sensor_test_history[0]["lu"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + assert sensor_test_history[1]["s"] == "off" + assert isinstance(sensor_test_history[1]["lu"], float) + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) + assert sensor_test_history[1]["a"] == {"any": "attr"} + + assert sensor_test_history[2]["s"] == "on" + assert sensor_test_history[2]["a"] == {"any": "attr"} + + +async def test_history_during_period_impossible_conditions( + recorder_mock, hass, hass_ws_client +): + """Test history_during_period returns when condition cannot be true.""" + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "changed"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "again"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + after = dt_util.utcnow() + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/history_during_period", + "start_time": after.isoformat(), + "end_time": after.isoformat(), + "entity_ids": ["sensor.test"], + "include_start_time_state": False, + "significant_changes_only": False, + "no_attributes": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["result"] == {} + + future = dt_util.utcnow() + timedelta(hours=10) + + await client.send_json( + { + "id": 2, + "type": "history/history_during_period", + "start_time": future.isoformat(), + "entity_ids": ["sensor.test"], + "include_start_time_state": True, + "significant_changes_only": True, + "no_attributes": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 2 + assert response["result"] == {} + + +@pytest.mark.parametrize( + "time_zone", ["UTC", "Europe/Berlin", "America/Chicago", "US/Hawaii"] +) +async def test_history_during_period_significant_domain( + time_zone, recorder_mock, hass, hass_ws_client +): + """Test history_during_period with climate domain.""" + hass.config.set_time_zone(time_zone) + now = dt_util.utcnow() + + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "on", attributes={"temperature": "1"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "off", attributes={"temperature": "2"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "off", attributes={"temperature": "3"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "off", attributes={"temperature": "4"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "on", attributes={"temperature": "5"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + await client.send_json( + { + "id": 2, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 2 + + sensor_test_history = response["result"]["climate.test"] + assert len(sensor_test_history) == 5 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {} + assert isinstance(sensor_test_history[0]["lu"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + assert "a" in sensor_test_history[1] + assert sensor_test_history[1]["s"] == "off" + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) + + assert sensor_test_history[4]["s"] == "on" + assert sensor_test_history[4]["a"] == {} + + await client.send_json( + { + "id": 3, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 3 + sensor_test_history = response["result"]["climate.test"] + + assert len(sensor_test_history) == 5 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {"temperature": "1"} + assert isinstance(sensor_test_history[0]["lu"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + assert sensor_test_history[1]["s"] == "off" + assert isinstance(sensor_test_history[1]["lu"], float) + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) + assert sensor_test_history[1]["a"] == {"temperature": "2"} + + assert sensor_test_history[4]["s"] == "on" + assert sensor_test_history[4]["a"] == {"temperature": "5"} + + await client.send_json( + { + "id": 4, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": True, + "no_attributes": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 4 + sensor_test_history = response["result"]["climate.test"] + + assert len(sensor_test_history) == 5 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {"temperature": "1"} + assert isinstance(sensor_test_history[0]["lu"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + assert sensor_test_history[1]["s"] == "off" + assert isinstance(sensor_test_history[1]["lu"], float) + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) + assert sensor_test_history[1]["a"] == {"temperature": "2"} + + assert sensor_test_history[2]["s"] == "off" + assert sensor_test_history[2]["a"] == {"temperature": "3"} + + assert sensor_test_history[3]["s"] == "off" + assert sensor_test_history[3]["a"] == {"temperature": "4"} + + assert sensor_test_history[4]["s"] == "on" + assert sensor_test_history[4]["a"] == {"temperature": "5"} + + # Test we impute the state time state + later = dt_util.utcnow() + await client.send_json( + { + "id": 5, + "type": "history/history_during_period", + "start_time": later.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": True, + "no_attributes": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 5 + sensor_test_history = response["result"]["climate.test"] + + assert len(sensor_test_history) == 1 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {"temperature": "5"} + assert sensor_test_history[0]["lu"] == later.timestamp() + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + +async def test_history_during_period_bad_start_time( + recorder_mock, hass, hass_ws_client +): + """Test history_during_period bad state time.""" + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/history_during_period", + "start_time": "cats", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_start_time" + + +async def test_history_during_period_bad_end_time(recorder_mock, hass, hass_ws_client): + """Test history_during_period bad end time.""" + now = dt_util.utcnow() + + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "end_time": "dogs", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_end_time" diff --git a/tests/components/history/test_websocket_api.py b/tests/components/history/test_websocket_api.py new file mode 100644 index 00000000000..e3ecf02bc7f --- /dev/null +++ b/tests/components/history/test_websocket_api.py @@ -0,0 +1,1561 @@ +"""The tests the History component websocket_api.""" +# pylint: disable=protected-access,invalid-name +from datetime import timedelta +from unittest.mock import patch + +import async_timeout +from freezegun import freeze_time +import pytest + +from homeassistant.components import history +from homeassistant.components.history import websocket_api +from homeassistant.const import ( + CONF_DOMAINS, + CONF_ENTITIES, + CONF_INCLUDE, + EVENT_HOMEASSISTANT_FINAL_WRITE, +) +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from tests.common import async_fire_time_changed +from tests.components.recorder.common import ( + async_recorder_block_till_done, + async_wait_recording_done, +) + + +def listeners_without_writes(listeners: dict[str, int]) -> dict[str, int]: + """Return listeners without final write listeners since we are not testing for these.""" + return { + key: value + for key, value in listeners.items() + if key != EVENT_HOMEASSISTANT_FINAL_WRITE + } + + +@pytest.mark.usefixtures("hass_history") +def test_setup(): + """Test setup method of history.""" + # Verification occurs in the fixture + + +async def test_history_during_period(recorder_mock, hass, hass_ws_client): + """Test history_during_period.""" + now = dt_util.utcnow() + + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "changed"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "again"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "entity_ids": ["sensor.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + await client.send_json( + { + "id": 2, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "entity_ids": ["sensor.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 2 + + sensor_test_history = response["result"]["sensor.test"] + assert len(sensor_test_history) == 3 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {} + assert isinstance(sensor_test_history[0]["lu"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + assert "a" not in sensor_test_history[1] + assert sensor_test_history[1]["s"] == "off" + assert isinstance(sensor_test_history[1]["lu"], float) + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) + + assert sensor_test_history[2]["s"] == "on" + assert "a" not in sensor_test_history[2] + + await client.send_json( + { + "id": 3, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "entity_ids": ["sensor.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 3 + sensor_test_history = response["result"]["sensor.test"] + + assert len(sensor_test_history) == 5 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {"any": "attr"} + assert isinstance(sensor_test_history[0]["lu"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + assert sensor_test_history[1]["s"] == "off" + assert isinstance(sensor_test_history[1]["lu"], float) + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) + assert sensor_test_history[1]["a"] == {"any": "attr"} + + assert sensor_test_history[4]["s"] == "on" + assert sensor_test_history[4]["a"] == {"any": "attr"} + + await client.send_json( + { + "id": 4, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "entity_ids": ["sensor.test"], + "include_start_time_state": True, + "significant_changes_only": True, + "no_attributes": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 4 + sensor_test_history = response["result"]["sensor.test"] + + assert len(sensor_test_history) == 3 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {"any": "attr"} + assert isinstance(sensor_test_history[0]["lu"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + assert sensor_test_history[1]["s"] == "off" + assert isinstance(sensor_test_history[1]["lu"], float) + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) + assert sensor_test_history[1]["a"] == {"any": "attr"} + + assert sensor_test_history[2]["s"] == "on" + assert sensor_test_history[2]["a"] == {"any": "attr"} + + +async def test_history_during_period_impossible_conditions( + recorder_mock, hass, hass_ws_client +): + """Test history_during_period returns when condition cannot be true.""" + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "changed"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "again"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + after = dt_util.utcnow() + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/history_during_period", + "start_time": after.isoformat(), + "end_time": after.isoformat(), + "entity_ids": ["sensor.test"], + "include_start_time_state": False, + "significant_changes_only": False, + "no_attributes": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["result"] == {} + + future = dt_util.utcnow() + timedelta(hours=10) + + await client.send_json( + { + "id": 2, + "type": "history/history_during_period", + "start_time": future.isoformat(), + "entity_ids": ["sensor.test"], + "include_start_time_state": True, + "significant_changes_only": True, + "no_attributes": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 2 + assert response["result"] == {} + + +@pytest.mark.parametrize( + "time_zone", ["UTC", "Europe/Berlin", "America/Chicago", "US/Hawaii"] +) +async def test_history_during_period_significant_domain( + time_zone, recorder_mock, hass, hass_ws_client +): + """Test history_during_period with climate domain.""" + hass.config.set_time_zone(time_zone) + now = dt_util.utcnow() + + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "on", attributes={"temperature": "1"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "off", attributes={"temperature": "2"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "off", attributes={"temperature": "3"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "off", attributes={"temperature": "4"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "on", attributes={"temperature": "5"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {} + + await client.send_json( + { + "id": 2, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 2 + + sensor_test_history = response["result"]["climate.test"] + assert len(sensor_test_history) == 5 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {} + assert isinstance(sensor_test_history[0]["lu"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + assert "a" in sensor_test_history[1] + assert sensor_test_history[1]["s"] == "off" + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) + + assert sensor_test_history[4]["s"] == "on" + assert sensor_test_history[4]["a"] == {} + + await client.send_json( + { + "id": 3, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 3 + sensor_test_history = response["result"]["climate.test"] + + assert len(sensor_test_history) == 5 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {"temperature": "1"} + assert isinstance(sensor_test_history[0]["lu"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + assert sensor_test_history[1]["s"] == "off" + assert isinstance(sensor_test_history[1]["lu"], float) + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) + assert sensor_test_history[1]["a"] == {"temperature": "2"} + + assert sensor_test_history[4]["s"] == "on" + assert sensor_test_history[4]["a"] == {"temperature": "5"} + + await client.send_json( + { + "id": 4, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": True, + "no_attributes": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 4 + sensor_test_history = response["result"]["climate.test"] + + assert len(sensor_test_history) == 5 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {"temperature": "1"} + assert isinstance(sensor_test_history[0]["lu"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + assert sensor_test_history[1]["s"] == "off" + assert isinstance(sensor_test_history[1]["lu"], float) + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) + assert sensor_test_history[1]["a"] == {"temperature": "2"} + + assert sensor_test_history[2]["s"] == "off" + assert sensor_test_history[2]["a"] == {"temperature": "3"} + + assert sensor_test_history[3]["s"] == "off" + assert sensor_test_history[3]["a"] == {"temperature": "4"} + + assert sensor_test_history[4]["s"] == "on" + assert sensor_test_history[4]["a"] == {"temperature": "5"} + + # Test we impute the state time state + later = dt_util.utcnow() + await client.send_json( + { + "id": 5, + "type": "history/history_during_period", + "start_time": later.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": True, + "no_attributes": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 5 + sensor_test_history = response["result"]["climate.test"] + + assert len(sensor_test_history) == 1 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {"temperature": "5"} + assert sensor_test_history[0]["lu"] == later.timestamp() + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + +async def test_history_during_period_bad_start_time( + recorder_mock, hass, hass_ws_client +): + """Test history_during_period bad state time.""" + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/history_during_period", + "start_time": "cats", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_start_time" + + +async def test_history_during_period_bad_end_time(recorder_mock, hass, hass_ws_client): + """Test history_during_period bad end time.""" + now = dt_util.utcnow() + + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/history_during_period", + "start_time": now.isoformat(), + "end_time": "dogs", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_end_time" + + +async def test_history_stream_historical_only(recorder_mock, hass, hass_ws_client): + """Test history stream.""" + now = dt_util.utcnow() + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + CONF_INCLUDE: { + CONF_ENTITIES: sort_order, + CONF_DOMAINS: ["sensor"], + }, + } + }, + ) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.three", "off", attributes={"any": "changed"}) + sensor_three_last_updated = hass.states.get("sensor.three").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.four", "off", attributes={"any": "again"}) + sensor_four_last_updated = hass.states.get("sensor.four").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + end_time = dt_util.utcnow() + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": now.isoformat(), + "end_time": end_time.isoformat(), + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + + assert response == { + "event": { + "end_time": sensor_four_last_updated.timestamp(), + "start_time": now.timestamp(), + "states": { + "sensor.four": [ + {"a": {}, "lu": sensor_four_last_updated.timestamp(), "s": "off"} + ], + "sensor.one": [ + {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + ], + "sensor.three": [ + {"a": {}, "lu": sensor_three_last_updated.timestamp(), "s": "off"} + ], + "sensor.two": [ + {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + ], + }, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_significant_domain_historical_only( + recorder_mock, hass, hass_ws_client +): + """Test the stream with climate domain with historical states only.""" + now = dt_util.utcnow() + + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "on", attributes={"temperature": "1"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "off", attributes={"temperature": "2"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "off", attributes={"temperature": "3"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "off", attributes={"temperature": "4"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("climate.test", "on", attributes={"temperature": "5"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + } + ) + async with async_timeout.timeout(3): + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + async with async_timeout.timeout(3): + response = await client.receive_json() + assert response == { + "event": { + "end_time": now.timestamp(), + "start_time": now.timestamp(), + "states": {}, + }, + "id": 1, + "type": "event", + } + + end_time = dt_util.utcnow() + await client.send_json( + { + "id": 2, + "type": "history/stream", + "start_time": now.isoformat(), + "end_time": end_time.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + async with async_timeout.timeout(3): + response = await client.receive_json() + assert response["success"] + assert response["id"] == 2 + assert response["type"] == "result" + + async with async_timeout.timeout(3): + response = await client.receive_json() + sensor_test_history = response["event"]["states"]["climate.test"] + assert len(sensor_test_history) == 5 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {} + assert isinstance(sensor_test_history[0]["lu"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + assert "a" in sensor_test_history[1] + assert sensor_test_history[1]["s"] == "off" + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) + + assert sensor_test_history[4]["s"] == "on" + assert sensor_test_history[4]["a"] == {} + + await client.send_json( + { + "id": 3, + "type": "history/stream", + "start_time": now.isoformat(), + "end_time": end_time.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": False, + } + ) + async with async_timeout.timeout(3): + response = await client.receive_json() + assert response["success"] + assert response["id"] == 3 + assert response["type"] == "result" + + async with async_timeout.timeout(3): + response = await client.receive_json() + sensor_test_history = response["event"]["states"]["climate.test"] + + assert len(sensor_test_history) == 5 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {"temperature": "1"} + assert isinstance(sensor_test_history[0]["lu"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + assert sensor_test_history[1]["s"] == "off" + assert isinstance(sensor_test_history[1]["lu"], float) + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) + assert sensor_test_history[1]["a"] == {"temperature": "2"} + + assert sensor_test_history[4]["s"] == "on" + assert sensor_test_history[4]["a"] == {"temperature": "5"} + + await client.send_json( + { + "id": 4, + "type": "history/stream", + "start_time": now.isoformat(), + "end_time": end_time.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": True, + "no_attributes": False, + } + ) + async with async_timeout.timeout(3): + response = await client.receive_json() + assert response["success"] + assert response["id"] == 4 + assert response["type"] == "result" + + async with async_timeout.timeout(3): + response = await client.receive_json() + sensor_test_history = response["event"]["states"]["climate.test"] + + assert len(sensor_test_history) == 5 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {"temperature": "1"} + assert isinstance(sensor_test_history[0]["lu"], float) + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + assert sensor_test_history[1]["s"] == "off" + assert isinstance(sensor_test_history[1]["lu"], float) + assert "lc" not in sensor_test_history[1] # skipped if the same a last_updated (lu) + assert sensor_test_history[1]["a"] == {"temperature": "2"} + + assert sensor_test_history[2]["s"] == "off" + assert sensor_test_history[2]["a"] == {"temperature": "3"} + + assert sensor_test_history[3]["s"] == "off" + assert sensor_test_history[3]["a"] == {"temperature": "4"} + + assert sensor_test_history[4]["s"] == "on" + assert sensor_test_history[4]["a"] == {"temperature": "5"} + + # Test we impute the state time state + later = dt_util.utcnow() + await client.send_json( + { + "id": 5, + "type": "history/stream", + "start_time": later.isoformat(), + "end_time": later.isoformat(), + "entity_ids": ["climate.test"], + "include_start_time_state": True, + "significant_changes_only": True, + "no_attributes": False, + } + ) + async with async_timeout.timeout(3): + response = await client.receive_json() + assert response["success"] + assert response["id"] == 5 + assert response["type"] == "result" + + async with async_timeout.timeout(3): + response = await client.receive_json() + sensor_test_history = response["event"]["states"]["climate.test"] + + assert len(sensor_test_history) == 1 + + assert sensor_test_history[0]["s"] == "on" + assert sensor_test_history[0]["a"] == {"temperature": "5"} + assert sensor_test_history[0]["lu"] == later.timestamp() + assert "lc" not in sensor_test_history[0] # skipped if the same a last_updated (lu) + + +async def test_history_stream_bad_start_time(recorder_mock, hass, hass_ws_client): + """Test history stream bad state time.""" + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": "cats", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_start_time" + + +async def test_history_stream_end_time_before_start_time( + recorder_mock, hass, hass_ws_client +): + """Test history stream with an end_time before the start_time.""" + end_time = dt_util.utcnow() - timedelta(seconds=2) + start_time = dt_util.utcnow() - timedelta(seconds=1) + + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": start_time.isoformat(), + "end_time": end_time.isoformat(), + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_end_time" + + +async def test_history_stream_bad_end_time(recorder_mock, hass, hass_ws_client): + """Test history stream bad end time.""" + now = dt_util.utcnow() + + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": now.isoformat(), + "end_time": "dogs", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_end_time" + + +async def test_history_stream_live_no_attributes_minimal_response( + recorder_mock, hass, hass_ws_client +): + """Test history stream with history and live data and no_attributes and minimal_response.""" + now = dt_util.utcnow() + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + CONF_INCLUDE: { + CONF_ENTITIES: sort_order, + CONF_DOMAINS: ["sensor"], + }, + } + }, + ) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": now.isoformat(), + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + ], + "sensor.two": [ + {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "one", attributes={"any": "attr"}) + hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + response = await client.receive_json() + assert response == { + "event": { + "states": { + "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], + "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], + }, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_live(recorder_mock, hass, hass_ws_client): + """Test history stream with history and live data.""" + now = dt_util.utcnow() + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + CONF_INCLUDE: { + CONF_ENTITIES: sort_order, + CONF_DOMAINS: ["sensor"], + }, + } + }, + ) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": now.isoformat(), + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": False, + "minimal_response": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + { + "a": {"any": "attr"}, + "lu": sensor_one_last_updated.timestamp(), + "s": "on", + } + ], + "sensor.two": [ + { + "a": {"any": "attr"}, + "lu": sensor_two_last_updated.timestamp(), + "s": "off", + } + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"diff": "attr"}) + hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_one_last_changed = hass.states.get("sensor.one").last_changed + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + response = await client.receive_json() + assert response == { + "event": { + "states": { + "sensor.one": [ + { + "lc": sensor_one_last_changed.timestamp(), + "lu": sensor_one_last_updated.timestamp(), + "s": "on", + "a": {"diff": "attr"}, + } + ], + "sensor.two": [ + { + "lu": sensor_two_last_updated.timestamp(), + "s": "two", + "a": {"any": "attr"}, + } + ], + }, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_live_minimal_response( + recorder_mock, hass, hass_ws_client +): + """Test history stream with history and live data and minimal_response.""" + now = dt_util.utcnow() + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + CONF_INCLUDE: { + CONF_ENTITIES: sort_order, + CONF_DOMAINS: ["sensor"], + }, + } + }, + ) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": now.isoformat(), + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": False, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + { + "a": {"any": "attr"}, + "lu": sensor_one_last_updated.timestamp(), + "s": "on", + } + ], + "sensor.two": [ + { + "a": {"any": "attr"}, + "lu": sensor_two_last_updated.timestamp(), + "s": "off", + } + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"diff": "attr"}) + hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) + # Only sensor.two has changed + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + hass.states.async_remove("sensor.one") + hass.states.async_remove("sensor.two") + await async_recorder_block_till_done(hass) + + response = await client.receive_json() + assert response == { + "event": { + "states": { + "sensor.two": [ + { + "lu": sensor_two_last_updated.timestamp(), + "s": "two", + "a": {"any": "attr"}, + } + ], + }, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_live_no_attributes(recorder_mock, hass, hass_ws_client): + """Test history stream with history and live data and no_attributes.""" + now = dt_util.utcnow() + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + CONF_INCLUDE: { + CONF_ENTITIES: sort_order, + CONF_DOMAINS: ["sensor"], + }, + } + }, + ) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "start_time": now.isoformat(), + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": False, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + ], + "sensor.two": [ + {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "one", attributes={"diff": "attr"}) + hass.states.async_set("sensor.two", "two", attributes={"diff": "attr"}) + await async_recorder_block_till_done(hass) + + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + response = await client.receive_json() + assert response == { + "event": { + "states": { + "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], + "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], + }, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_live_no_attributes_minimal_response_specific_entities( + recorder_mock, hass, hass_ws_client +): + """Test history stream with history and live data and no_attributes and minimal_response with specific entities.""" + now = dt_util.utcnow() + wanted_entities = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + {history.DOMAIN: {}}, + ) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "entity_ids": wanted_entities, + "start_time": now.isoformat(), + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + ], + "sensor.two": [ + {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "one", attributes={"any": "attr"}) + hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + response = await client.receive_json() + assert response == { + "event": { + "states": { + "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], + "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], + }, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_live_with_future_end_time( + recorder_mock, hass, hass_ws_client +): + """Test history stream with history and live data with future end time.""" + now = dt_util.utcnow() + wanted_entities = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + {history.DOMAIN: {}}, + ) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + future = now + timedelta(seconds=10) + + client = await hass_ws_client() + init_listeners = hass.bus.async_listeners() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "entity_ids": wanted_entities, + "start_time": now.isoformat(), + "end_time": future.isoformat(), + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + ], + "sensor.two": [ + {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "one", attributes={"any": "attr"}) + hass.states.async_set("sensor.two", "two", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + response = await client.receive_json() + assert response == { + "event": { + "states": { + "sensor.one": [{"lu": sensor_one_last_updated.timestamp(), "s": "one"}], + "sensor.two": [{"lu": sensor_two_last_updated.timestamp(), "s": "two"}], + }, + }, + "id": 1, + "type": "event", + } + + async_fire_time_changed(hass, future + timedelta(seconds=1)) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "future", attributes={"any": "attr"}) + # Check our listener got unsubscribed + await async_wait_recording_done(hass) + await async_recorder_block_till_done(hass) + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) + + +@pytest.mark.parametrize("include_start_time_state", (True, False)) +async def test_history_stream_before_history_starts( + recorder_mock, hass, hass_ws_client, include_start_time_state +): + """Test history stream before we have history.""" + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + CONF_INCLUDE: { + CONF_ENTITIES: sort_order, + CONF_DOMAINS: ["sensor"], + }, + } + }, + ) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + await async_wait_recording_done(hass) + far_past = dt_util.utcnow() - timedelta(days=1000) + far_past_end = far_past + timedelta(seconds=10) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "entity_ids": ["sensor.one"], + "start_time": far_past.isoformat(), + "end_time": far_past_end.isoformat(), + "include_start_time_state": include_start_time_state, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + assert response == { + "event": { + "end_time": far_past_end.timestamp(), + "start_time": far_past.timestamp(), + "states": {}, + }, + "id": 1, + "type": "event", + } + + +async def test_history_stream_for_entity_with_no_possible_changes( + recorder_mock, hass, hass_ws_client +): + """Test history stream for future with no possible changes where end time is less than or equal to now.""" + sort_order = ["sensor.two", "sensor.four", "sensor.one"] + await async_setup_component( + hass, + "history", + { + history.DOMAIN: { + history.CONF_ORDER: True, + CONF_INCLUDE: { + CONF_ENTITIES: sort_order, + CONF_DOMAINS: ["sensor"], + }, + } + }, + ) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + await async_wait_recording_done(hass) + + last_updated = hass.states.get("sensor.one").last_updated + start_time = last_updated + timedelta(seconds=10) + end_time = start_time + timedelta(seconds=10) + + with freeze_time(end_time): + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/stream", + "entity_ids": ["sensor.one"], + "start_time": start_time.isoformat(), + "end_time": end_time.isoformat(), + "include_start_time_state": False, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + assert response == { + "event": { + "end_time": end_time.timestamp(), + "start_time": start_time.timestamp(), + "states": {}, + }, + "id": 1, + "type": "event", + } + + +async def test_overflow_queue(recorder_mock, hass, hass_ws_client): + """Test overflowing the history stream queue.""" + now = dt_util.utcnow() + wanted_entities = ["sensor.two", "sensor.four", "sensor.one"] + with patch.object(websocket_api, "MAX_PENDING_HISTORY_STATES", 5): + await async_setup_component( + hass, + "history", + {history.DOMAIN: {}}, + ) + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.one", "on", attributes={"any": "attr"}) + sensor_one_last_updated = hass.states.get("sensor.one").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.two", "off", attributes={"any": "attr"}) + sensor_two_last_updated = hass.states.get("sensor.two").last_updated + await async_recorder_block_till_done(hass) + hass.states.async_set("switch.excluded", "off", attributes={"any": "again"}) + await async_wait_recording_done(hass) + + await async_wait_recording_done(hass) + + client = await hass_ws_client() + init_listeners = hass.bus.async_listeners() + + await client.send_json( + { + "id": 1, + "type": "history/stream", + "entity_ids": wanted_entities, + "start_time": now.isoformat(), + "include_start_time_state": True, + "significant_changes_only": False, + "no_attributes": True, + "minimal_response": True, + } + ) + response = await client.receive_json() + assert response["success"] + assert response["id"] == 1 + assert response["type"] == "result" + + response = await client.receive_json() + first_end_time = sensor_two_last_updated.timestamp() + + assert response == { + "event": { + "end_time": first_end_time, + "start_time": now.timestamp(), + "states": { + "sensor.one": [ + {"a": {}, "lu": sensor_one_last_updated.timestamp(), "s": "on"} + ], + "sensor.two": [ + {"a": {}, "lu": sensor_two_last_updated.timestamp(), "s": "off"} + ], + }, + }, + "id": 1, + "type": "event", + } + + await async_recorder_block_till_done(hass) + # Overflow the queue + for val in range(10): + hass.states.async_set("sensor.one", str(val), attributes={"any": "attr"}) + hass.states.async_set("sensor.two", str(val), attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + + assert listeners_without_writes( + hass.bus.async_listeners() + ) == listeners_without_writes(init_listeners) diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 6bae61b5fd8..3cb32608159 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -1,5 +1,5 @@ """The test for the History Statistics sensor platform.""" -# pylint: disable=protected-access + from datetime import timedelta import unittest from unittest.mock import patch diff --git a/tests/components/home_plus_control/test_config_flow.py b/tests/components/home_plus_control/test_config_flow.py index f4f7e1143a5..60dcbeef545 100644 --- a/tests/components/home_plus_control/test_config_flow.py +++ b/tests/components/home_plus_control/test_config_flow.py @@ -36,7 +36,7 @@ async def test_full_flow( "home_plus_control", context={"source": config_entries.SOURCE_USER} ) - state = config_entry_oauth2_flow._encode_jwt( # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( hass, { "flow_id": result["flow_id"], @@ -154,7 +154,7 @@ async def test_abort_if_invalid_token( "home_plus_control", context={"source": config_entries.SOURCE_USER} ) - state = config_entry_oauth2_flow._encode_jwt( # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( hass, { "flow_id": result["flow_id"], diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index 0c980bcf07e..a405a7a5672 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -1,5 +1,5 @@ """The tests for Core components.""" -# pylint: disable=protected-access + import asyncio import unittest from unittest.mock import Mock, patch @@ -114,7 +114,6 @@ def reload_core_config(hass): class TestComponentsCore(unittest.TestCase): """Test homeassistant.components module.""" - # pylint: disable=invalid-name def setUp(self): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() diff --git a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py index 0d703b04bfb..abe66d35a96 100644 --- a/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py +++ b/tests/components/homeassistant_hardware/test_silabs_multiprotocol_addon.py @@ -20,9 +20,6 @@ from tests.common import MockConfigEntry, MockModule, mock_integration, mock_pla TEST_DOMAIN = "test" -pytest.skip(reason="Temporarily disabled", allow_module_level=True) - - class TestConfigFlow(ConfigFlow, domain=TEST_DOMAIN): """Handle a config flow for the silabs multiprotocol add-on.""" diff --git a/tests/components/homeassistant_sky_connect/__init__.py b/tests/components/homeassistant_sky_connect/__init__.py index 90cd1594710..3a55fec688f 100644 --- a/tests/components/homeassistant_sky_connect/__init__.py +++ b/tests/components/homeassistant_sky_connect/__init__.py @@ -1 +1 @@ -"""Tests for the Home Assistant Sky Connect integration.""" +"""Tests for the Home Assistant SkyConnect integration.""" diff --git a/tests/components/homeassistant_sky_connect/conftest.py b/tests/components/homeassistant_sky_connect/conftest.py index f7f0bb8d128..7fcc1f86880 100644 --- a/tests/components/homeassistant_sky_connect/conftest.py +++ b/tests/components/homeassistant_sky_connect/conftest.py @@ -1,4 +1,4 @@ -"""Test fixtures for the Home Assistant Sky Connect integration.""" +"""Test fixtures for the Home Assistant SkyConnect integration.""" from collections.abc import Generator from unittest.mock import MagicMock, patch diff --git a/tests/components/homeassistant_sky_connect/test_config_flow.py b/tests/components/homeassistant_sky_connect/test_config_flow.py index 931abc69c4a..6ef3d13636e 100644 --- a/tests/components/homeassistant_sky_connect/test_config_flow.py +++ b/tests/components/homeassistant_sky_connect/test_config_flow.py @@ -1,9 +1,7 @@ -"""Test the Home Assistant Sky Connect config flow.""" +"""Test the Home Assistant SkyConnect config flow.""" import copy from unittest.mock import Mock, patch -import pytest - from homeassistant.components import homeassistant_sky_connect, usb from homeassistant.components.homeassistant_sky_connect.const import DOMAIN from homeassistant.components.zha.core.const import ( @@ -46,7 +44,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: } assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "Home Assistant Sky Connect" + assert result["title"] == "Home Assistant SkyConnect" assert result["data"] == expected_data assert result["options"] == {} assert len(mock_setup_entry.mock_calls) == 1 @@ -54,7 +52,7 @@ async def test_config_flow(hass: HomeAssistant) -> None: config_entry = hass.config_entries.async_entries(DOMAIN)[0] assert config_entry.data == expected_data assert config_entry.options == {} - assert config_entry.title == "Home Assistant Sky Connect" + assert config_entry.title == "Home Assistant SkyConnect" assert ( config_entry.unique_id == f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}" @@ -68,7 +66,7 @@ async def test_config_flow_unique_id(hass: HomeAssistant) -> None: data={}, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", ) config_entry.add_to_hass(hass) @@ -93,7 +91,7 @@ async def test_config_flow_multiple_entries(hass: HomeAssistant) -> None: data={}, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", ) config_entry.add_to_hass(hass) @@ -119,7 +117,7 @@ async def test_config_flow_update_device(hass: HomeAssistant) -> None: data={}, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", ) config_entry.add_to_hass(hass) @@ -152,7 +150,6 @@ async def test_config_flow_update_device(hass: HomeAssistant) -> None: assert len(mock_unload_entry.mock_calls) == 1 -@pytest.mark.skip(reason="Temporarily disabled") async def test_option_flow_install_multi_pan_addon( hass: HomeAssistant, addon_store_info, @@ -176,7 +173,7 @@ async def test_option_flow_install_multi_pan_addon( }, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", ) config_entry.add_to_hass(hass) @@ -243,7 +240,6 @@ def mock_detect_radio_type(radio_type=RadioType.ezsp, ret=True): return detect -@pytest.mark.skip(reason="Temporarily disabled") @patch( "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", mock_detect_radio_type(), @@ -271,7 +267,7 @@ async def test_option_flow_install_multi_pan_addon_zha( }, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", unique_id=f"{USB_DATA.vid}:{USB_DATA.pid}_{USB_DATA.serial_number}_{USB_DATA.manufacturer}_{USB_DATA.description}", ) config_entry.add_to_hass(hass) diff --git a/tests/components/homeassistant_sky_connect/test_hardware.py b/tests/components/homeassistant_sky_connect/test_hardware.py index 09e650388c5..b9489c7ebad 100644 --- a/tests/components/homeassistant_sky_connect/test_hardware.py +++ b/tests/components/homeassistant_sky_connect/test_hardware.py @@ -1,4 +1,4 @@ -"""Test the Home Assistant Sky Connect hardware platform.""" +"""Test the Home Assistant SkyConnect hardware platform.""" from unittest.mock import patch from homeassistant.components.homeassistant_sky_connect.const import DOMAIN @@ -38,7 +38,7 @@ async def test_hardware_info( data=CONFIG_ENTRY_DATA, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", unique_id="unique_1", ) config_entry.add_to_hass(hass) @@ -46,7 +46,7 @@ async def test_hardware_info( data=CONFIG_ENTRY_DATA_2, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", unique_id="unique_2", ) config_entry_2.add_to_hass(hass) @@ -76,7 +76,7 @@ async def test_hardware_info( "manufacturer": "bla_manufacturer", "description": "bla_description", }, - "name": "Home Assistant Sky Connect", + "name": "Home Assistant SkyConnect", "url": None, }, { @@ -89,7 +89,7 @@ async def test_hardware_info( "manufacturer": "bla_manufacturer_2", "description": "bla_description_2", }, - "name": "Home Assistant Sky Connect", + "name": "Home Assistant SkyConnect", "url": None, }, ] diff --git a/tests/components/homeassistant_sky_connect/test_init.py b/tests/components/homeassistant_sky_connect/test_init.py index c47066e8bc9..7fc9069c30b 100644 --- a/tests/components/homeassistant_sky_connect/test_init.py +++ b/tests/components/homeassistant_sky_connect/test_init.py @@ -1,4 +1,4 @@ -"""Test the Home Assistant Sky Connect integration.""" +"""Test the Home Assistant SkyConnect integration.""" from collections.abc import Generator from typing import Any from unittest.mock import MagicMock, Mock, patch @@ -64,7 +64,7 @@ async def test_setup_entry( data=CONFIG_ENTRY_DATA, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", ) config_entry.add_to_hass(hass) with patch( @@ -112,7 +112,7 @@ async def test_setup_zha( data=CONFIG_ENTRY_DATA, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", ) config_entry.add_to_hass(hass) with patch( @@ -163,7 +163,7 @@ async def test_setup_zha_multipan( data=CONFIG_ENTRY_DATA, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", ) config_entry.add_to_hass(hass) with patch( @@ -200,7 +200,7 @@ async def test_setup_zha_multipan( "radio_type": "ezsp", } assert config_entry.options == {} - assert config_entry.title == "Sky Connect Multi-PAN" + assert config_entry.title == "SkyConnect Multi-PAN" async def test_setup_zha_multipan_other_device( @@ -264,7 +264,7 @@ async def test_setup_entry_wait_usb(hass: HomeAssistant) -> None: data=CONFIG_ENTRY_DATA, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", ) config_entry.add_to_hass(hass) with patch( @@ -295,7 +295,7 @@ async def test_setup_entry_addon_info_fails( data=CONFIG_ENTRY_DATA, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", ) config_entry.add_to_hass(hass) with patch( @@ -324,7 +324,7 @@ async def test_setup_entry_addon_not_running( data=CONFIG_ENTRY_DATA, domain=DOMAIN, options={}, - title="Home Assistant Sky Connect", + title="Home Assistant SkyConnect", ) config_entry.add_to_hass(hass) with patch( diff --git a/tests/components/homeassistant_yellow/test_config_flow.py b/tests/components/homeassistant_yellow/test_config_flow.py index 3d846501524..53d1c5e974d 100644 --- a/tests/components/homeassistant_yellow/test_config_flow.py +++ b/tests/components/homeassistant_yellow/test_config_flow.py @@ -1,8 +1,6 @@ """Test the Home Assistant Yellow config flow.""" from unittest.mock import Mock, patch -import pytest - from homeassistant.components.homeassistant_yellow.const import DOMAIN from homeassistant.components.zha.core.const import DOMAIN as ZHA_DOMAIN from homeassistant.core import HomeAssistant @@ -61,7 +59,6 @@ async def test_config_flow_single_entry(hass: HomeAssistant) -> None: mock_setup_entry.assert_not_called() -@pytest.mark.skip(reason="Temporarily disabled") async def test_option_flow_install_multi_pan_addon( hass: HomeAssistant, addon_store_info, @@ -130,7 +127,6 @@ async def test_option_flow_install_multi_pan_addon( assert result["type"] == FlowResultType.CREATE_ENTRY -@pytest.mark.skip(reason="Temporarily disabled") async def test_option_flow_install_multi_pan_addon_zha( hass: HomeAssistant, addon_store_info, diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 320407f480a..6ef7202fb4e 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -30,8 +30,7 @@ from homeassistant.const import ( LIGHT_LUX, PERCENTAGE, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) from homeassistant.core import State @@ -269,13 +268,13 @@ def test_type_media_player(type_name, entity_id, state, attrs, config): "TemperatureSensor", "sensor.temperature", "23", - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ), ( "TemperatureSensor", "sensor.temperature", "74", - {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT}, ), ], ) diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 28dfe04932f..8baf8ee9df9 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -31,8 +31,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) from homeassistant.core import CoreState from homeassistant.helpers import entity_registry as er @@ -56,21 +55,25 @@ async def test_temperature(hass, hk_driver): assert acc.char_temp.properties[key] == value hass.states.async_set( - entity_id, STATE_UNKNOWN, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + entity_id, STATE_UNKNOWN, {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) await hass.async_block_till_done() assert acc.char_temp.value == 0.0 - hass.states.async_set(entity_id, "20", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + hass.states.async_set( + entity_id, "20", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} + ) await hass.async_block_till_done() assert acc.char_temp.value == 20 - hass.states.async_set(entity_id, "0", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}) + hass.states.async_set( + entity_id, "0", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} + ) await hass.async_block_till_done() assert acc.char_temp.value == 0 hass.states.async_set( - entity_id, "75.2", {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT} + entity_id, "75.2", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT} ) await hass.async_block_till_done() assert acc.char_temp.value == 24 diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 902c70aba5d..1a18c7fe805 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -68,8 +68,7 @@ from homeassistant.const import ( ATTR_TEMPERATURE, CONF_TEMPERATURE_UNIT, EVENT_HOMEASSISTANT_START, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) from homeassistant.core import CoreState from homeassistant.helpers import entity_registry as er @@ -743,6 +742,29 @@ async def test_thermostat_humidity(hass, hk_driver, events): assert events[-1].data[ATTR_VALUE] == "35%" +async def test_thermostat_humidity_with_target_humidity(hass, hk_driver, events): + """Test if accessory and HA are updated accordingly with humidity without target hudmidity. + + This test is for thermostats that do not support target humidity but + have a current humidity sensor. + """ + entity_id = "climate.test" + + # support_auto = True + hass.states.async_set(entity_id, HVACMode.OFF, {ATTR_CURRENT_HUMIDITY: 40}) + 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_current_humidity.value == 40 + hass.states.async_set(entity_id, HVACMode.HEAT_COOL, {ATTR_CURRENT_HUMIDITY: 65}) + await hass.async_block_till_done() + assert acc.char_current_humidity.value == 65 + + async def test_thermostat_power_state(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" @@ -877,7 +899,9 @@ async def test_thermostat_fahrenheit(hass, hk_driver, events): }, ) await hass.async_block_till_done() - with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT, new=TEMP_FAHRENHEIT): + with patch.object( + hass.config.units, CONF_TEMPERATURE_UNIT, new=UnitOfTemperature.FAHRENHEIT + ): acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run() @@ -986,7 +1010,7 @@ async def test_thermostat_get_temperature_range(hass, hk_driver): await hass.async_block_till_done() assert acc.get_temperature_range() == (20, 25) - acc._unit = TEMP_FAHRENHEIT + acc._unit = UnitOfTemperature.FAHRENHEIT hass.states.async_set( entity_id, HVACMode.OFF, {ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70} ) @@ -1761,7 +1785,7 @@ async def test_water_heater(hass, hk_driver, events): assert call_set_temperature[0].data[ATTR_TEMPERATURE] == 52.0 assert acc.char_target_temp.value == 52.0 assert len(events) == 1 - assert events[-1].data[ATTR_VALUE] == f"52.0{TEMP_CELSIUS}" + assert events[-1].data[ATTR_VALUE] == f"52.0{UnitOfTemperature.CELSIUS}" acc.char_target_heat_cool.client_update_value(1) await hass.async_block_till_done() @@ -1779,7 +1803,9 @@ async def test_water_heater_fahrenheit(hass, hk_driver, events): hass.states.async_set(entity_id, HVACMode.HEAT) await hass.async_block_till_done() - with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT, new=TEMP_FAHRENHEIT): + with patch.object( + hass.config.units, CONF_TEMPERATURE_UNIT, new=UnitOfTemperature.FAHRENHEIT + ): acc = WaterHeater(hass, hk_driver, "WaterHeater", entity_id, 2, None) await acc.run() await hass.async_block_till_done() @@ -1819,7 +1845,7 @@ async def test_water_heater_get_temperature_range(hass, hk_driver): await hass.async_block_till_done() assert acc.get_temperature_range() == (20, 25) - acc._unit = TEMP_FAHRENHEIT + acc._unit = UnitOfTemperature.FAHRENHEIT hass.states.async_set( entity_id, HVACMode.OFF, {ATTR_MIN_TEMP: 60, ATTR_MAX_TEMP: 70} ) diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index f3811ce34c5..b7031cedb37 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -48,8 +48,7 @@ from homeassistant.const import ( CONF_PORT, CONF_TYPE, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) from homeassistant.core import State @@ -211,14 +210,14 @@ def test_cleanup_name_for_homekit(): def test_temperature_to_homekit(): """Test temperature conversion from HA to HomeKit.""" - assert temperature_to_homekit(20.46, TEMP_CELSIUS) == 20.5 - assert temperature_to_homekit(92.1, TEMP_FAHRENHEIT) == 33.4 + assert temperature_to_homekit(20.46, UnitOfTemperature.CELSIUS) == 20.5 + assert temperature_to_homekit(92.1, UnitOfTemperature.FAHRENHEIT) == 33.4 def test_temperature_to_states(): """Test temperature conversion from HomeKit to HA.""" - assert temperature_to_states(20, TEMP_CELSIUS) == 20.0 - assert temperature_to_states(20.2, TEMP_FAHRENHEIT) == 68.5 + assert temperature_to_states(20, UnitOfTemperature.CELSIUS) == 20.0 + assert temperature_to_states(20.2, UnitOfTemperature.FAHRENHEIT) == 68.5 def test_density_to_air_quality(): diff --git a/tests/components/homekit_controller/test_device_trigger.py b/tests/components/homekit_controller/test_device_trigger.py index 6f17f5db786..d3862c5ac9f 100644 --- a/tests/components/homekit_controller/test_device_trigger.py +++ b/tests/components/homekit_controller/test_device_trigger.py @@ -20,7 +20,6 @@ from tests.common import ( from tests.components.blueprint.conftest import stub_blueprint_populate # noqa: F401 -# pylint: disable=redefined-outer-name @pytest.fixture def calls(hass): """Track calls to a mock service.""" diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index c720df4a1bb..c033670efa6 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -37,9 +37,7 @@ def mock_connection_fixture() -> AsyncConnection: def _rest_call_side_effect(path, body=None): return path, body - connection._restCall.side_effect = ( # pylint: disable=protected-access - _rest_call_side_effect - ) + connection._restCall.side_effect = _rest_call_side_effect connection.api_call = AsyncMock(return_value=True) connection.init = AsyncMock(side_effect=True) diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index 22d950d0817..8e9b27d59ba 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -62,9 +62,7 @@ async def async_manipulate_test_data( fire_target = hmip_device if fire_device is None else fire_device if isinstance(fire_target, AsyncHome): - fire_target.fire_update_event( - fire_target._rawJSONData # pylint: disable=protected-access - ) + fire_target.fire_update_event(fire_target._rawJSONData) else: fire_target.fire_update_event() @@ -206,9 +204,7 @@ class HomeTemplate(Home): def _get_mock(instance): """Create a mock and copy instance attributes over mock.""" if isinstance(instance, Mock): - instance.__dict__.update( - instance._mock_wraps.__dict__ # pylint: disable=protected-access - ) + instance.__dict__.update(instance._mock_wraps.__dict__) return instance mock = Mock(spec=instance, wraps=instance) diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py index 92782f2cbb2..c5e17d6718f 100644 --- a/tests/components/homematicip_cloud/test_alarm_control_panel.py +++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py @@ -18,7 +18,7 @@ async def _async_manipulate_security_zones( hass, home, internal_active=False, external_active=False, alarm_triggered=False ): """Set new values on hmip security zones.""" - json = home._rawJSONData # pylint: disable=protected-access + json = home._rawJSONData json["functionalHomes"]["SECURITY_AND_ALARM"]["alarmActive"] = alarm_triggered external_zone_id = json["functionalHomes"]["SECURITY_AND_ALARM"]["securityZones"][ "EXTERNAL" diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 1f7b91d59a6..3d6642dfdad 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -408,7 +408,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_duration" assert home.mock_calls[-1][1] == (60,) - assert len(home._connection.mock_calls) == 1 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 1 await hass.services.async_call( "homematicip_cloud", @@ -418,7 +418,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_duration" assert home.mock_calls[-1][1] == (60,) - assert len(home._connection.mock_calls) == 2 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 2 await hass.services.async_call( "homematicip_cloud", @@ -428,7 +428,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_period" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),) - assert len(home._connection.mock_calls) == 3 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 3 await hass.services.async_call( "homematicip_cloud", @@ -438,7 +438,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_period" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),) - assert len(home._connection.mock_calls) == 4 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 4 await hass.services.async_call( "homematicip_cloud", @@ -448,7 +448,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_vacation" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5) - assert len(home._connection.mock_calls) == 5 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 5 await hass.services.async_call( "homematicip_cloud", @@ -458,7 +458,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_vacation" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5) - assert len(home._connection.mock_calls) == 6 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 6 await hass.services.async_call( "homematicip_cloud", @@ -468,14 +468,14 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "deactivate_absence" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 7 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 7 await hass.services.async_call( "homematicip_cloud", "deactivate_eco_mode", blocking=True ) assert home.mock_calls[-1][0] == "deactivate_absence" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 8 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 8 await hass.services.async_call( "homematicip_cloud", @@ -485,14 +485,14 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "deactivate_vacation" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 9 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 9 await hass.services.async_call( "homematicip_cloud", "deactivate_vacation", blocking=True ) assert home.mock_calls[-1][0] == "deactivate_vacation" assert home.mock_calls[-1][1] == () - assert len(home._connection.mock_calls) == 10 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 10 not_existing_hap_id = "5555F7110000000000000001" await hass.services.async_call( @@ -504,7 +504,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): assert home.mock_calls[-1][0] == "deactivate_vacation" assert home.mock_calls[-1][1] == () # There is no further call on connection. - assert len(home._connection.mock_calls) == 10 # pylint: disable=protected-access + assert len(home._connection.mock_calls) == 10 async def test_hmip_heating_group_services(hass, default_mock_hap_factory): @@ -529,9 +529,7 @@ async def test_hmip_heating_group_services(hass, default_mock_hap_factory): ) assert hmip_device.mock_calls[-1][0] == "set_active_profile" assert hmip_device.mock_calls[-1][1] == (1,) - assert ( - len(hmip_device._connection.mock_calls) == 2 # pylint: disable=protected-access - ) + assert len(hmip_device._connection.mock_calls) == 2 await hass.services.async_call( "homematicip_cloud", @@ -541,6 +539,4 @@ async def test_hmip_heating_group_services(hass, default_mock_hap_factory): ) assert hmip_device.mock_calls[-1][0] == "set_active_profile" assert hmip_device.mock_calls[-1][1] == (1,) - assert ( - len(hmip_device._connection.mock_calls) == 4 # pylint: disable=protected-access - ) + assert len(hmip_device._connection.mock_calls) == 4 diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 597a82ea810..af4ba4fdb1a 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -183,7 +183,7 @@ async def test_hap_reconnected(hass, default_mock_hap_factory): ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_UNAVAILABLE - mock_hap._accesspoint_connected = False # pylint: disable=protected-access + mock_hap._accesspoint_connected = False await async_manipulate_test_data(hass, mock_hap.home, "connected", True) await hass.async_block_till_done() ha_state = hass.states.get(entity_id) diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index a8b3229e8db..2d7d793f54e 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -23,6 +23,8 @@ from homeassistant.exceptions import ConfigEntryNotReady from .helper import HAPID, HAPPIN +from tests.common import MockConfigEntry + async def test_auth_setup(hass): """Test auth setup for client registration.""" @@ -71,18 +73,19 @@ async def test_auth_auth_check_and_register_with_exception(hass): assert await hmip_auth.async_register() is False -async def test_hap_setup_works(): +async def test_hap_setup_works(hass): """Test a successful setup of a accesspoint.""" - hass = Mock() - entry = Mock() + # This test should not be accessing the integration internals + entry = MockConfigEntry( + domain=HMIPC_DOMAIN, + data={HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "123", HMIPC_NAME: "hmip"}, + ) home = Mock() - entry.data = {HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "123", HMIPC_NAME: "hmip"} hap = HomematicipHAP(hass, entry) with patch.object(hap, "get_hap", return_value=home): assert await hap.async_setup() assert hap.home is home - assert len(hass.config_entries.async_setup_platforms.mock_calls) == 1 async def test_hap_setup_connection_error(): diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index 33da0f217ae..9b8630e96d4 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -24,12 +24,13 @@ from homeassistant.components.homematicip_cloud.sensor import ( from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, - LENGTH_MILLIMETERS, LIGHT_LUX, PERCENTAGE, - POWER_WATT, - SPEED_KILOMETERS_PER_HOUR, - TEMP_CELSIUS, + STATE_UNKNOWN, + UnitOfLength, + UnitOfPower, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.setup import async_setup_component @@ -82,7 +83,7 @@ async def test_hmip_heating_thermostat(hass, default_mock_hap_factory): await async_manipulate_test_data(hass, hmip_device, "valveState", "nn") ha_state = hass.states.get(entity_id) - assert ha_state.state == "nn" + assert ha_state.state == STATE_UNKNOWN await async_manipulate_test_data( hass, hmip_device, "valveState", ValveState.ADAPTION_DONE @@ -132,7 +133,7 @@ async def test_hmip_temperature_sensor1(hass, default_mock_hap_factory): ) assert ha_state.state == "21.0" - assert ha_state.attributes["unit_of_measurement"] == TEMP_CELSIUS + assert ha_state.attributes["unit_of_measurement"] == UnitOfTemperature.CELSIUS await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 23.5) ha_state = hass.states.get(entity_id) assert ha_state.state == "23.5" @@ -157,7 +158,7 @@ async def test_hmip_temperature_sensor2(hass, default_mock_hap_factory): ) assert ha_state.state == "20.0" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS await async_manipulate_test_data(hass, hmip_device, "valveActualTemperature", 23.5) ha_state = hass.states.get(entity_id) assert ha_state.state == "23.5" @@ -182,7 +183,7 @@ async def test_hmip_temperature_sensor3(hass, default_mock_hap_factory): ) assert ha_state.state == "23.3" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 23.5) ha_state = hass.states.get(entity_id) assert ha_state.state == "23.5" @@ -227,7 +228,7 @@ async def test_hmip_thermostat_evo_temperature(hass, default_mock_hap_factory): ) assert ha_state.state == "18.7" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS await async_manipulate_test_data(hass, hmip_device, "valveActualTemperature", 23.5) ha_state = hass.states.get(entity_id) assert ha_state.state == "23.5" @@ -251,7 +252,7 @@ async def test_hmip_power_sensor(hass, default_mock_hap_factory): ) assert ha_state.state == "0.0" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfPower.WATT await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 23.5) ha_state = hass.states.get(entity_id) assert ha_state.state == "23.5" @@ -331,7 +332,9 @@ async def test_hmip_windspeed_sensor(hass, default_mock_hap_factory): ) assert ha_state.state == "2.6" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == SPEED_KILOMETERS_PER_HOUR + assert ( + ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfSpeed.KILOMETERS_PER_HOUR + ) await async_manipulate_test_data(hass, hmip_device, "windSpeed", 9.4) ha_state = hass.states.get(entity_id) assert ha_state.state == "9.4" @@ -351,7 +354,7 @@ async def test_hmip_windspeed_sensor(hass, default_mock_hap_factory): 205: "SSW", 227.5: "SW", 250: "WSW", - 272.5: POWER_WATT, + 272.5: UnitOfPower.WATT, 295: "WNW", 317.5: "NW", 340: "NNW", @@ -378,7 +381,7 @@ async def test_hmip_today_rain_sensor(hass, default_mock_hap_factory): ) assert ha_state.state == "3.9" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == LENGTH_MILLIMETERS + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfLength.MILLIMETERS await async_manipulate_test_data(hass, hmip_device, "todayRainCounter", 14.2) ha_state = hass.states.get(entity_id) assert ha_state.state == "14.2" @@ -403,7 +406,7 @@ async def test_hmip_temperature_external_sensor_channel_1( ha_state = hass.states.get(entity_id) assert ha_state.state == "25.4" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS await async_manipulate_test_data(hass, hmip_device, "temperatureExternalOne", 23.5) ha_state = hass.states.get(entity_id) assert ha_state.state == "23.5" @@ -428,7 +431,7 @@ async def test_hmip_temperature_external_sensor_channel_2( ha_state = hass.states.get(entity_id) assert ha_state.state == "22.4" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS await async_manipulate_test_data(hass, hmip_device, "temperatureExternalTwo", 23.4) ha_state = hass.states.get(entity_id) assert ha_state.state == "23.4" @@ -451,7 +454,7 @@ async def test_hmip_temperature_external_sensor_delta(hass, default_mock_hap_fac ha_state = hass.states.get(entity_id) assert ha_state.state == "0.4" - assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS await async_manipulate_test_data( hass, hmip_device, "temperatureExternalDelta", -0.5 ) diff --git a/tests/components/homewizard/conftest.py b/tests/components/homewizard/conftest.py index c9a04c55dae..b1bfb1190dc 100644 --- a/tests/components/homewizard/conftest.py +++ b/tests/components/homewizard/conftest.py @@ -1,6 +1,7 @@ """Fixtures for HomeWizard integration tests.""" +from collections.abc import Generator import json -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from homewizard_energy.features import Features from homewizard_energy.models import Data, Device, State, System @@ -80,3 +81,13 @@ async def init_integration( await hass.async_block_till_done() return mock_config_entry + + +@pytest.fixture +def mock_onboarding() -> Generator[MagicMock, None, None]: + """Mock that Home Assistant is currently onboarding.""" + with patch( + "homeassistant.components.onboarding.async_is_onboarded", + return_value=False, + ) as mock_onboarding: + yield mock_onboarding diff --git a/tests/components/homewizard/fixtures/data.json b/tests/components/homewizard/fixtures/data.json index 35cfd2197a1..f73d3ac1a19 100644 --- a/tests/components/homewizard/fixtures/data.json +++ b/tests/components/homewizard/fixtures/data.json @@ -1,18 +1,41 @@ { - "smr_version": 50, - "meter_model": "ISKRA 2M550T-101", "wifi_ssid": "My Wi-Fi", "wifi_strength": 100, - "total_power_import_t1_kwh": 1234.111, - "total_power_import_t2_kwh": 5678.222, + "smr_version": 50, + "meter_model": "ISKRA 2M550T-101", + "unique_id": "00112233445566778899AABBCCDDEEFF", + "active_tariff": 2, + "total_power_import_kwh": 13779.338, + "total_power_import_t1_kwh": 10830.511, + "total_power_import_t2_kwh": 2948.827, + "total_power_export_kwh": 13086.777, "total_power_export_t1_kwh": 4321.333, "total_power_export_t2_kwh": 8765.444, "active_power_w": -123, "active_power_l1_w": -123, "active_power_l2_w": 456, "active_power_l3_w": 123.456, + "active_voltage_l1_v": 230.111, + "active_voltage_l2_v": 230.222, + "active_voltage_l3_v": 230.333, + "active_current_l1_a": -4, + "active_current_l2_a": 2, + "active_current_l3_a": 0, + "active_frequency_hz": 50, + "voltage_sag_l1_count": 1, + "voltage_sag_l2_count": 2, + "voltage_sag_l3_count": 3, + "voltage_swell_l1_count": 4, + "voltage_swell_l2_count": 5, + "voltage_swell_l3_count": 6, + "any_power_fail_count": 4, + "long_power_fail_count": 5, "total_gas_m3": 1122.333, "gas_timestamp": 210314112233, + "gas_unique_id": "01FFEEDDCCBBAA99887766554433221100", + "active_power_average_w": 123.0, + "montly_power_peak_w": 1111.0, + "montly_power_peak_timestamp": 230101080010, "active_liter_lpm": 12.345, "total_liter_m3": 1234.567 } diff --git a/tests/components/homewizard/generator.py b/tests/components/homewizard/generator.py index dff1a4462d3..f9bdea74fb4 100644 --- a/tests/components/homewizard/generator.py +++ b/tests/components/homewizard/generator.py @@ -3,7 +3,7 @@ from unittest.mock import AsyncMock from homewizard_energy.features import Features -from homewizard_energy.models import Device +from homewizard_energy.models import Data, Device def get_mock_device( @@ -26,7 +26,7 @@ def get_mock_device( firmware_version=firmware_version, ) ) - mock_device.data = AsyncMock(return_value=None) + mock_device.data = AsyncMock(return_value=Data.from_dict({})) mock_device.state = AsyncMock(return_value=None) mock_device.system = AsyncMock(return_value=None) mock_device.features = AsyncMock( diff --git a/tests/components/homewizard/test_button.py b/tests/components/homewizard/test_button.py index 79c6fe3c4a9..819162f04b5 100644 --- a/tests/components/homewizard/test_button.py +++ b/tests/components/homewizard/test_button.py @@ -1,8 +1,12 @@ """Test the identify button for HomeWizard.""" from unittest.mock import patch +from homewizard_energy.errors import DisabledError, RequestError +from pytest import raises + from homeassistant.components import button from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNKNOWN +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from .generator import get_mock_device @@ -60,8 +64,8 @@ async def test_identify_button_is_loaded( assert entry.unique_id == "aabbccddeeff_identify" -async def test_cloud_connection_on_off(hass, mock_config_entry_data, mock_config_entry): - """Test the creation and values of the Litter-Robot button.""" +async def test_identify_press(hass, mock_config_entry_data, mock_config_entry): + """Test button press is handled correctly.""" api = get_mock_device(product_type="HWE-SKT", firmware_version="3.02") @@ -89,3 +93,79 @@ async def test_cloud_connection_on_off(hass, mock_config_entry_data, mock_config blocking=True, ) assert api.identify.call_count == 1 + + +async def test_identify_press_catches_requesterror( + hass, mock_config_entry_data, mock_config_entry +): + """Test button press is handled RequestError correctly.""" + + api = get_mock_device(product_type="HWE-SKT", firmware_version="3.02") + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert ( + hass.states.get("button.product_name_aabbccddeeff_identify").state + == STATE_UNKNOWN + ) + + # Raise RequestError when identify is called + api.identify.side_effect = RequestError() + + assert api.identify.call_count == 0 + + with raises(HomeAssistantError): + await hass.services.async_call( + button.DOMAIN, + button.SERVICE_PRESS, + {"entity_id": "button.product_name_aabbccddeeff_identify"}, + blocking=True, + ) + assert api.identify.call_count == 1 + + +async def test_identify_press_catches_disablederror( + hass, mock_config_entry_data, mock_config_entry +): + """Test button press is handled DisabledError correctly.""" + + api = get_mock_device(product_type="HWE-SKT", firmware_version="3.02") + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert ( + hass.states.get("button.product_name_aabbccddeeff_identify").state + == STATE_UNKNOWN + ) + + # Raise RequestError when identify is called + api.identify.side_effect = DisabledError() + + assert api.identify.call_count == 0 + + with raises(HomeAssistantError): + await hass.services.async_call( + button.DOMAIN, + button.SERVICE_PRESS, + {"entity_id": "button.product_name_aabbccddeeff_identify"}, + blocking=True, + ) + assert api.identify.call_count == 1 diff --git a/tests/components/homewizard/test_config_flow.py b/tests/components/homewizard/test_config_flow.py index 453a29c74f8..9b6648af3d3 100644 --- a/tests/components/homewizard/test_config_flow.py +++ b/tests/components/homewizard/test_config_flow.py @@ -1,6 +1,5 @@ """Test the homewizard config flow.""" -import logging -from unittest.mock import patch +from unittest.mock import MagicMock, patch from homewizard_energy.errors import DisabledError, RequestError, UnsupportedError @@ -13,8 +12,7 @@ from homeassistant.data_entry_flow import FlowResultType from .generator import get_mock_device from tests.common import MockConfigEntry - -_LOGGER = logging.getLogger(__name__) +from tests.test_util.aiohttp import AiohttpClientMocker async def test_manual_flow_works(hass, aioclient_mock): @@ -112,37 +110,112 @@ async def test_discovery_flow_works(hass, aioclient_mock): assert result["result"].unique_id == "HWE-P1_aabbccddeeff" -async def test_config_flow_imports_entry(aioclient_mock, hass): - """Test config flow accepts imported configuration.""" +async def test_discovery_flow_during_onboarding( + hass, aioclient_mock: AiohttpClientMocker, mock_onboarding: MagicMock +) -> None: + """Test discovery setup flow during onboarding.""" + + with patch( + "homeassistant.components.homewizard.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", + return_value=get_mock_device(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.43.183", + addresses=["192.168.43.183"], + port=80, + hostname="p1meter-ddeeff.local.", + type="mock_type", + name="mock_name", + properties={ + "api_enabled": "1", + "path": "/api/v1", + "product_name": "P1 meter", + "product_type": "HWE-P1", + "serial": "aabbccddeeff", + }, + ), + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "P1 meter (aabbccddeeff)" + assert result["data"][CONF_IP_ADDRESS] == "192.168.43.183" + + assert result["result"] + assert result["result"].unique_id == "HWE-P1_aabbccddeeff" + + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_onboarding.mock_calls) == 1 + + +async def test_discovery_flow_during_onboarding_disabled_api( + hass, aioclient_mock: AiohttpClientMocker, mock_onboarding: MagicMock +) -> None: + """Test discovery setup flow during onboarding with a disabled API.""" + + def mock_initialize(): + raise DisabledError device = get_mock_device() - - mock_entry = MockConfigEntry(domain="homewizard_energy", data={"host": "1.2.3.4"}) - mock_entry.add_to_hass(hass) + device.device.side_effect = mock_initialize with patch( "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", return_value=device, - ), patch( - "homeassistant.components.homewizard.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, - "old_config_entry_id": mock_entry.entry_id, - }, - data=mock_entry.data, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.43.183", + addresses=["192.168.43.183"], + port=80, + hostname="p1meter-ddeeff.local.", + type="mock_type", + name="mock_name", + properties={ + "api_enabled": "0", + "path": "/api/v1", + "product_name": "P1 meter", + "product_type": "HWE-P1", + "serial": "aabbccddeeff", + }, + ), ) - assert result["type"] == "create_entry" - assert result["title"] == "P1 meter (aabbccddeeff)" - assert result["data"][CONF_IP_ADDRESS] == "1.2.3.4" + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "discovery_confirm" + assert result["errors"] == {"base": "api_not_enabled"} + + # We are onboarded, user enabled API again and picks up from discovery/config flow + device.device.side_effect = None + mock_onboarding.return_value = True + + with patch( + "homeassistant.components.homewizard.async_setup_entry", + return_value=True, + ) as mock_setup_entry, patch( + "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", + return_value=device, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"ip_address": "192.168.43.183"} + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "P1 meter (aabbccddeeff)" + assert result["data"][CONF_IP_ADDRESS] == "192.168.43.183" + + assert result["result"] + assert result["result"].unique_id == "HWE-P1_aabbccddeeff" - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert len(device.device.mock_calls) == len(device.close.mock_calls) assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_onboarding.mock_calls) == 1 async def test_discovery_disabled_api(hass, aioclient_mock): @@ -385,6 +458,9 @@ async def test_reauth_flow(hass, aioclient_mock): device = get_mock_device() with patch( + "homeassistant.components.homewizard.async_setup_entry", + return_value=True, + ), patch( "homeassistant.components.homewizard.config_flow.HomeWizardEnergy", return_value=device, ): diff --git a/tests/components/homewizard/test_diagnostics.py b/tests/components/homewizard/test_diagnostics.py index ae703c58cfd..b0885886ec0 100644 --- a/tests/components/homewizard/test_diagnostics.py +++ b/tests/components/homewizard/test_diagnostics.py @@ -27,22 +27,50 @@ async def test_diagnostics( "firmware_version": "2.11", }, "data": { - "smr_version": 50, - "meter_model": "ISKRA 2M550T-101", "wifi_ssid": REDACTED, "wifi_strength": 100, - "total_power_import_t1_kwh": 1234.111, - "total_power_import_t2_kwh": 5678.222, + "smr_version": 50, + "meter_model": "ISKRA 2M550T-101", + "unique_meter_id": REDACTED, + "active_tariff": 2, + "total_power_import_kwh": 13779.338, + "total_power_import_t1_kwh": 10830.511, + "total_power_import_t2_kwh": 2948.827, + "total_power_import_t3_kwh": None, + "total_power_import_t4_kwh": None, + "total_power_export_kwh": 13086.777, "total_power_export_t1_kwh": 4321.333, "total_power_export_t2_kwh": 8765.444, + "total_power_export_t3_kwh": None, + "total_power_export_t4_kwh": None, "active_power_w": -123, "active_power_l1_w": -123, "active_power_l2_w": 456, "active_power_l3_w": 123.456, + "active_voltage_l1_v": 230.111, + "active_voltage_l2_v": 230.222, + "active_voltage_l3_v": 230.333, + "active_current_l1_a": -4, + "active_current_l2_a": 2, + "active_current_l3_a": 0, + "active_frequency_hz": 50, + "voltage_sag_l1_count": 1, + "voltage_sag_l2_count": 2, + "voltage_sag_l3_count": 3, + "voltage_swell_l1_count": 4, + "voltage_swell_l2_count": 5, + "voltage_swell_l3_count": 6, + "any_power_fail_count": 4, + "long_power_fail_count": 5, + "active_power_average_w": 123.0, + "monthly_power_peak_w": 1111.0, + "monthly_power_peak_timestamp": "2023-01-01T08:00:10", "total_gas_m3": 1122.333, "gas_timestamp": "2021-03-14T11:22:33", + "gas_unique_id": REDACTED, "active_liter_lpm": 12.345, "total_liter_m3": 1234.567, + "external_devices": None, }, "state": {"power_on": True, "switch_lock": False, "brightness": 255}, "system": {"cloud_enabled": True}, diff --git a/tests/components/homewizard/test_init.py b/tests/components/homewizard/test_init.py index dea2972d79f..5e494e42154 100644 --- a/tests/components/homewizard/test_init.py +++ b/tests/components/homewizard/test_init.py @@ -4,11 +4,9 @@ from unittest.mock import patch from homewizard_energy.errors import DisabledError, HomeWizardEnergyException -from homeassistant import config_entries from homeassistant.components.homewizard.const import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import CONF_IP_ADDRESS -from homeassistant.helpers import entity_registry as er from .generator import get_mock_device @@ -70,97 +68,6 @@ async def test_load_failed_host_unavailable(aioclient_mock, hass): assert entry.state is ConfigEntryState.SETUP_RETRY -async def test_init_accepts_and_migrates_old_entry(aioclient_mock, hass): - """Test config flow accepts imported configuration.""" - - device = get_mock_device() - - # Add original entry - original_entry = MockConfigEntry( - domain="homewizard_energy", - data={CONF_IP_ADDRESS: "1.2.3.4"}, - entry_id="old_id", - ) - original_entry.add_to_hass(hass) - - # Give it some entities to see of they migrate properly - ent_reg = er.async_get(hass) - old_entity_active_power = ent_reg.async_get_or_create( - "sensor", - "homewizard_energy", - "p1_active_power_unique_id", - config_entry=original_entry, - original_name="Active Power", - suggested_object_id="p1_active_power", - ) - old_entity_switch = ent_reg.async_get_or_create( - "switch", - "homewizard_energy", - "socket_switch_unique_id", - config_entry=original_entry, - original_name="Switch", - suggested_object_id="socket_switch", - ) - old_entity_disabled_sensor = ent_reg.async_get_or_create( - "sensor", - "homewizard_energy", - "socket_disabled_unique_id", - config_entry=original_entry, - original_name="Switch Disabled", - suggested_object_id="socket_disabled", - disabled_by=er.RegistryEntryDisabler.USER, - ) - # Update some user-customs - ent_reg.async_update_entity(old_entity_active_power.entity_id, name="new_name") - ent_reg.async_update_entity(old_entity_switch.entity_id, icon="new_icon") - - imported_entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_IP_ADDRESS: "1.2.3.4", "old_config_entry_id": "old_id"}, - source=config_entries.SOURCE_IMPORT, - entry_id="new_id", - ) - 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( - "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", - return_value=device, - ): - await hass.config_entries.async_setup(imported_entry.entry_id) - await hass.async_block_till_done() - - assert original_entry.state is ConfigEntryState.NOT_LOADED - assert imported_entry.state is ConfigEntryState.LOADED - - # Check if new entities are migrated - new_entity_active_power = ent_reg.async_get(old_entity_active_power.entity_id) - assert new_entity_active_power.platform == DOMAIN - assert new_entity_active_power.name == "new_name" - assert new_entity_active_power.icon is None - assert new_entity_active_power.original_name == "Active Power" - assert new_entity_active_power.unique_id == "p1_active_power_unique_id" - assert new_entity_active_power.disabled_by is None - - new_entity_switch = ent_reg.async_get(old_entity_switch.entity_id) - assert new_entity_switch.platform == DOMAIN - assert new_entity_switch.name is None - assert new_entity_switch.icon == "new_icon" - assert new_entity_switch.original_name == "Switch" - assert new_entity_switch.unique_id == "socket_switch_unique_id" - assert new_entity_switch.disabled_by is None - - new_entity_disabled_sensor = ent_reg.async_get(old_entity_disabled_sensor.entity_id) - assert new_entity_disabled_sensor.platform == DOMAIN - assert new_entity_disabled_sensor.name is None - assert new_entity_disabled_sensor.original_name == "Switch Disabled" - assert new_entity_disabled_sensor.unique_id == "socket_disabled_unique_id" - assert new_entity_disabled_sensor.disabled_by == er.RegistryEntryDisabler.USER - - async def test_load_detect_api_disabled(aioclient_mock, hass): """Test setup detects disabled API.""" diff --git a/tests/components/homewizard/test_number.py b/tests/components/homewizard/test_number.py index 9538fd3cef9..54c14a38407 100644 --- a/tests/components/homewizard/test_number.py +++ b/tests/components/homewizard/test_number.py @@ -2,11 +2,14 @@ from unittest.mock import AsyncMock, patch +from homewizard_energy.errors import DisabledError, RequestError from homewizard_energy.models import State +from pytest import raises from homeassistant.components import number from homeassistant.components.number import ATTR_VALUE, SERVICE_SET_VALUE from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from .generator import get_mock_device @@ -139,3 +142,118 @@ async def test_brightness_level_set(hass, mock_config_entry_data, mock_config_en == "0" ) assert len(api.state_set.mock_calls) == 2 + + +async def test_brightness_level_set_catches_requesterror( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity raises HomeAssistantError when RequestError was raised.""" + + api = get_mock_device() + api.state = AsyncMock(return_value=State.from_dict({"brightness": 255})) + + api.state_set = AsyncMock(side_effect=RequestError()) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Set level halfway + with raises(HomeAssistantError): + await hass.services.async_call( + number.DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.product_name_aabbccddeeff_status_light_brightness", + ATTR_VALUE: 50, + }, + blocking=True, + ) + + +async def test_brightness_level_set_catches_disablederror( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity raises HomeAssistantError when DisabledError was raised.""" + + api = get_mock_device() + api.state = AsyncMock(return_value=State.from_dict({"brightness": 255})) + + api.state_set = AsyncMock(side_effect=DisabledError()) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Set level halfway + with raises(HomeAssistantError): + await hass.services.async_call( + number.DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.product_name_aabbccddeeff_status_light_brightness", + ATTR_VALUE: 50, + }, + blocking=True, + ) + + +async def test_brightness_level_set_catches_invalid_value( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity raises ValueError when value was invalid.""" + + api = get_mock_device() + api.state = AsyncMock(return_value=State.from_dict({"brightness": 255})) + + def state_set(brightness): + api.state = AsyncMock(return_value=State.from_dict({"brightness": brightness})) + + api.state_set = AsyncMock(side_effect=state_set) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + with raises(ValueError): + await hass.services.async_call( + number.DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.product_name_aabbccddeeff_status_light_brightness", + ATTR_VALUE: -1, + }, + blocking=True, + ) + + with raises(ValueError): + await hass.services.async_call( + number.DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.product_name_aabbccddeeff_status_light_brightness", + ATTR_VALUE: 101, + }, + blocking=True, + ) diff --git a/tests/components/homewizard/test_sensor.py b/tests/components/homewizard/test_sensor.py index 8ed6f9365c8..fa60b4a4325 100644 --- a/tests/components/homewizard/test_sensor.py +++ b/tests/components/homewizard/test_sensor.py @@ -7,6 +7,7 @@ from homewizard_energy.errors import DisabledError, RequestError from homewizard_energy.models import Data from homeassistant.components.sensor import ( + ATTR_OPTIONS, ATTR_STATE_CLASS, SensorDeviceClass, SensorStateClass, @@ -16,9 +17,12 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, - VOLUME_CUBIC_METERS, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfFrequency, + UnitOfPower, + UnitOfVolume, ) from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util @@ -106,6 +110,46 @@ async def test_sensor_entity_meter_model( assert state.attributes.get(ATTR_ICON) == "mdi:gauge" +async def test_sensor_entity_unique_meter_id( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads unique meter id.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"unique_id": "4E47475955"})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get("sensor.product_name_aabbccddeeff_smart_meter_identifier") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_smart_meter_identifier" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_unique_meter_id" + assert not entry.disabled + assert state.state == "NGGYU" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Smart meter identifier" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + assert state.attributes.get(ATTR_ICON) == "mdi:alphabetical-variant" + + async def test_sensor_entity_wifi_ssid(hass, mock_config_entry_data, mock_config_entry): """Test entity loads wifi ssid.""" @@ -142,6 +186,45 @@ async def test_sensor_entity_wifi_ssid(hass, mock_config_entry_data, mock_config assert state.attributes.get(ATTR_ICON) == "mdi:wifi" +async def test_sensor_entity_active_tariff( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active_tariff.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"active_tariff": 2})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get("sensor.product_name_aabbccddeeff_active_tariff") + entry = entity_registry.async_get("sensor.product_name_aabbccddeeff_active_tariff") + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_active_tariff" + assert not entry.disabled + assert state.state == "2" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active tariff" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert state.attributes.get(ATTR_ICON) == "mdi:calendar-clock" + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM + assert state.attributes.get(ATTR_OPTIONS) == ["1", "2", "3", "4"] + + async def test_sensor_entity_wifi_strength( hass, mock_config_entry_data, mock_config_entry ): @@ -206,7 +289,7 @@ async def test_sensor_entity_total_power_import_t1_kwh( == "Product Name (aabbccddeeff) Total power import T1" ) 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_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -248,7 +331,7 @@ async def test_sensor_entity_total_power_import_t2_kwh( == "Product Name (aabbccddeeff) Total power import T2" ) 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_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -290,7 +373,7 @@ async def test_sensor_entity_total_power_export_t1_kwh( == "Product Name (aabbccddeeff) Total power export T1" ) 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_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -332,7 +415,7 @@ async def test_sensor_entity_total_power_export_t2_kwh( == "Product Name (aabbccddeeff) Total power export T2" ) 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_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -370,7 +453,7 @@ async def test_sensor_entity_active_power( == "Product Name (aabbccddeeff) Active power" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes @@ -410,7 +493,7 @@ async def test_sensor_entity_active_power_l1( == "Product Name (aabbccddeeff) Active power L1" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes @@ -450,7 +533,7 @@ async def test_sensor_entity_active_power_l2( == "Product Name (aabbccddeeff) Active power L2" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes @@ -490,7 +573,7 @@ async def test_sensor_entity_active_power_l3( == "Product Name (aabbccddeeff) Active power L3" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes @@ -526,11 +609,867 @@ async def test_sensor_entity_total_gas(hass, mock_config_entry_data, mock_config == "Product Name (aabbccddeeff) Total gas" ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.CUBIC_METERS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.GAS assert ATTR_ICON not in state.attributes +async def test_sensor_entity_unique_gas_meter_id( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads unique gas meter id.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"gas_unique_id": "4E47475955"})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get("sensor.product_name_aabbccddeeff_gas_meter_identifier") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_gas_meter_identifier" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_gas_unique_id" + assert not entry.disabled + assert state.state == "NGGYU" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Gas meter identifier" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + assert state.attributes.get(ATTR_ICON) == "mdi:alphabetical-variant" + + +async def test_sensor_entity_active_voltage_l1( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active voltage l1.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"active_voltage_l1_v": 230.123})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + disabled_entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_active_voltage_l1" + ) + assert disabled_entry + assert disabled_entry.disabled + assert disabled_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + # Enable + entry = entity_registry.async_update_entity( + disabled_entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert not entry.disabled + assert entry.unique_id == "aabbccddeeff_active_voltage_l1_v" + + # Let HA reload the integration so state is set + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=30), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.product_name_aabbccddeeff_active_voltage_l1") + assert state + assert state.state == "230.123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active voltage L1" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfElectricPotential.VOLT + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE + assert ATTR_ICON not in state.attributes + + +async def test_sensor_entity_active_voltage_l2( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active voltage l2.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"active_voltage_l2_v": 230.123})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + disabled_entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_active_voltage_l2" + ) + assert disabled_entry + assert disabled_entry.disabled + assert disabled_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + # Enable + entry = entity_registry.async_update_entity( + disabled_entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert not entry.disabled + assert entry.unique_id == "aabbccddeeff_active_voltage_l2_v" + + # Let HA reload the integration so state is set + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=30), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.product_name_aabbccddeeff_active_voltage_l2") + assert state + assert state.state == "230.123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active voltage L2" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfElectricPotential.VOLT + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE + assert ATTR_ICON not in state.attributes + + +async def test_sensor_entity_active_voltage_l3( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active voltage l3.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"active_voltage_l3_v": 230.123})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + disabled_entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_active_voltage_l3" + ) + assert disabled_entry + assert disabled_entry.disabled + assert disabled_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + # Enable + entry = entity_registry.async_update_entity( + disabled_entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert not entry.disabled + assert entry.unique_id == "aabbccddeeff_active_voltage_l3_v" + + # Let HA reload the integration so state is set + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=30), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.product_name_aabbccddeeff_active_voltage_l3") + assert state + assert state.state == "230.123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active voltage L3" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfElectricPotential.VOLT + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE + assert ATTR_ICON not in state.attributes + + +async def test_sensor_entity_active_current_l1( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active current l1.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"active_current_l1_a": 12.34})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + disabled_entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_active_current_l1" + ) + assert disabled_entry + assert disabled_entry.disabled + assert disabled_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + # Enable + entry = entity_registry.async_update_entity( + disabled_entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert not entry.disabled + assert entry.unique_id == "aabbccddeeff_active_current_l1_a" + + # Let HA reload the integration so state is set + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=30), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.product_name_aabbccddeeff_active_current_l1") + assert state + assert state.state == "12.34" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active current L1" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfElectricCurrent.AMPERE + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT + assert ATTR_ICON not in state.attributes + + +async def test_sensor_entity_active_current_l2( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active current l2.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"active_current_l2_a": 12.34})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + disabled_entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_active_current_l2" + ) + assert disabled_entry + assert disabled_entry.disabled + assert disabled_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + # Enable + entry = entity_registry.async_update_entity( + disabled_entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert not entry.disabled + assert entry.unique_id == "aabbccddeeff_active_current_l2_a" + + # Let HA reload the integration so state is set + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=30), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.product_name_aabbccddeeff_active_current_l2") + assert state + assert state.state == "12.34" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active current L2" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfElectricCurrent.AMPERE + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT + assert ATTR_ICON not in state.attributes + + +async def test_sensor_entity_active_current_l3( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active current l3.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"active_current_l3_a": 12.34})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + disabled_entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_active_current_l3" + ) + assert disabled_entry + assert disabled_entry.disabled + assert disabled_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + # Enable + entry = entity_registry.async_update_entity( + disabled_entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert not entry.disabled + assert entry.unique_id == "aabbccddeeff_active_current_l3_a" + + # Let HA reload the integration so state is set + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=30), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.product_name_aabbccddeeff_active_current_l3") + assert state + assert state.state == "12.34" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active current L3" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfElectricCurrent.AMPERE + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT + assert ATTR_ICON not in state.attributes + + +async def test_sensor_entity_active_frequency( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active frequency.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"active_frequency_hz": 50.12})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + disabled_entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_active_frequency" + ) + assert disabled_entry + assert disabled_entry.disabled + assert disabled_entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + # Enable + entry = entity_registry.async_update_entity( + disabled_entry.entity_id, **{"disabled_by": None} + ) + await hass.async_block_till_done() + assert not entry.disabled + assert entry.unique_id == "aabbccddeeff_active_frequency_hz" + + # Let HA reload the integration so state is set + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=30), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.product_name_aabbccddeeff_active_frequency") + assert state + assert state.state == "50.12" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active frequency" + ) + assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfFrequency.HERTZ + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.FREQUENCY + assert ATTR_ICON not in state.attributes + + +async def test_sensor_entity_voltage_sag_count_l1( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads voltage_sag_count_l1.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"voltage_sag_l1_count": 123})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get("sensor.product_name_aabbccddeeff_voltage_sags_detected_l1") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_voltage_sags_detected_l1" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_voltage_sag_l1_count" + assert not entry.disabled + assert state.state == "123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Voltage sags detected L1" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + + +async def test_sensor_entity_voltage_sag_count_l2( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads voltage_sag_count_l2.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"voltage_sag_l2_count": 123})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get("sensor.product_name_aabbccddeeff_voltage_sags_detected_l2") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_voltage_sags_detected_l2" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_voltage_sag_l2_count" + assert not entry.disabled + assert state.state == "123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Voltage sags detected L2" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + + +async def test_sensor_entity_voltage_sag_count_l3( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads voltage_sag_count_l3.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"voltage_sag_l3_count": 123})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get("sensor.product_name_aabbccddeeff_voltage_sags_detected_l3") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_voltage_sags_detected_l3" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_voltage_sag_l3_count" + assert not entry.disabled + assert state.state == "123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Voltage sags detected L3" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + + +async def test_sensor_entity_voltage_swell_count_l1( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads voltage_swell_count_l1.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"voltage_swell_l1_count": 123})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get( + "sensor.product_name_aabbccddeeff_voltage_swells_detected_l1" + ) + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_voltage_swells_detected_l1" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_voltage_swell_l1_count" + assert not entry.disabled + assert state.state == "123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Voltage swells detected L1" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + + +async def test_sensor_entity_voltage_swell_count_l2( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads voltage_swell_count_l2.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"voltage_swell_l2_count": 123})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get( + "sensor.product_name_aabbccddeeff_voltage_swells_detected_l2" + ) + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_voltage_swells_detected_l2" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_voltage_swell_l2_count" + assert not entry.disabled + assert state.state == "123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Voltage swells detected L2" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + + +async def test_sensor_entity_voltage_swell_count_l3( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads voltage_swell_count_l3.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"voltage_swell_l3_count": 123})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get( + "sensor.product_name_aabbccddeeff_voltage_swells_detected_l3" + ) + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_voltage_swells_detected_l3" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_voltage_swell_l3_count" + assert not entry.disabled + assert state.state == "123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Voltage swells detected L3" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + + +async def test_sensor_entity_any_power_fail_count( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads any power fail count.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"any_power_fail_count": 123})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get("sensor.product_name_aabbccddeeff_power_failures_detected") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_power_failures_detected" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_any_power_fail_count" + assert not entry.disabled + assert state.state == "123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Power failures detected" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + + +async def test_sensor_entity_long_power_fail_count( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads long power fail count.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"long_power_fail_count": 123})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get( + "sensor.product_name_aabbccddeeff_long_power_failures_detected" + ) + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_long_power_failures_detected" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_long_power_fail_count" + assert not entry.disabled + assert state.state == "123" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Long power failures detected" + ) + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + assert ATTR_DEVICE_CLASS not in state.attributes + + +async def test_sensor_entity_active_power_average( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads active power average.""" + + api = get_mock_device() + api.data = AsyncMock( + return_value=Data.from_dict({"active_power_average_w": 123.456}) + ) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get("sensor.product_name_aabbccddeeff_active_average_demand") + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_active_average_demand" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_active_power_average_w" + assert not entry.disabled + assert state.state == "123.456" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Active average demand" + ) + + assert state.attributes.get(ATTR_STATE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER + assert ATTR_ICON not in state.attributes + + +async def test_sensor_entity_monthly_power_peak( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity loads monthly power peak.""" + + api = get_mock_device() + api.data = AsyncMock(return_value=Data.from_dict({"montly_power_peak_w": 1234.456})) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + + state = hass.states.get( + "sensor.product_name_aabbccddeeff_peak_demand_current_month" + ) + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_peak_demand_current_month" + ) + assert entry + assert state + assert entry.unique_id == "aabbccddeeff_monthly_power_peak_w" + assert not entry.disabled + assert state.state == "1234.456" + assert ( + state.attributes.get(ATTR_FRIENDLY_NAME) + == "Product Name (aabbccddeeff) Peak demand current month" + ) + + assert state.attributes.get(ATTR_STATE_CLASS) is None + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER + assert ATTR_ICON not in state.attributes + + async def test_sensor_entity_active_liters( hass, mock_config_entry_data, mock_config_entry ): @@ -608,7 +1547,7 @@ async def test_sensor_entity_total_liters( ) assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.CUBIC_METERS assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER assert state.attributes.get(ATTR_ICON) == "mdi:gauge" @@ -660,7 +1599,13 @@ async def test_sensor_entity_export_disabled_when_unused( api = get_mock_device() api.data = AsyncMock( return_value=Data.from_dict( - {"total_power_export_t1_kwh": 0, "total_power_export_t2_kwh": 0} + { + "total_power_export_kwh": 0, + "total_power_export_t1_kwh": 0, + "total_power_export_t2_kwh": 0, + "total_power_export_t3_kwh": 0, + "total_power_export_t4_kwh": 0, + } ) ) @@ -677,6 +1622,12 @@ async def test_sensor_entity_export_disabled_when_unused( entity_registry = er.async_get(hass) + entry = entity_registry.async_get( + "sensor.product_name_aabbccddeeff_total_power_export" + ) + assert entry + assert entry.disabled + entry = entity_registry.async_get( "sensor.product_name_aabbccddeeff_total_power_export_t1" ) diff --git a/tests/components/homewizard/test_switch.py b/tests/components/homewizard/test_switch.py index a964d548dd3..79e576a18d8 100644 --- a/tests/components/homewizard/test_switch.py +++ b/tests/components/homewizard/test_switch.py @@ -2,7 +2,9 @@ from unittest.mock import AsyncMock, patch +from homewizard_energy.errors import DisabledError, RequestError from homewizard_energy.models import State, System +from pytest import raises from homeassistant.components import switch from homeassistant.components.switch import SwitchDeviceClass @@ -16,6 +18,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from .generator import get_mock_device @@ -95,10 +98,8 @@ async def test_switch_loads_entities(hass, mock_config_entry_data, mock_config_e state_switch_lock.attributes.get(ATTR_FRIENDLY_NAME) == "Product Name (aabbccddeeff) Switch lock" ) - assert ( - state_switch_lock.attributes.get(ATTR_DEVICE_CLASS) == SwitchDeviceClass.SWITCH - ) - assert ATTR_ICON not in state_switch_lock.attributes + assert state_switch_lock.attributes.get(ATTR_ICON) == "mdi:lock-open" + assert ATTR_DEVICE_CLASS not in state_switch_lock.attributes async def test_switch_power_on_off(hass, mock_config_entry_data, mock_config_entry): @@ -346,3 +347,157 @@ async def test_cloud_connection_on_off(hass, mock_config_entry_data, mock_config == STATE_OFF ) assert len(api.system_set.mock_calls) == 2 + + +async def test_switch_handles_requesterror( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity raises HomeAssistantError when RequestError was raised.""" + + api = get_mock_device(product_type="HWE-SKT", firmware_version="3.02") + api.state = AsyncMock( + return_value=State.from_dict({"power_on": False, "switch_lock": False}) + ) + api.system = AsyncMock(return_value=System.from_dict({"cloud_enabled": False})) + + api.state_set = AsyncMock(side_effect=RequestError()) + api.system_set = AsyncMock(side_effect=RequestError()) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Power on toggle + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {"entity_id": "switch.product_name_aabbccddeeff"}, + blocking=True, + ) + + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": "switch.product_name_aabbccddeeff_cloud_connection"}, + blocking=True, + ) + + # Switch Lock toggle + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {"entity_id": "switch.product_name_aabbccddeeff_switch_lock"}, + blocking=True, + ) + + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": "switch.product_name_aabbccddeeff_switch_lock"}, + blocking=True, + ) + + # Disable Cloud toggle + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {"entity_id": "switch.product_name_aabbccddeeff_cloud_connection"}, + blocking=True, + ) + + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": "switch.product_name_aabbccddeeff_cloud_connection"}, + blocking=True, + ) + + +async def test_switch_handles_disablederror( + hass, mock_config_entry_data, mock_config_entry +): + """Test entity raises HomeAssistantError when Disabled was raised.""" + + api = get_mock_device(product_type="HWE-SKT", firmware_version="3.02") + api.state = AsyncMock( + return_value=State.from_dict({"power_on": False, "switch_lock": False}) + ) + api.system = AsyncMock(return_value=System.from_dict({"cloud_enabled": False})) + + api.state_set = AsyncMock(side_effect=DisabledError()) + api.system_set = AsyncMock(side_effect=DisabledError()) + + with patch( + "homeassistant.components.homewizard.coordinator.HomeWizardEnergy", + return_value=api, + ): + entry = mock_config_entry + entry.data = mock_config_entry_data + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # Power on toggle + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {"entity_id": "switch.product_name_aabbccddeeff"}, + blocking=True, + ) + + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": "switch.product_name_aabbccddeeff_cloud_connection"}, + blocking=True, + ) + + # Switch Lock toggle + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {"entity_id": "switch.product_name_aabbccddeeff_switch_lock"}, + blocking=True, + ) + + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": "switch.product_name_aabbccddeeff_switch_lock"}, + blocking=True, + ) + + # Disable Cloud toggle + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_ON, + {"entity_id": "switch.product_name_aabbccddeeff_cloud_connection"}, + blocking=True, + ) + + with raises(HomeAssistantError): + await hass.services.async_call( + switch.DOMAIN, + SERVICE_TURN_OFF, + {"entity_id": "switch.product_name_aabbccddeeff_cloud_connection"}, + blocking=True, + ) diff --git a/tests/components/honeywell/conftest.py b/tests/components/honeywell/conftest.py index dcb8edb4015..bead64c71d1 100644 --- a/tests/components/honeywell/conftest.py +++ b/tests/components/honeywell/conftest.py @@ -1,9 +1,9 @@ """Fixtures for honeywell tests.""" -from unittest.mock import create_autospec, patch +from unittest.mock import AsyncMock, create_autospec, patch +import AIOSomecomfort import pytest -import somecomfort from homeassistant.components.honeywell.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -30,7 +30,7 @@ def config_entry(config_data): @pytest.fixture def device(): """Mock a somecomfort.Device.""" - mock_device = create_autospec(somecomfort.Device, instance=True) + mock_device = create_autospec(AIOSomecomfort.device.Device, instance=True) mock_device.deviceid = 1234567 mock_device._data = { "canControlHumidification": False, @@ -48,7 +48,7 @@ def device(): @pytest.fixture def device_with_outdoor_sensor(): """Mock a somecomfort.Device.""" - mock_device = create_autospec(somecomfort.Device, instance=True) + mock_device = create_autospec(AIOSomecomfort.device.Device, instance=True) mock_device.deviceid = 1234567 mock_device._data = { "canControlHumidification": False, @@ -67,7 +67,7 @@ def device_with_outdoor_sensor(): @pytest.fixture def another_device(): """Mock a somecomfort.Device.""" - mock_device = create_autospec(somecomfort.Device, instance=True) + mock_device = create_autospec(AIOSomecomfort.device.Device, instance=True) mock_device.deviceid = 7654321 mock_device._data = { "canControlHumidification": False, @@ -85,7 +85,7 @@ def another_device(): @pytest.fixture def location(device): """Mock a somecomfort.Location.""" - mock_location = create_autospec(somecomfort.Location, instance=True) + mock_location = create_autospec(AIOSomecomfort.location.Location, instance=True) mock_location.locationid.return_value = "location1" mock_location.devices_by_id = {device.deviceid: device} return mock_location @@ -94,11 +94,13 @@ def location(device): @pytest.fixture(autouse=True) def client(location): """Mock a somecomfort.SomeComfort client.""" - client_mock = create_autospec(somecomfort.SomeComfort, instance=True) + client_mock = create_autospec(AIOSomecomfort.AIOSomeComfort, instance=True) client_mock.locations_by_id = {location.locationid: location} + client_mock.login = AsyncMock(return_value=True) + client_mock.discover = AsyncMock() with patch( - "homeassistant.components.honeywell.somecomfort.SomeComfort" + "homeassistant.components.honeywell.AIOSomecomfort.AIOSomeComfort" ) as sc_class_mock: sc_class_mock.return_value = client_mock yield client_mock diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index d877133bdcd..46ab48572f8 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -1,7 +1,7 @@ """Tests for honeywell config flow.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch -import somecomfort +import AIOSomecomfort from homeassistant import data_entry_flow from homeassistant.components.honeywell.const import ( @@ -33,33 +33,43 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: assert result["step_id"] == "user" -async def test_connection_error(hass: HomeAssistant) -> None: +async def test_connection_error(hass: HomeAssistant, client: MagicMock) -> None: + """Test that an error message is shown on connection fail.""" + client.login.side_effect = AIOSomecomfort.ConnectionError + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG + ) + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_auth_error(hass: HomeAssistant, client: MagicMock) -> None: """Test that an error message is shown on login fail.""" - with patch( - "somecomfort.SomeComfort", - side_effect=somecomfort.AuthError, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG - ) - assert result["errors"] == {"base": "invalid_auth"} + client.login.side_effect = AIOSomecomfort.AuthError + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG + ) + assert result["errors"] == {"base": "invalid_auth"} async def test_create_entry(hass: HomeAssistant) -> None: """Test that the config entry is created.""" with patch( - "somecomfort.SomeComfort", + "homeassistant.components.honeywell.async_setup_entry", + return_value=True, ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"] == FAKE_CONFIG + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == FAKE_CONFIG -@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0) async def test_show_option_form( - hass: HomeAssistant, config_entry: MockConfigEntry, location + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test that the option form is shown.""" config_entry.add_to_hass(hass) @@ -68,15 +78,18 @@ async def test_show_option_form( assert config_entry.state is ConfigEntryState.LOADED - result = await hass.config_entries.options.async_init(config_entry.entry_id) + with patch( + "homeassistant.components.honeywell.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" -@patch("homeassistant.components.honeywell.UPDATE_LOOP_SLEEP_TIME", 0) async def test_create_option_entry( - hass: HomeAssistant, config_entry: MockConfigEntry, location + hass: HomeAssistant, config_entry: MockConfigEntry ) -> None: """Test that the config entry is created.""" config_entry.add_to_hass(hass) @@ -85,11 +98,18 @@ async def test_create_option_entry( assert config_entry.state is ConfigEntryState.LOADED - options_form = await hass.config_entries.options.async_init(config_entry.entry_id) - result = await hass.config_entries.options.async_configure( - options_form["flow_id"], - user_input={CONF_COOL_AWAY_TEMPERATURE: 1, CONF_HEAT_AWAY_TEMPERATURE: 2}, - ) + with patch( + "homeassistant.components.honeywell.async_setup_entry", + return_value=True, + ): + options_form = await hass.config_entries.options.async_init( + config_entry.entry_id + ) + result = await hass.config_entries.options.async_configure( + options_form["flow_id"], + user_input={CONF_COOL_AWAY_TEMPERATURE: 1, CONF_HEAT_AWAY_TEMPERATURE: 2}, + ) + await hass.async_block_till_done() assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert config_entry.options == { diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py index 83be3a05873..4ecd2a3172d 100644 --- a/tests/components/honeywell/test_init.py +++ b/tests/components/honeywell/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import create_autospec, patch -import somecomfort +import AIOSomecomfort from homeassistant.components.honeywell.const import ( CONF_COOL_AWAY_TEMPERATURE, @@ -46,7 +46,7 @@ async def test_setup_multiple_thermostats_with_same_deviceid( hass: HomeAssistant, caplog, config_entry: MockConfigEntry, device, client ) -> None: """Test Honeywell TCC API returning duplicate device IDs.""" - mock_location2 = create_autospec(somecomfort.Location, instance=True) + mock_location2 = create_autospec(AIOSomecomfort.Location, instance=True) mock_location2.locationid.return_value = "location2" mock_location2.devices_by_id = {device.deviceid: device} client.locations_by_id["location2"] = mock_location2 @@ -71,13 +71,10 @@ async def test_away_temps_migration(hass: HomeAssistant) -> None: options={}, ) - with patch( - "homeassistant.components.honeywell.somecomfort.SomeComfort", - ): - legacy_config.add_to_hass(hass) - await hass.config_entries.async_setup(legacy_config.entry_id) - await hass.async_block_till_done() - assert legacy_config.options == { - CONF_COOL_AWAY_TEMPERATURE: 1, - CONF_HEAT_AWAY_TEMPERATURE: 2, - } + legacy_config.add_to_hass(hass) + await hass.config_entries.async_setup(legacy_config.entry_id) + await hass.async_block_till_done() + assert legacy_config.options == { + CONF_COOL_AWAY_TEMPERATURE: 1, + CONF_HEAT_AWAY_TEMPERATURE: 2, + } diff --git a/tests/components/honeywell/test_sensor.py b/tests/components/honeywell/test_sensor.py index 6a5b5636745..7ed047262bf 100644 --- a/tests/components/honeywell/test_sensor.py +++ b/tests/components/honeywell/test_sensor.py @@ -1,18 +1,24 @@ """Test honeywell sensor.""" -from somecomfort import Device, Location +from AIOSomecomfort.device import Device +from AIOSomecomfort.location import Location +import pytest from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry +@pytest.mark.parametrize("unit,temp", [("C", "5"), ("F", "-15")]) async def test_outdoor_sensor( hass: HomeAssistant, config_entry: MockConfigEntry, location: Location, device_with_outdoor_sensor: Device, + unit, + temp, ): """Test outdoor temperature sensor.""" + device_with_outdoor_sensor.temperature_unit = unit location.devices_by_id[ device_with_outdoor_sensor.deviceid ] = device_with_outdoor_sensor @@ -25,5 +31,5 @@ async def test_outdoor_sensor( assert temperature_state assert humidity_state - assert temperature_state.state == "5" + assert temperature_state.state == temp assert humidity_state.state == "25" diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index a4249a1efb6..b839d048272 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -1,7 +1,5 @@ """The tests for the Home Assistant HTTP component.""" from http import HTTPStatus - -# pylint: disable=protected-access from ipaddress import ip_address import os from unittest.mock import Mock, mock_open, patch diff --git a/tests/components/http/test_view.py b/tests/components/http/test_view.py index f6a2ff85d3a..4709806fedd 100644 --- a/tests/components/http/test_view.py +++ b/tests/components/http/test_view.py @@ -1,4 +1,5 @@ """Tests for Home Assistant View.""" +from decimal import Decimal from http import HTTPStatus import json from unittest.mock import AsyncMock, Mock @@ -32,18 +33,18 @@ def mock_request_with_stopping(): async def test_invalid_json(caplog): """Test trying to return invalid JSON.""" - view = HomeAssistantView() - with pytest.raises(HTTPInternalServerError): - view.json(rb"\ud800") + HomeAssistantView.json({"hello": Decimal("2.0")}) - assert "Unable to serialize to JSON" in caplog.text + assert ( + "Unable to serialize to JSON. Bad data found at $.hello=2.0(" + in caplog.text + ) -async def test_nan_serialized_to_null(caplog): +async def test_nan_serialized_to_null(): """Test nan serialized to null JSON.""" - view = HomeAssistantView() - response = view.json(float("NaN")) + response = HomeAssistantView.json(float("NaN")) assert json.loads(response.body.decode("utf-8")) is None diff --git a/tests/components/hue/const.py b/tests/components/hue/const.py index 03b2f1947cf..01b9c7f84b8 100644 --- a/tests/components/hue/const.py +++ b/tests/components/hue/const.py @@ -32,7 +32,6 @@ FAKE_LIGHT = { }, "id": "fake_light_id_1", "id_v1": "/lights/1", - "metadata": {"archetype": "unknown", "name": "Hue fake light"}, "mode": "normal", "on": {"on": False}, "owner": {"rid": "fake_device_id_1", "rtype": "device"}, @@ -93,5 +92,6 @@ FAKE_SCENE = { }, "palette": {"color": [], "color_temperature": [], "dimming": []}, "speed": 0.5, + "auto_dynamic": False, "type": "scene", } diff --git a/tests/components/hue/fixtures/v2_resources.json b/tests/components/hue/fixtures/v2_resources.json index be9509f9652..51eff8451b8 100644 --- a/tests/components/hue/fixtures/v2_resources.json +++ b/tests/components/hue/fixtures/v2_resources.json @@ -168,6 +168,7 @@ "dimming": [] }, "speed": 0.6269841194152832, + "auto_dynamic": false, "type": "scene" }, { @@ -220,6 +221,7 @@ "dimming": [] }, "speed": 0.5, + "auto_dynamic": false, "type": "scene" }, { @@ -624,14 +626,12 @@ }, "effects": { "status_values": ["no_effect", "candle", "fire"], - "status": "no_effect" + "status": "no_effect", + "effect_values": ["no_effect", "candle", "fire"], + "effect": "no_effect" }, "id": "02cba059-9c2c-4d45-97e4-4f79b1bfbaa1", "id_v1": "/lights/29", - "metadata": { - "archetype": "floor_shade", - "name": "Hue light with color and color temperature 1" - }, "mode": "normal", "on": { "on": true @@ -666,18 +666,18 @@ }, "effects": { "status_values": ["no_effect", "candle"], - "status": "no_effect" + "status": "no_effect", + "effect_values": ["no_effect", "candle", "fire"], + "effect": "no_effect" }, "timed_effects": { "status_values": ["no_effect", "sunrise"], - "status": "no_effect" + "status": "no_effect", + "effect_values": ["no_effect", "sunrise"], + "effect": "no_effect" }, "id": "3a6710fa-4474-4eba-b533-5e6e72968feb", "id_v1": "/lights/4", - "metadata": { - "archetype": "ceiling_round", - "name": "Hue light with color temperature only" - }, "mode": "normal", "on": { "on": false @@ -733,10 +733,6 @@ }, "id": "b3fe71ef-d0ef-48de-9355-d9e604377df0", "id_v1": "/lights/16", - "metadata": { - "archetype": "hue_lightstrip", - "name": "Hue light with color and color temperature 2" - }, "mode": "normal", "on": { "on": true @@ -759,10 +755,6 @@ }, "id": "7697ac8a-25aa-4576-bb40-0036c0db15b9", "id_v1": "/lights/23", - "metadata": { - "archetype": "classic_bulb", - "name": "Hue on/off light" - }, "mode": "normal", "on": { "on": false @@ -810,10 +802,6 @@ }, "id": "74a45fee-1b3d-4553-b5ab-040da8a10cfd", "id_v1": "/lights/11", - "metadata": { - "archetype": "hue_bloom", - "name": "Hue light with color only" - }, "mode": "normal", "on": { "on": true @@ -914,10 +902,6 @@ }, "id": "8015b17f-8336-415b-966a-b364bd082397", "id_v1": "/lights/24", - "metadata": { - "archetype": "hue_lightstrip_tv", - "name": "Hue light with color and color temperature gradient" - }, "mode": "normal", "on": { "on": true diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index d8be19f92c6..aad574bd1db 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -8,7 +8,7 @@ import pytest import voluptuous as vol from homeassistant import config_entries -from homeassistant.components import ssdp, zeroconf +from homeassistant.components import zeroconf from homeassistant.components.hue import config_flow, const from homeassistant.components.hue.errors import CannotConnect from homeassistant.helpers import device_registry as dr @@ -327,176 +327,6 @@ async def test_flow_link_cannot_connect(hass): assert result["reason"] == "cannot_connect" -@pytest.mark.parametrize("mf_url", config_flow.HUE_MANUFACTURERURL) -async def test_bridge_ssdp(hass, mf_url, aioclient_mock): - """Test a bridge being discovered.""" - create_mock_api_discovery(aioclient_mock, [("0.0.0.0", "1234")]) - result = await hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location="http://0.0.0.0/", - upnp={ - ssdp.ATTR_UPNP_MANUFACTURER_URL: mf_url, - ssdp.ATTR_UPNP_SERIAL: "1234", - }, - ), - ) - - assert result["type"] == "form" - assert result["step_id"] == "link" - - -async def test_bridge_ssdp_discover_other_bridge(hass): - """Test that discovery ignores other bridges.""" - result = await hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - upnp={ssdp.ATTR_UPNP_MANUFACTURER_URL: "http://www.notphilips.com"}, - ), - ) - - assert result["type"] == "abort" - assert result["reason"] == "not_hue_bridge" - - -async def test_bridge_ssdp_emulated_hue(hass): - """Test if discovery info is from an emulated hue instance.""" - result = await hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location="http://0.0.0.0/", - upnp={ - ssdp.ATTR_UPNP_FRIENDLY_NAME: "Home Assistant Bridge", - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - ssdp.ATTR_UPNP_SERIAL: "1234", - }, - ), - ) - - assert result["type"] == "abort" - assert result["reason"] == "not_hue_bridge" - - -async def test_bridge_ssdp_missing_location(hass): - """Test if discovery info is missing a location attribute.""" - result = await hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - upnp={ - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - ssdp.ATTR_UPNP_SERIAL: "1234", - }, - ), - ) - - assert result["type"] == "abort" - assert result["reason"] == "not_hue_bridge" - - -async def test_bridge_ssdp_missing_serial(hass): - """Test if discovery info is a serial attribute.""" - result = await hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location="http://0.0.0.0/", - upnp={ - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - }, - ), - ) - - assert result["type"] == "abort" - assert result["reason"] == "not_hue_bridge" - - -@pytest.mark.parametrize( - "location,reason", - ( - ("http:///", "not_hue_bridge"), - ("http://[fd00::eeb5:faff:fe84:b17d]/description.xml", "invalid_host"), - ), -) -async def test_bridge_ssdp_invalid_location(hass, location, reason): - """Test if discovery info is a serial attribute.""" - result = await hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location=location, - upnp={ - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - ssdp.ATTR_UPNP_SERIAL: "1234", - }, - ), - ) - - assert result["type"] == "abort" - assert result["reason"] == reason - - -async def test_bridge_ssdp_espalexa(hass): - """Test if discovery info is from an Espalexa based device.""" - result = await hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location="http://0.0.0.0/", - upnp={ - ssdp.ATTR_UPNP_FRIENDLY_NAME: "Espalexa (0.0.0.0)", - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - ssdp.ATTR_UPNP_SERIAL: "1234", - }, - ), - ) - - assert result["type"] == "abort" - assert result["reason"] == "not_hue_bridge" - - -async def test_bridge_ssdp_already_configured(hass, aioclient_mock): - """Test if a discovered bridge has already been configured.""" - create_mock_api_discovery(aioclient_mock, [("0.0.0.0", "1234")]) - MockConfigEntry( - domain="hue", unique_id="1234", data={"host": "0.0.0.0"} - ).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location="http://0.0.0.0/", - upnp={ - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - ssdp.ATTR_UPNP_SERIAL: "1234", - }, - ), - ) - - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - - async def test_import_with_no_config(hass, aioclient_mock): """Test importing a host without an existing config file.""" create_mock_api_discovery(aioclient_mock, [("0.0.0.0", "1234")]) @@ -634,33 +464,6 @@ async def test_bridge_homekit_already_configured(hass, aioclient_mock): assert result["reason"] == "already_configured" -async def test_ssdp_discovery_update_configuration(hass, aioclient_mock): - """Test if a discovered bridge is configured and updated with new host.""" - create_mock_api_discovery(aioclient_mock, [("1.1.1.1", "aabbccddeeff")]) - entry = MockConfigEntry( - domain="hue", unique_id="aabbccddeeff", data={"host": "0.0.0.0"} - ) - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - const.DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data=ssdp.SsdpServiceInfo( - ssdp_usn="mock_usn", - ssdp_st="mock_st", - ssdp_location="http://1.1.1.1/", - upnp={ - ssdp.ATTR_UPNP_MANUFACTURER_URL: config_flow.HUE_MANUFACTURERURL[0], - ssdp.ATTR_UPNP_SERIAL: "aabbccddeeff", - }, - ), - ) - - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - assert entry.data["host"] == "1.1.1.1" - - async def test_options_flow_v1(hass): """Test options config flow for a V1 bridge.""" entry = MockConfigEntry( @@ -772,7 +575,7 @@ async def test_bridge_zeroconf_already_exists(hass, aioclient_mock): ) entry = MockConfigEntry( domain="hue", - source=config_entries.SOURCE_SSDP, + source=config_entries.SOURCE_HOMEKIT, data={"host": "0.0.0.0"}, unique_id="ecb5faabcabc", ) diff --git a/tests/components/hue/test_light_v2.py b/tests/components/hue/test_light_v2.py index bba28e03477..0427c9f7c4d 100644 --- a/tests/components/hue/test_light_v2.py +++ b/tests/components/hue/test_light_v2.py @@ -277,7 +277,7 @@ async def test_light_added(hass, mock_bridge_v2): await setup_platform(hass, mock_bridge_v2, "light") - test_entity_id = "light.hue_fake_light" + test_entity_id = "light.hue_mocked_device" # verify entity does not exist before we start assert hass.states.get(test_entity_id) is None @@ -290,7 +290,7 @@ async def test_light_added(hass, mock_bridge_v2): test_entity = hass.states.get(test_entity_id) assert test_entity is not None assert test_entity.state == "off" - assert test_entity.attributes["friendly_name"] == FAKE_LIGHT["metadata"]["name"] + assert test_entity.attributes["friendly_name"] == FAKE_DEVICE["metadata"]["name"] async def test_light_availability(hass, mock_bridge_v2, v2_resources_test_data): diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py index 43789988003..bd792f3230f 100644 --- a/tests/components/huisbaasje/test_sensor.py +++ b/tests/components/huisbaasje/test_sensor.py @@ -15,9 +15,9 @@ from homeassistant.const import ( CONF_ID, CONF_PASSWORD, CONF_USERNAME, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, - VOLUME_CUBIC_METERS, + UnitOfEnergy, + UnitOfPower, + UnitOfVolume, ) from homeassistant.core import HomeAssistant @@ -65,7 +65,9 @@ async def test_setup_entry(hass: HomeAssistant): current_power.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT ) - assert current_power.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert ( + current_power.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT + ) current_power_in = hass.states.get("sensor.huisbaasje_current_power_in_peak") assert current_power_in.state == "1012.0" @@ -78,7 +80,10 @@ async def test_setup_entry(hass: HomeAssistant): current_power_in.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT ) - assert current_power_in.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert ( + current_power_in.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfPower.WATT + ) current_power_in_low = hass.states.get( "sensor.huisbaasje_current_power_in_off_peak" @@ -94,7 +99,8 @@ async def test_setup_entry(hass: HomeAssistant): is SensorStateClass.MEASUREMENT ) assert ( - current_power_in_low.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + current_power_in_low.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfPower.WATT ) current_power_out = hass.states.get("sensor.huisbaasje_current_power_out_peak") @@ -108,7 +114,10 @@ async def test_setup_entry(hass: HomeAssistant): current_power_out.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT ) - assert current_power_out.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert ( + current_power_out.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfPower.WATT + ) current_power_out_low = hass.states.get( "sensor.huisbaasje_current_power_out_off_peak" @@ -124,7 +133,8 @@ async def test_setup_entry(hass: HomeAssistant): is SensorStateClass.MEASUREMENT ) assert ( - current_power_out_low.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + current_power_out_low.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfPower.WATT ) energy_consumption_peak_today = hass.states.get( @@ -145,7 +155,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_consumption_peak_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == ENERGY_KILO_WATT_HOUR + == UnitOfEnergy.KILO_WATT_HOUR ) energy_consumption_off_peak_today = hass.states.get( @@ -166,7 +176,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_consumption_off_peak_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == ENERGY_KILO_WATT_HOUR + == UnitOfEnergy.KILO_WATT_HOUR ) energy_production_peak_today = hass.states.get( @@ -187,7 +197,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_production_peak_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == ENERGY_KILO_WATT_HOUR + == UnitOfEnergy.KILO_WATT_HOUR ) energy_production_off_peak_today = hass.states.get( @@ -208,7 +218,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_production_off_peak_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == ENERGY_KILO_WATT_HOUR + == UnitOfEnergy.KILO_WATT_HOUR ) energy_today = hass.states.get("sensor.huisbaasje_energy_today") @@ -223,7 +233,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == ENERGY_KILO_WATT_HOUR + == UnitOfEnergy.KILO_WATT_HOUR ) energy_this_week = hass.states.get("sensor.huisbaasje_energy_this_week") @@ -239,7 +249,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_this_week.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == ENERGY_KILO_WATT_HOUR + == UnitOfEnergy.KILO_WATT_HOUR ) energy_this_month = hass.states.get("sensor.huisbaasje_energy_this_month") @@ -255,7 +265,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_this_month.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == ENERGY_KILO_WATT_HOUR + == UnitOfEnergy.KILO_WATT_HOUR ) energy_this_year = hass.states.get("sensor.huisbaasje_energy_this_year") @@ -271,7 +281,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( energy_this_year.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == ENERGY_KILO_WATT_HOUR + == UnitOfEnergy.KILO_WATT_HOUR ) current_gas = hass.states.get("sensor.huisbaasje_current_gas") @@ -294,7 +304,10 @@ async def test_setup_entry(hass: HomeAssistant): gas_today.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING ) - assert gas_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + assert ( + gas_today.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfVolume.CUBIC_METERS + ) gas_this_week = hass.states.get("sensor.huisbaasje_gas_this_week") assert gas_this_week.state == "5.6" @@ -306,7 +319,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( gas_this_week.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == VOLUME_CUBIC_METERS + == UnitOfVolume.CUBIC_METERS ) gas_this_month = hass.states.get("sensor.huisbaasje_gas_this_month") @@ -319,7 +332,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( gas_this_month.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == VOLUME_CUBIC_METERS + == UnitOfVolume.CUBIC_METERS ) gas_this_year = hass.states.get("sensor.huisbaasje_gas_this_year") @@ -332,7 +345,7 @@ async def test_setup_entry(hass: HomeAssistant): ) assert ( gas_this_year.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == VOLUME_CUBIC_METERS + == UnitOfVolume.CUBIC_METERS ) # Assert mocks are called diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py index 0789a344ec6..80a585f9740 100644 --- a/tests/components/hyperion/__init__.py +++ b/tests/components/hyperion/__init__.py @@ -151,7 +151,7 @@ async def setup_test_config_entry( config_entry = config_entry or add_test_config_entry(hass, options=options) hyperion_client = hyperion_client or create_mock_client() - # pylint: disable=attribute-defined-outside-init + # pylint: disable-next=attribute-defined-outside-init hyperion_client.instances = [TEST_INSTANCE_1] with patch( diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index 27f6f25856d..ad71f392bc6 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -765,7 +765,7 @@ async def test_options_priority(hass: HomeAssistant) -> None: {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, blocking=True, ) - # pylint: disable=unsubscriptable-object + # pylint: disable-next=unsubscriptable-object assert client.async_send_set_color.call_args[1][CONF_PRIORITY] == new_priority diff --git a/tests/components/ign_sismologia/test_geo_location.py b/tests/components/ign_sismologia/test_geo_location.py index 1ad5ae2a2b2..f04ccbaaf19 100644 --- a/tests/components/ign_sismologia/test_geo_location.py +++ b/tests/components/ign_sismologia/test_geo_location.py @@ -24,7 +24,7 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_RADIUS, EVENT_HOMEASSISTANT_START, - LENGTH_KILOMETERS, + UnitOfLength, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -128,7 +128,7 @@ async def test_setup(hass): ), ATTR_IMAGE_URL: "http://image.url/map.jpg", ATTR_MAGNITUDE: 5.7, - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "ign_sismologia", ATTR_ICON: "mdi:pulse", } @@ -144,7 +144,7 @@ async def test_setup(hass): ATTR_FRIENDLY_NAME: "M 4.6", ATTR_TITLE: "Title 2", ATTR_MAGNITUDE: 4.6, - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "ign_sismologia", ATTR_ICON: "mdi:pulse", } @@ -160,7 +160,7 @@ async def test_setup(hass): ATTR_FRIENDLY_NAME: "Region 3", ATTR_TITLE: "Title 3", ATTR_REGION: "Region 3", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "ign_sismologia", ATTR_ICON: "mdi:pulse", } diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 37f84fb971f..b3322a79d20 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -40,16 +40,6 @@ async def setup_image_processing(hass, aiohttp_unused_port): await hass.async_block_till_done() -async def setup_image_processing_alpr(hass): - """Set up things to be run when tests are started.""" - config = {ip.DOMAIN: {"platform": "demo"}, "camera": {"platform": "demo"}} - - await async_setup_component(hass, ip.DOMAIN, config) - await hass.async_block_till_done() - - return async_capture_events(hass, "image_processing.found_plate") - - async def setup_image_processing_face(hass): """Set up things to be run when tests are started.""" config = {ip.DOMAIN: {"platform": "demo"}, "camera": {"platform": "demo"}} @@ -119,77 +109,6 @@ async def test_get_image_without_exists_camera( assert state.state == "0" -async def test_alpr_event_single_call(hass, aioclient_mock): - """Set up and scan a picture and test plates from event.""" - alpr_events = await setup_image_processing_alpr(hass) - aioclient_mock.get(get_url(hass), content=b"image") - - common.async_scan(hass, entity_id="image_processing.demo_alpr") - await hass.async_block_till_done() - - state = hass.states.get("image_processing.demo_alpr") - - assert len(alpr_events) == 4 - assert state.state == "AC3829" - - event_data = [ - event.data for event in alpr_events if event.data.get("plate") == "AC3829" - ] - assert len(event_data) == 1 - assert event_data[0]["plate"] == "AC3829" - assert event_data[0]["confidence"] == 98.3 - assert event_data[0]["entity_id"] == "image_processing.demo_alpr" - - -async def test_alpr_event_double_call(hass, aioclient_mock): - """Set up and scan a picture and test plates from event.""" - alpr_events = await setup_image_processing_alpr(hass) - aioclient_mock.get(get_url(hass), content=b"image") - - common.async_scan(hass, entity_id="image_processing.demo_alpr") - common.async_scan(hass, entity_id="image_processing.demo_alpr") - await hass.async_block_till_done() - - state = hass.states.get("image_processing.demo_alpr") - - assert len(alpr_events) == 4 - assert state.state == "AC3829" - - event_data = [ - event.data for event in alpr_events if event.data.get("plate") == "AC3829" - ] - assert len(event_data) == 1 - assert event_data[0]["plate"] == "AC3829" - assert event_data[0]["confidence"] == 98.3 - assert event_data[0]["entity_id"] == "image_processing.demo_alpr" - - -@patch( - "homeassistant.components.demo.image_processing.DemoImageProcessingAlpr.confidence", - new_callable=PropertyMock(return_value=95), -) -async def test_alpr_event_single_call_confidence(confidence_mock, hass, aioclient_mock): - """Set up and scan a picture and test plates from event.""" - alpr_events = await setup_image_processing_alpr(hass) - aioclient_mock.get(get_url(hass), content=b"image") - - common.async_scan(hass, entity_id="image_processing.demo_alpr") - await hass.async_block_till_done() - - state = hass.states.get("image_processing.demo_alpr") - - assert len(alpr_events) == 2 - assert state.state == "AC3829" - - event_data = [ - event.data for event in alpr_events if event.data.get("plate") == "AC3829" - ] - assert len(event_data) == 1 - assert event_data[0]["plate"] == "AC3829" - assert event_data[0]["confidence"] == 98.3 - assert event_data[0]["entity_id"] == "image_processing.demo_alpr" - - async def test_face_event_call(hass, aioclient_mock): """Set up and scan a picture and test faces from event.""" face_events = await setup_image_processing_face(hass) diff --git a/tests/components/imap/__init__.py b/tests/components/imap/__init__.py new file mode 100644 index 00000000000..db4c252334c --- /dev/null +++ b/tests/components/imap/__init__.py @@ -0,0 +1 @@ +"""Tests for the imap integration.""" diff --git a/tests/components/imap/test_config_flow.py b/tests/components/imap/test_config_flow.py new file mode 100644 index 00000000000..7fc5f998843 --- /dev/null +++ b/tests/components/imap/test_config_flow.py @@ -0,0 +1,349 @@ +"""Test the imap config flow.""" +import asyncio +from unittest.mock import patch + +from aioimaplib import AioImapException +import pytest + +from homeassistant import config_entries +from homeassistant.components.imap.const import ( + CONF_CHARSET, + CONF_FOLDER, + CONF_SEARCH, + DOMAIN, +) +from homeassistant.components.imap.errors import InvalidAuth, InvalidFolder +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +MOCK_CONFIG = { + "username": "email@email.com", + "password": "password", + "server": "imap.server.com", + "port": 993, + "charset": "utf-8", + "folder": "INBOX", + "search": "UnSeen UnDeleted", +} + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server" + ) as mock_client, patch( + "homeassistant.components.imap.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + mock_client.return_value.search.return_value = ( + "OK", + [b""], + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "email@email.com" + assert result2["data"] == MOCK_CONFIG + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_flow_success(hass: HomeAssistant) -> None: + """Test a successful import of yaml.""" + with patch( + "homeassistant.components.imap.config_flow.connect_to_server" + ) as mock_client, patch( + "homeassistant.components.imap.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + mock_client.return_value.search.return_value = ( + "OK", + [b""], + ) + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "name": "IMAP", + "username": "email@email.com", + "password": "password", + "server": "imap.server.com", + "port": 993, + "charset": "utf-8", + "folder": "INBOX", + "search": "UnSeen UnDeleted", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "IMAP" + assert result2["data"] == { + "name": "IMAP", + "username": "email@email.com", + "password": "password", + "server": "imap.server.com", + "port": 993, + "charset": "utf-8", + "folder": "INBOX", + "search": "UnSeen UnDeleted", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_entry_already_configured(hass: HomeAssistant) -> None: + """Test aborting if the entry is already configured.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "email@email.com", + "password": "password", + "server": "imap.server.com", + "port": 993, + "charset": "utf-8", + "folder": "INBOX", + "search": "UnSeen UnDeleted", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == { + CONF_USERNAME: "invalid_auth", + CONF_PASSWORD: "invalid_auth", + } + + +@pytest.mark.parametrize( + "exc", + [asyncio.TimeoutError, AioImapException("")], +) +async def test_form_cannot_connect(hass: HomeAssistant, exc: Exception) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server", + side_effect=exc, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_invalid_charset(hass: HomeAssistant) -> None: + """Test we handle invalid charset.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server" + ) as mock_client: + mock_client.return_value.search.return_value = ( + "NO", + [b"The specified charset is not supported"], + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {CONF_CHARSET: "invalid_charset"} + + +async def test_form_invalid_folder(hass: HomeAssistant) -> None: + """Test we handle invalid folder selection.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server", + side_effect=InvalidFolder, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {CONF_FOLDER: "invalid_folder"} + + +async def test_form_invalid_search(hass: HomeAssistant) -> None: + """Test we handle invalid search.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server" + ) as mock_client: + mock_client.return_value.search.return_value = ( + "BAD", + [b"Invalid search"], + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], MOCK_CONFIG + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {CONF_SEARCH: "invalid_search"} + + +async def test_reauth_success(hass: HomeAssistant) -> None: + """Test we can reauth.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_CONFIG, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["description_placeholders"] == {CONF_USERNAME: "email@email.com"} + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server" + ) as mock_client, patch( + "homeassistant.components.imap.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + mock_client.return_value.search.return_value = ( + "OK", + [b""], + ) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth_failed(hass: HomeAssistant) -> None: + """Test we can reauth.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_CONFIG, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-wrong-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == { + CONF_USERNAME: "invalid_auth", + CONF_PASSWORD: "invalid_auth", + } + + +async def test_reauth_failed_conn_error(hass: HomeAssistant) -> None: + """Test we can reauth.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + }, + data=MOCK_CONFIG, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.imap.config_flow.connect_to_server", + side_effect=asyncio.TimeoutError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-wrong-password", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index 78648852803..3ea9fd35d0d 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -72,7 +72,7 @@ def get_mock_call_fixture(request): if request.param == influxdb.API_VERSION_2: return lambda body, precision=None: v2_call(body, precision) - # pylint: disable=unnecessary-lambda + # pylint: disable-next=unnecessary-lambda return lambda body, precision=None: call(body, time_precision=precision) diff --git a/tests/components/influxdb/test_sensor.py b/tests/components/influxdb/test_sensor.py index 6bc2014d24e..d595006f413 100644 --- a/tests/components/influxdb/test_sensor.py +++ b/tests/components/influxdb/test_sensor.py @@ -245,7 +245,7 @@ async def test_minimal_config(hass, mock_client, config_ext, queries, set_query_ "unit_of_measurement": "unit", "measurement": "measurement", "where": "where", - "value_template": "value", + "value_template": "123", "database": "db2", "group_function": "fn", "field": "field", diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py index 2e044c7a90f..18318d82b75 100644 --- a/tests/components/input_boolean/test_init.py +++ b/tests/components/input_boolean/test_init.py @@ -1,5 +1,5 @@ """The tests for the input_boolean component.""" -# pylint: disable=protected-access + import logging from unittest.mock import patch diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index 9e694488797..16f96f8343d 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -1,5 +1,5 @@ """Tests for the Input slider component.""" -# pylint: disable=protected-access + import datetime from unittest.mock import patch diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index 7ba7489f644..66b882e3b3f 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -1,5 +1,5 @@ """The tests for the Input number component.""" -# pylint: disable=protected-access + from unittest.mock import patch import pytest diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index 1a1618d7805..60260ca9c78 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -1,5 +1,5 @@ """The tests for the Input select component.""" -# pylint: disable=protected-access + from unittest.mock import patch import pytest diff --git a/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py index 8256d9d351f..90c327e6a78 100644 --- a/tests/components/input_text/test_init.py +++ b/tests/components/input_text/test_init.py @@ -1,5 +1,5 @@ """The tests for the Input text component.""" -# pylint: disable=protected-access + from unittest.mock import patch import pytest diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 35a32ef969c..99a505cc630 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -12,6 +12,7 @@ from homeassistant.components.insteon.config_flow import ( STEP_ADD_OVERRIDE, STEP_ADD_X10, STEP_CHANGE_HUB_CONFIG, + STEP_CHANGE_PLM_CONFIG, STEP_HUB_V2, STEP_REMOVE_OVERRIDE, STEP_REMOVE_X10, @@ -334,6 +335,28 @@ async def test_options_change_hub_config(hass: HomeAssistant): assert config_entry.data == {**user_input, CONF_HUB_VERSION: 2} +async def test_options_change_plm_config(hass: HomeAssistant): + """Test changing PLM config.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + entry_id="abcde12345", + data=MOCK_USER_INPUT_PLM, + options={}, + ) + + config_entry.add_to_hass(hass) + result = await _options_init_form( + hass, config_entry.entry_id, STEP_CHANGE_PLM_CONFIG + ) + + user_input = {CONF_DEVICE: "/dev/some_other_device"} + result, _ = await _options_form(hass, result["flow_id"], user_input) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert config_entry.options == {} + assert config_entry.data == user_input + + async def test_options_add_device_override(hass: HomeAssistant): """Test adding a device override.""" config_entry = MockConfigEntry( diff --git a/tests/components/insteon/test_init.py b/tests/components/insteon/test_init.py index eb821f15cb5..83bc5814a3b 100644 --- a/tests/components/insteon/test_init.py +++ b/tests/components/insteon/test_init.py @@ -73,7 +73,7 @@ async def test_setup_entry(hass: HomeAssistant): await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - # pylint: disable=no-member + # pylint: disable-next=no-member assert insteon.devices.async_save.call_count == 1 assert mock_close.called diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 9cc1bbb2692..12f9781a81a 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -5,17 +5,13 @@ from unittest.mock import patch from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, - DATA_KILOBYTES, - DATA_RATE_BYTES_PER_SECOND, - ENERGY_KILO_WATT_HOUR, - ENERGY_WATT_HOUR, - POWER_KILO_WATT, - POWER_WATT, STATE_UNAVAILABLE, STATE_UNKNOWN, - TIME_HOURS, - TIME_SECONDS, + UnitOfDataRate, + UnitOfEnergy, + UnitOfInformation, UnitOfPower, + UnitOfTime, ) from homeassistant.core import HomeAssistant, State from homeassistant.setup import async_setup_component @@ -40,7 +36,9 @@ async def test_state(hass) -> None: assert await async_setup_component(hass, "sensor", config) entity_id = config["sensor"]["source"] - hass.states.async_set(entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT}) + hass.states.async_set( + entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT} + ) await hass.async_block_till_done() state = hass.states.get("sensor.integration") @@ -55,7 +53,7 @@ async def test_state(hass) -> None: 1, { "device_class": SensorDeviceClass.POWER, - ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT, }, force_update=True, ) @@ -67,7 +65,7 @@ async def test_state(hass) -> None: # Testing a power sensor at 1 KiloWatts for 1hour = 1kWh assert round(float(state.state), config["sensor"]["round"]) == 1.0 - assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get("device_class") == SensorDeviceClass.ENERGY assert state.attributes.get("state_class") is SensorStateClass.TOTAL @@ -82,7 +80,7 @@ async def test_restore_state(hass: HomeAssistant) -> None: "100.0", { "device_class": SensorDeviceClass.ENERGY, - "unit_of_measurement": ENERGY_KILO_WATT_HOUR, + "unit_of_measurement": UnitOfEnergy.KILO_WATT_HOUR, }, ), ), @@ -103,7 +101,7 @@ async def test_restore_state(hass: HomeAssistant) -> None: state = hass.states.get("sensor.integration") assert state assert state.state == "100.00" - assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get("device_class") == SensorDeviceClass.ENERGY @@ -166,7 +164,7 @@ async def test_trapezoidal(hass): hass.states.async_set( entity_id, value, - {ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT}, force_update=True, ) await hass.async_block_till_done() @@ -176,7 +174,7 @@ async def test_trapezoidal(hass): assert round(float(state.state), config["sensor"]["round"]) == 8.33 - assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR async def test_left(hass): @@ -194,7 +192,9 @@ async def test_left(hass): assert await async_setup_component(hass, "sensor", config) entity_id = config["sensor"]["source"] - hass.states.async_set(entity_id, 0, {ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT}) + hass.states.async_set( + entity_id, 0, {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT} + ) await hass.async_block_till_done() # Testing a power sensor with non-monotonic intervals and values @@ -204,7 +204,7 @@ async def test_left(hass): hass.states.async_set( entity_id, value, - {ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT}, force_update=True, ) await hass.async_block_till_done() @@ -214,7 +214,7 @@ async def test_left(hass): assert round(float(state.state), config["sensor"]["round"]) == 7.5 - assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR async def test_right(hass): @@ -232,7 +232,9 @@ async def test_right(hass): assert await async_setup_component(hass, "sensor", config) entity_id = config["sensor"]["source"] - hass.states.async_set(entity_id, 0, {ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT}) + hass.states.async_set( + entity_id, 0, {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT} + ) await hass.async_block_till_done() # Testing a power sensor with non-monotonic intervals and values @@ -242,7 +244,7 @@ async def test_right(hass): hass.states.async_set( entity_id, value, - {ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT}, force_update=True, ) await hass.async_block_till_done() @@ -252,7 +254,7 @@ async def test_right(hass): assert round(float(state.state), config["sensor"]["round"]) == 9.17 - assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR async def test_prefix(hass): @@ -270,13 +272,16 @@ async def test_prefix(hass): assert await async_setup_component(hass, "sensor", config) entity_id = config["sensor"]["source"] - hass.states.async_set(entity_id, 1000, {"unit_of_measurement": POWER_WATT}) + hass.states.async_set(entity_id, 1000, {"unit_of_measurement": UnitOfPower.WATT}) await hass.async_block_till_done() now = dt_util.utcnow() + timedelta(seconds=3600) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.states.async_set( - entity_id, 1000, {"unit_of_measurement": POWER_WATT}, force_update=True + entity_id, + 1000, + {"unit_of_measurement": UnitOfPower.WATT}, + force_update=True, ) await hass.async_block_till_done() @@ -285,7 +290,7 @@ async def test_prefix(hass): # Testing a power sensor at 1000 Watts for 1hour = 1kWh assert round(float(state.state), config["sensor"]["round"]) == 1.0 - assert state.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.KILO_WATT_HOUR async def test_suffix(hass): @@ -297,7 +302,7 @@ async def test_suffix(hass): "source": "sensor.bytes_per_second", "round": 2, "unit_prefix": "k", - "unit_time": TIME_SECONDS, + "unit_time": UnitOfTime.SECONDS, } } @@ -305,7 +310,7 @@ async def test_suffix(hass): entity_id = config["sensor"]["source"] hass.states.async_set( - entity_id, 1000, {ATTR_UNIT_OF_MEASUREMENT: DATA_RATE_BYTES_PER_SECOND} + entity_id, 1000, {ATTR_UNIT_OF_MEASUREMENT: UnitOfDataRate.BYTES_PER_SECOND} ) await hass.async_block_till_done() @@ -314,7 +319,7 @@ async def test_suffix(hass): hass.states.async_set( entity_id, 1000, - {ATTR_UNIT_OF_MEASUREMENT: DATA_RATE_BYTES_PER_SECOND}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfDataRate.BYTES_PER_SECOND}, force_update=True, ) await hass.async_block_till_done() @@ -324,7 +329,7 @@ async def test_suffix(hass): # Testing a network speed sensor at 1000 bytes/s over 10s = 10kbytes assert round(float(state.state)) == 10 - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == DATA_KILOBYTES + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfInformation.KILOBYTES async def test_suffix_2(hass): @@ -335,7 +340,7 @@ async def test_suffix_2(hass): "name": "integration", "source": "sensor.cubic_meters_per_hour", "round": 2, - "unit_time": TIME_HOURS, + "unit_time": UnitOfTime.HOURS, } } @@ -384,7 +389,7 @@ async def test_units(hass): await hass.async_block_till_done() hass.states.async_set(entity_id, 200, {"unit_of_measurement": None}) await hass.async_block_till_done() - hass.states.async_set(entity_id, 300, {"unit_of_measurement": POWER_WATT}) + hass.states.async_set(entity_id, 300, {"unit_of_measurement": UnitOfPower.WATT}) await hass.async_block_till_done() state = hass.states.get("sensor.integration") @@ -392,7 +397,7 @@ async def test_units(hass): # Testing the sensor ignored the source sensor's units until # they became valid - assert state.attributes.get("unit_of_measurement") == ENERGY_WATT_HOUR + assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.WATT_HOUR # When source state goes to None / Unknown, expect an early exit without # changes to the state or unit_of_measurement @@ -401,7 +406,7 @@ async def test_units(hass): new_state = hass.states.get("sensor.integration") assert state == new_state - assert state.attributes.get("unit_of_measurement") == ENERGY_WATT_HOUR + assert state.attributes.get("unit_of_measurement") == UnitOfEnergy.WATT_HOUR async def test_device_class(hass): diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py index a936c11d0fa..beb17dc6b37 100644 --- a/tests/components/intent/test_init.py +++ b/tests/components/intent/test_init.py @@ -96,12 +96,11 @@ async def test_turn_on_intent(hass): hass.states.async_set("light.test_light", "off") calls = async_mock_service(hass, "light", SERVICE_TURN_ON) - response = await intent.async_handle( + await intent.async_handle( hass, "test", "HassTurnOn", {"name": {"value": "test light"}} ) await hass.async_block_till_done() - assert response.speech["plain"]["speech"] == "Turned test light on" assert len(calls) == 1 call = calls[0] assert call.domain == "light" @@ -118,12 +117,11 @@ async def test_turn_off_intent(hass): hass.states.async_set("light.test_light", "on") calls = async_mock_service(hass, "light", SERVICE_TURN_OFF) - response = await intent.async_handle( + await intent.async_handle( hass, "test", "HassTurnOff", {"name": {"value": "test light"}} ) await hass.async_block_till_done() - assert response.speech["plain"]["speech"] == "Turned test light off" assert len(calls) == 1 call = calls[0] assert call.domain == "light" @@ -140,12 +138,11 @@ async def test_toggle_intent(hass): hass.states.async_set("light.test_light", "off") calls = async_mock_service(hass, "light", SERVICE_TOGGLE) - response = await intent.async_handle( + await intent.async_handle( hass, "test", "HassToggle", {"name": {"value": "test light"}} ) await hass.async_block_till_done() - assert response.speech["plain"]["speech"] == "Toggled test light" assert len(calls) == 1 call = calls[0] assert call.domain == "light" @@ -167,12 +164,11 @@ async def test_turn_on_multiple_intent(hass): hass.states.async_set("light.test_lighter", "off") calls = async_mock_service(hass, "light", SERVICE_TURN_ON) - response = await intent.async_handle( - hass, "test", "HassTurnOn", {"name": {"value": "test lights"}} + await intent.async_handle( + hass, "test", "HassTurnOn", {"name": {"value": "test lights 2"}} ) await hass.async_block_till_done() - assert response.speech["plain"]["speech"] == "Turned test lights 2 on" assert len(calls) == 1 call = calls[0] assert call.domain == "light" diff --git a/tests/components/iotawatt/__init__.py b/tests/components/iotawatt/__init__.py index 07ea6dfc15c..5f66c145ad6 100644 --- a/tests/components/iotawatt/__init__.py +++ b/tests/components/iotawatt/__init__.py @@ -22,27 +22,3 @@ OUTPUT_SENSOR = Sensor( mac_addr="mock-mac", fromStart=True, ) - -INPUT_ACCUMULATED_SENSOR = Sensor( - channel="N/A", - base_name="My WattHour Accumulated Input Sensor", - suffix=".wh", - io_type="Input", - unit="WattHours", - value=500, - begin="", - mac_addr="mock-mac", - fromStart=False, -) - -OUTPUT_ACCUMULATED_SENSOR = Sensor( - channel="N/A", - base_name="My WattHour Accumulated Output Sensor", - suffix=".wh", - io_type="Output", - unit="WattHours", - value=200, - begin="", - mac_addr="mock-mac", - fromStart=False, -) diff --git a/tests/components/iotawatt/test_sensor.py b/tests/components/iotawatt/test_sensor.py index b025ed13d73..54463bbee6b 100644 --- a/tests/components/iotawatt/test_sensor.py +++ b/tests/components/iotawatt/test_sensor.py @@ -1,7 +1,6 @@ """Test setting up sensors.""" from datetime import timedelta -from homeassistant.components.iotawatt.const import ATTR_LAST_UPDATE from homeassistant.components.sensor import ( ATTR_STATE_CLASS, SensorDeviceClass, @@ -11,21 +10,15 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, - ENERGY_WATT_HOUR, - POWER_WATT, + UnitOfEnergy, + UnitOfPower, ) -from homeassistant.core import State from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from . import ( - INPUT_ACCUMULATED_SENSOR, - INPUT_SENSOR, - OUTPUT_ACCUMULATED_SENSOR, - OUTPUT_SENSOR, -) +from . import INPUT_SENSOR, OUTPUT_SENSOR -from tests.common import async_fire_time_changed, mock_restore_cache +from tests.common import async_fire_time_changed async def test_sensor_type_input(hass, mock_iotawatt): @@ -47,7 +40,7 @@ async def test_sensor_type_input(hass, mock_iotawatt): assert state.state == "23" assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT assert state.attributes[ATTR_FRIENDLY_NAME] == "My Sensor" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfPower.WATT assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER assert state.attributes["channel"] == "1" assert state.attributes["type"] == "Input" @@ -74,7 +67,7 @@ async def test_sensor_type_output(hass, mock_iotawatt): assert state.state == "243" assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL assert state.attributes[ATTR_FRIENDLY_NAME] == "My WattHour Sensor" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfEnergy.WATT_HOUR assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY assert state.attributes["type"] == "Output" @@ -83,161 +76,3 @@ async def test_sensor_type_output(hass, mock_iotawatt): await hass.async_block_till_done() assert hass.states.get("sensor.my_watthour_sensor") is None - - -async def test_sensor_type_accumulated_output(hass, mock_iotawatt): - """Tests the sensor type of Accumulated Output and that it's properly restored from saved state.""" - mock_iotawatt.getSensors.return_value["sensors"][ - "my_watthour_accumulated_output_sensor_key" - ] = OUTPUT_ACCUMULATED_SENSOR - - DUMMY_DATE = "2021-09-01T14:00:00+10:00" - - mock_restore_cache( - hass, - ( - State( - "sensor.my_watthour_accumulated_output_sensor_wh_accumulated", - "100.0", - { - "device_class": SensorDeviceClass.ENERGY, - "unit_of_measurement": ENERGY_WATT_HOUR, - "last_update": DUMMY_DATE, - }, - ), - ), - ) - - assert await async_setup_component(hass, "iotawatt", {}) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids()) == 1 - - state = hass.states.get( - "sensor.my_watthour_accumulated_output_sensor_wh_accumulated" - ) - assert state is not None - - assert state.state == "300.0" # 100 + 200 - assert ( - state.attributes[ATTR_FRIENDLY_NAME] - == "My WattHour Accumulated Output Sensor.wh Accumulated" - ) - assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR - assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY - assert state.attributes["type"] == "Output" - assert state.attributes[ATTR_LAST_UPDATE] is not None - assert state.attributes[ATTR_LAST_UPDATE] != DUMMY_DATE - - -async def test_sensor_type_accumulated_output_error_restore(hass, mock_iotawatt): - """Tests the sensor type of Accumulated Output and that it's properly restored from saved state.""" - mock_iotawatt.getSensors.return_value["sensors"][ - "my_watthour_accumulated_output_sensor_key" - ] = OUTPUT_ACCUMULATED_SENSOR - - DUMMY_DATE = "2021-09-01T14:00:00+10:00" - - mock_restore_cache( - hass, - ( - State( - "sensor.my_watthour_accumulated_output_sensor_wh_accumulated", - "unknown", - ), - ), - ) - - assert await async_setup_component(hass, "iotawatt", {}) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids()) == 1 - - state = hass.states.get( - "sensor.my_watthour_accumulated_output_sensor_wh_accumulated" - ) - assert state is not None - - assert state.state == "200.0" # Returns the new read as restore failed. - assert ( - state.attributes[ATTR_FRIENDLY_NAME] - == "My WattHour Accumulated Output Sensor.wh Accumulated" - ) - assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR - assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY - assert state.attributes["type"] == "Output" - assert state.attributes[ATTR_LAST_UPDATE] is not None - assert state.attributes[ATTR_LAST_UPDATE] != DUMMY_DATE - - -async def test_sensor_type_multiple_accumulated_output(hass, mock_iotawatt): - """Tests the sensor type of Accumulated Output and that it's properly restored from saved state.""" - mock_iotawatt.getSensors.return_value["sensors"][ - "my_watthour_accumulated_output_sensor_key" - ] = OUTPUT_ACCUMULATED_SENSOR - mock_iotawatt.getSensors.return_value["sensors"][ - "my_watthour_accumulated_input_sensor_key" - ] = INPUT_ACCUMULATED_SENSOR - - DUMMY_DATE = "2021-09-01T14:00:00+10:00" - - mock_restore_cache( - hass, - ( - State( - "sensor.my_watthour_accumulated_output_sensor_wh_accumulated", - "100.0", - { - "device_class": SensorDeviceClass.ENERGY, - "unit_of_measurement": ENERGY_WATT_HOUR, - "last_update": DUMMY_DATE, - }, - ), - State( - "sensor.my_watthour_accumulated_input_sensor_wh_accumulated", - "50.0", - { - "device_class": SensorDeviceClass.ENERGY, - "unit_of_measurement": ENERGY_WATT_HOUR, - "last_update": DUMMY_DATE, - }, - ), - ), - ) - - assert await async_setup_component(hass, "iotawatt", {}) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids()) == 2 - - state = hass.states.get( - "sensor.my_watthour_accumulated_output_sensor_wh_accumulated" - ) - assert state is not None - - assert state.state == "300.0" # 100 + 200 - assert ( - state.attributes[ATTR_FRIENDLY_NAME] - == "My WattHour Accumulated Output Sensor.wh Accumulated" - ) - assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_WATT_HOUR - assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY - assert state.attributes["type"] == "Output" - assert state.attributes[ATTR_LAST_UPDATE] is not None - assert state.attributes[ATTR_LAST_UPDATE] != DUMMY_DATE - - state = hass.states.get( - "sensor.my_watthour_accumulated_input_sensor_wh_accumulated" - ) - assert state is not None - - assert state.state == "550.0" # 50 + 500 - assert ( - state.attributes[ATTR_FRIENDLY_NAME] - == "My WattHour Accumulated Input Sensor.wh Accumulated" - ) - assert state.attributes[ATTR_LAST_UPDATE] is not None - assert state.attributes[ATTR_LAST_UPDATE] != DUMMY_DATE diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index d772c6e9163..f8dd94ffc72 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -3,7 +3,10 @@ from datetime import datetime from unittest.mock import patch from homeassistant.components.ipp.const import DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import ( + ATTR_OPTIONS as SENSOR_ATTR_OPTIONS, + DOMAIN as SENSOR_DOMAIN, +) from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -41,6 +44,11 @@ async def test_sensors( assert state assert state.attributes.get(ATTR_ICON) == "mdi:printer" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.attributes.get(SENSOR_ATTR_OPTIONS) == ["idle", "printing", "stopped"] + + entry = registry.async_get("sensor.epson_xp_6000_series") + assert entry + assert entry.translation_key == "printer" state = hass.states.get("sensor.epson_xp_6000_series_black_ink") assert state diff --git a/tests/components/isy994/__init__.py b/tests/components/isy994/__init__.py index 9aee1e15905..10784fff737 100644 --- a/tests/components/isy994/__init__.py +++ b/tests/components/isy994/__init__.py @@ -1 +1 @@ -"""Tests for the Universal Devices ISY994 integration.""" +"""Tests for the Universal Devices ISY/IoX integration.""" diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index b87662718e5..c6d20daec72 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -1,4 +1,4 @@ -"""Test the Universal Devices ISY994 config flow.""" +"""Test the Universal Devices ISY/IoX config flow.""" import re from unittest.mock import patch @@ -44,7 +44,7 @@ MOCK_USER_INPUT = { CONF_PASSWORD: MOCK_PASSWORD, CONF_TLS_VER: MOCK_TLS_VERSION, } -MOCK_POLISY_USER_INPUT = { +MOCK_IOX_USER_INPUT = { CONF_HOST: f"http://{MOCK_HOSTNAME}:8080", CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD, @@ -587,14 +587,54 @@ async def test_form_dhcp_with_polisy(hass: HomeAssistant): ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - MOCK_POLISY_USER_INPUT, + MOCK_IOX_USER_INPUT, ) await hass.async_block_till_done() assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" assert result2["result"].unique_id == MOCK_UUID - assert result2["data"] == MOCK_POLISY_USER_INPUT + assert result2["data"] == MOCK_IOX_USER_INPUT + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_dhcp_with_eisy(hass: HomeAssistant): + """Test we can setup from dhcp with eisy.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=dhcp.DhcpServiceInfo( + ip="1.2.3.4", + hostname="eisy", + macaddress=MOCK_MAC, + ), + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + assert ( + _get_schema_default(result["data_schema"].schema, CONF_HOST) + == "http://1.2.3.4:8080" + ) + + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( + PATCH_ASYNC_SETUP, return_value=True + ) as mock_setup, patch( + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_IOX_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" + assert result2["result"].unique_id == MOCK_UUID + assert result2["data"] == MOCK_IOX_USER_INPUT assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/isy994/test_system_health.py b/tests/components/isy994/test_system_health.py index 63810b10464..c21d3257e1b 100644 --- a/tests/components/isy994/test_system_health.py +++ b/tests/components/isy994/test_system_health.py @@ -1,10 +1,10 @@ -"""Test ISY994 system health.""" +"""Test ISY system health.""" import asyncio from unittest.mock import Mock from aiohttp import ClientError -from homeassistant.components.isy994.const import DOMAIN, ISY994_ISY, ISY_URL_POSTFIX +from homeassistant.components.isy994.const import DOMAIN, ISY_URL_POSTFIX from homeassistant.const import CONF_HOST from homeassistant.setup import async_setup_component @@ -31,15 +31,16 @@ async def test_system_health(hass, aioclient_mock): unique_id=MOCK_UUID, ).add_to_hass(hass) - hass.data[DOMAIN] = {} - hass.data[DOMAIN][MOCK_ENTRY_ID] = {} - hass.data[DOMAIN][MOCK_ENTRY_ID][ISY994_ISY] = Mock( - connected=True, - websocket=Mock( - last_heartbeat=MOCK_HEARTBEAT, - status=MOCK_CONNECTED, - ), + isy_data = Mock( + root=Mock( + connected=True, + websocket=Mock( + last_heartbeat=MOCK_HEARTBEAT, + status=MOCK_CONNECTED, + ), + ) ) + hass.data[DOMAIN] = {MOCK_ENTRY_ID: isy_data} info = await get_system_health_info(hass, DOMAIN) @@ -67,15 +68,16 @@ async def test_system_health_failed_connect(hass, aioclient_mock): unique_id=MOCK_UUID, ).add_to_hass(hass) - hass.data[DOMAIN] = {} - hass.data[DOMAIN][MOCK_ENTRY_ID] = {} - hass.data[DOMAIN][MOCK_ENTRY_ID][ISY994_ISY] = Mock( - connected=True, - websocket=Mock( - last_heartbeat=MOCK_HEARTBEAT, - status=MOCK_CONNECTED, - ), + isy_data = Mock( + root=Mock( + connected=True, + websocket=Mock( + last_heartbeat=MOCK_HEARTBEAT, + status=MOCK_CONNECTED, + ), + ) ) + hass.data[DOMAIN] = {MOCK_ENTRY_ID: isy_data} info = await get_system_health_info(hass, DOMAIN) diff --git a/tests/components/justnimbus/test_config_flow.py b/tests/components/justnimbus/test_config_flow.py index 1d3565c21bd..1d75a72fe2e 100644 --- a/tests/components/justnimbus/test_config_flow.py +++ b/tests/components/justnimbus/test_config_flow.py @@ -83,7 +83,6 @@ async def test_abort_already_configured(hass: HomeAssistant) -> None: ) assert result.get("type") == FlowResultType.FORM assert result.get("errors") is None - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( flow_id=result["flow_id"], diff --git a/tests/components/kitchen_sink/__init__.py b/tests/components/kitchen_sink/__init__.py new file mode 100644 index 00000000000..0b6af465c4c --- /dev/null +++ b/tests/components/kitchen_sink/__init__.py @@ -0,0 +1 @@ +"""Tests for the Everything but the Kitchen Sink integration.""" diff --git a/tests/components/kitchen_sink/test_config_flow.py b/tests/components/kitchen_sink/test_config_flow.py new file mode 100644 index 00000000000..685e4db209e --- /dev/null +++ b/tests/components/kitchen_sink/test_config_flow.py @@ -0,0 +1,47 @@ +"""Test the Everything but the Kitchen Sink config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.kitchen_sink import DOMAIN + + +async def test_import(hass): + """Test that we can import a config entry.""" + with patch("homeassistant.components.kitchen_sink.async_setup_entry"): + assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + 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 == {} + + +async def test_import_once(hass): + """Test that we don't create multiple config entries.""" + with patch( + "homeassistant.components.kitchen_sink.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={}, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "Kitchen Sink" + assert result["data"] == {} + assert result["options"] == {} + mock_setup_entry.assert_called_once() + + # Test importing again doesn't create a 2nd entry + with patch( + "homeassistant.components.kitchen_sink.async_setup_entry" + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={}, + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" + mock_setup_entry.assert_not_called() diff --git a/tests/components/kitchen_sink/test_init.py b/tests/components/kitchen_sink/test_init.py new file mode 100644 index 00000000000..fcdf5ab8f54 --- /dev/null +++ b/tests/components/kitchen_sink/test_init.py @@ -0,0 +1,287 @@ +"""The tests for the Everything but the Kitchen Sink integration.""" +import datetime +from http import HTTPStatus +from unittest.mock import ANY + +import pytest + +from homeassistant.components.kitchen_sink import DOMAIN +from homeassistant.components.recorder import get_instance +from homeassistant.components.recorder.statistics import ( + async_add_external_statistics, + get_last_statistics, + list_statistic_ids, +) +from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM + +from tests.components.recorder.common import async_wait_recording_done + + +@pytest.fixture +def mock_history(hass): + """Mock history component loaded.""" + hass.config.components.add("history") + + +async def test_demo_statistics(recorder_mock, mock_history, hass): + """Test that the kitchen sink component makes some statistics available.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + await hass.async_start() + await async_wait_recording_done(hass) + + statistic_ids = await get_instance(hass).async_add_executor_job( + list_statistic_ids, hass + ) + assert { + "display_unit_of_measurement": "°C", + "has_mean": True, + "has_sum": False, + "name": "Outdoor temperature", + "source": DOMAIN, + "statistic_id": f"{DOMAIN}:temperature_outdoor", + "statistics_unit_of_measurement": "°C", + "unit_class": "temperature", + } in statistic_ids + assert { + "display_unit_of_measurement": "kWh", + "has_mean": False, + "has_sum": True, + "name": "Energy consumption 1", + "source": DOMAIN, + "statistic_id": f"{DOMAIN}:energy_consumption_kwh", + "statistics_unit_of_measurement": "kWh", + "unit_class": "energy", + } in statistic_ids + + +async def test_demo_statistics_growth(recorder_mock, mock_history, hass): + """Test that the kitchen sink sum statistics adds to the previous state.""" + hass.config.units = US_CUSTOMARY_SYSTEM + + now = dt_util.now() + last_week = now - datetime.timedelta(days=7) + last_week_midnight = last_week.replace(hour=0, minute=0, second=0, microsecond=0) + + statistic_id = f"{DOMAIN}:energy_consumption_kwh" + metadata = { + "source": DOMAIN, + "name": "Energy consumption 1", + "statistic_id": statistic_id, + "unit_of_measurement": "m³", + "has_mean": False, + "has_sum": True, + } + statistics = [ + { + "start": last_week_midnight, + "sum": 2**20, + } + ] + async_add_external_statistics(hass, metadata, statistics) + await async_wait_recording_done(hass) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + await hass.async_start() + await async_wait_recording_done(hass) + + statistics = await get_instance(hass).async_add_executor_job( + get_last_statistics, hass, 1, statistic_id, False, {"sum"} + ) + assert statistics[statistic_id][0]["sum"] > 2**20 + assert statistics[statistic_id][0]["sum"] <= (2**20 + 24) + + +async def test_issues_created(mock_history, hass, hass_client, hass_ws_client): + """Test issues are created and can be fixed.""" + assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + await hass.async_start() + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + { + "breaks_in_ha_version": "2023.1.1", + "created": ANY, + "dismissed_version": None, + "domain": DOMAIN, + "ignored": False, + "is_fixable": False, + "issue_id": "transmogrifier_deprecated", + "issue_domain": None, + "learn_more_url": "https://en.wiktionary.org/wiki/transmogrifier", + "severity": "warning", + "translation_key": "transmogrifier_deprecated", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": "2023.1.1", + "created": ANY, + "dismissed_version": None, + "domain": DOMAIN, + "ignored": False, + "is_fixable": True, + "issue_id": "out_of_blinker_fluid", + "issue_domain": None, + "learn_more_url": "https://www.youtube.com/watch?v=b9rntRxLlbU", + "severity": "critical", + "translation_key": "out_of_blinker_fluid", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": DOMAIN, + "ignored": False, + "is_fixable": False, + "issue_id": "unfixable_problem", + "issue_domain": None, + "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "severity": "warning", + "translation_key": "unfixable_problem", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": DOMAIN, + "ignored": False, + "is_fixable": True, + "issue_domain": None, + "issue_id": "bad_psu", + "learn_more_url": "https://www.youtube.com/watch?v=b9rntRxLlbU", + "severity": "critical", + "translation_key": "bad_psu", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": DOMAIN, + "is_fixable": True, + "issue_domain": None, + "issue_id": "cold_tea", + "learn_more_url": None, + "severity": "warning", + "translation_key": "cold_tea", + "translation_placeholders": None, + "ignored": False, + }, + ] + } + + url = "/api/repairs/issues/fix" + resp = await client.post( + url, json={"handler": DOMAIN, "issue_id": "out_of_blinker_fluid"} + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "data_schema": [], + "description_placeholders": None, + "errors": None, + "flow_id": ANY, + "handler": DOMAIN, + "last_step": None, + "step_id": "confirm", + "type": "form", + } + + url = f"/api/repairs/issues/fix/{flow_id}" + resp = await client.post(url) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "description": None, + "description_placeholders": None, + "flow_id": flow_id, + "handler": DOMAIN, + "type": "create_entry", + "version": 1, + } + + await ws_client.send_json({"id": 4, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert msg["result"] == { + "issues": [ + { + "breaks_in_ha_version": "2023.1.1", + "created": ANY, + "dismissed_version": None, + "domain": DOMAIN, + "ignored": False, + "is_fixable": False, + "issue_id": "transmogrifier_deprecated", + "issue_domain": None, + "learn_more_url": "https://en.wiktionary.org/wiki/transmogrifier", + "severity": "warning", + "translation_key": "transmogrifier_deprecated", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": DOMAIN, + "ignored": False, + "is_fixable": False, + "issue_id": "unfixable_problem", + "issue_domain": None, + "learn_more_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + "severity": "warning", + "translation_key": "unfixable_problem", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": DOMAIN, + "ignored": False, + "is_fixable": True, + "issue_domain": None, + "issue_id": "bad_psu", + "learn_more_url": "https://www.youtube.com/watch?v=b9rntRxLlbU", + "severity": "critical", + "translation_key": "bad_psu", + "translation_placeholders": None, + }, + { + "breaks_in_ha_version": None, + "created": ANY, + "dismissed_version": None, + "domain": DOMAIN, + "is_fixable": True, + "issue_domain": None, + "issue_id": "cold_tea", + "learn_more_url": None, + "severity": "warning", + "translation_key": "cold_tea", + "translation_placeholders": None, + "ignored": False, + }, + ] + } diff --git a/tests/components/knx/conftest.py b/tests/components/knx/conftest.py index b6735df3624..a67847d26fd 100644 --- a/tests/components/knx/conftest.py +++ b/tests/components/knx/conftest.py @@ -58,6 +58,9 @@ class KNXTestKit: async def patch_xknx_start(): """Patch `xknx.start` for unittests.""" + self.xknx.cemi_handler.send_telegram = AsyncMock( + side_effect=self._outgoing_telegrams.put + ) # after XKNX.__init__() to not overwrite it by the config entry again # before StateUpdater starts to avoid slow down of tests self.xknx.rate_limit = 0 @@ -72,7 +75,6 @@ class KNXTestKit: mock = Mock() mock.start = AsyncMock(side_effect=patch_xknx_start) mock.stop = AsyncMock() - mock.send_telegram = AsyncMock(side_effect=self._outgoing_telegrams.put) return mock def fish_xknx(*args, **kwargs): diff --git a/tests/components/konnected/test_panel.py b/tests/components/konnected/test_panel.py index e7e5784ba71..7eb23a45cd1 100644 --- a/tests/components/konnected/test_panel.py +++ b/tests/components/konnected/test_panel.py @@ -135,12 +135,10 @@ async def test_create_and_setup(hass, mock_panel): await device.update_switch("1", 0) # confirm the correct api is used - # pylint: disable=no-member assert mock_panel.put_device.call_count == 1 assert mock_panel.put_zone.call_count == 0 # confirm the settings are sent to the panel - # pylint: disable=no-member assert mock_panel.put_settings.call_args_list[0][1] == { "sensors": [{"pin": "1"}, {"pin": "2"}, {"pin": "5"}], "actuators": [{"trigger": 0, "pin": "8"}, {"trigger": 1, "pin": "9"}], @@ -153,16 +151,25 @@ async def test_create_and_setup(hass, mock_panel): } # confirm the device settings are saved in hass.data + # This test should not access hass.data since its integration internals assert device.stored_configuration == { "binary_sensors": { "1": { + "entity_id": "binary_sensor.konnected_445566_zone_1", "inverse": False, "name": "Konnected 445566 Zone 1", "state": None, "type": "door", }, - "2": {"inverse": True, "name": "winder", "state": None, "type": "window"}, + "2": { + "entity_id": "binary_sensor.winder", + "inverse": True, + "name": "winder", + "state": None, + "type": "window", + }, "3": { + "entity_id": "binary_sensor.konnected_445566_zone_3", "inverse": False, "name": "Konnected 445566 Zone 3", "state": None, @@ -170,14 +177,16 @@ async def test_create_and_setup(hass, mock_panel): }, }, "blink": True, - "panel": device, "discovery": True, "host": "1.2.3.4", + "panel": device, "port": 1234, "sensors": [ { + "humidity": "sensor.konnected_445566_sensor_4_humidity", "name": "Konnected 445566 Sensor 4", "poll_interval": 3, + "temperature": "sensor.konnected_445566_sensor_4_temperature", "type": "dht", "zone": "4", }, @@ -186,6 +195,7 @@ async def test_create_and_setup(hass, mock_panel): "switches": [ { "activation": "low", + "entity_id": "switch.switcher", "momentary": 50, "name": "switcher", "pause": 100, @@ -195,6 +205,7 @@ async def test_create_and_setup(hass, mock_panel): }, { "activation": "high", + "entity_id": "switch.konnected_445566_actuator_6", "momentary": None, "name": "Konnected 445566 Actuator 6", "pause": None, @@ -288,12 +299,10 @@ async def test_create_and_setup_pro(hass, mock_panel): await device.update_switch("2", 1) # confirm the correct api is used - # pylint: disable=no-member assert mock_panel.put_device.call_count == 0 assert mock_panel.put_zone.call_count == 1 # confirm the settings are sent to the panel - # pylint: disable=no-member assert mock_panel.put_settings.call_args_list[0][1] == { "sensors": [{"zone": "2"}, {"zone": "6"}, {"zone": "10"}, {"zone": "11"}], "actuators": [ @@ -311,37 +320,49 @@ async def test_create_and_setup_pro(hass, mock_panel): } # confirm the device settings are saved in hass.data + # hass.data should not be accessed in tests as its considered integration internals assert device.stored_configuration == { "binary_sensors": { - "11": { - "inverse": False, - "name": "Konnected 445566 Zone 11", - "state": None, - "type": "window", - }, "10": { + "entity_id": "binary_sensor.konnected_445566_zone_10", "inverse": False, "name": "Konnected 445566 Zone 10", "state": None, "type": "door", }, + "11": { + "entity_id": "binary_sensor.konnected_445566_zone_11", + "inverse": False, + "name": "Konnected 445566 Zone 11", + "state": None, + "type": "window", + }, "2": { + "entity_id": "binary_sensor.konnected_445566_zone_2", "inverse": False, "name": "Konnected 445566 Zone 2", "state": None, "type": "door", }, - "6": {"inverse": True, "name": "winder", "state": None, "type": "window"}, + "6": { + "entity_id": "binary_sensor.winder", + "inverse": True, + "name": "winder", + "state": None, + "type": "window", + }, }, "blink": True, - "panel": device, "discovery": True, "host": "1.2.3.4", + "panel": device, "port": 1234, "sensors": [ { + "humidity": "sensor.konnected_445566_sensor_3_humidity", "name": "Konnected 445566 Sensor 3", "poll_interval": 5, + "temperature": "sensor.konnected_445566_sensor_3_temperature", "type": "dht", "zone": "3", }, @@ -350,6 +371,7 @@ async def test_create_and_setup_pro(hass, mock_panel): "switches": [ { "activation": "high", + "entity_id": "switch.konnected_445566_actuator_4", "momentary": None, "name": "Konnected 445566 Actuator 4", "pause": None, @@ -359,6 +381,7 @@ async def test_create_and_setup_pro(hass, mock_panel): }, { "activation": "low", + "entity_id": "switch.switcher", "momentary": 50, "name": "switcher", "pause": 100, @@ -368,6 +391,7 @@ async def test_create_and_setup_pro(hass, mock_panel): }, { "activation": "high", + "entity_id": "switch.konnected_445566_actuator_out1", "momentary": None, "name": "Konnected 445566 Actuator out1", "pause": None, @@ -377,6 +401,7 @@ async def test_create_and_setup_pro(hass, mock_panel): }, { "activation": "high", + "entity_id": "switch.konnected_445566_actuator_alarm1", "momentary": None, "name": "Konnected 445566 Actuator alarm1", "pause": None, @@ -483,12 +508,10 @@ async def test_default_options(hass, mock_panel): await device.update_switch("1", 0) # confirm the correct api is used - # pylint: disable=no-member assert mock_panel.put_device.call_count == 1 assert mock_panel.put_zone.call_count == 0 # confirm the settings are sent to the panel - # pylint: disable=no-member assert mock_panel.put_settings.call_args_list[0][1] == { "sensors": [{"pin": "1"}, {"pin": "2"}, {"pin": "5"}], "actuators": [{"trigger": 0, "pin": "8"}, {"trigger": 1, "pin": "9"}], @@ -501,16 +524,25 @@ async def test_default_options(hass, mock_panel): } # confirm the device settings are saved in hass.data + # This test should not access hass.data since its integration internals assert device.stored_configuration == { "binary_sensors": { "1": { + "entity_id": "binary_sensor.konnected_445566_zone_1", "inverse": False, "name": "Konnected 445566 Zone 1", "state": None, "type": "door", }, - "2": {"inverse": True, "name": "winder", "state": None, "type": "window"}, + "2": { + "entity_id": "binary_sensor.winder", + "inverse": True, + "name": "winder", + "state": None, + "type": "window", + }, "3": { + "entity_id": "binary_sensor.konnected_445566_zone_3", "inverse": False, "name": "Konnected 445566 Zone 3", "state": None, @@ -518,14 +550,16 @@ async def test_default_options(hass, mock_panel): }, }, "blink": True, - "panel": device, "discovery": True, "host": "1.2.3.4", + "panel": device, "port": 1234, "sensors": [ { + "humidity": "sensor.konnected_445566_sensor_4_humidity", "name": "Konnected 445566 Sensor 4", "poll_interval": 3, + "temperature": "sensor.konnected_445566_sensor_4_temperature", "type": "dht", "zone": "4", }, @@ -534,6 +568,7 @@ async def test_default_options(hass, mock_panel): "switches": [ { "activation": "low", + "entity_id": "switch.switcher", "momentary": 50, "name": "switcher", "pause": 100, @@ -543,6 +578,7 @@ async def test_default_options(hass, mock_panel): }, { "activation": "high", + "entity_id": "switch.konnected_445566_actuator_6", "momentary": None, "name": "Konnected 445566 Actuator 6", "pause": None, diff --git a/tests/components/kostal_plenticore/conftest.py b/tests/components/kostal_plenticore/conftest.py index 4e789a34198..f0e7752d7c0 100644 --- a/tests/components/kostal_plenticore/conftest.py +++ b/tests/components/kostal_plenticore/conftest.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch -from kostal.plenticore import MeData, VersionData +from pykoplenti import MeData, VersionData import pytest from homeassistant.components.kostal_plenticore.helper import Plenticore diff --git a/tests/components/kostal_plenticore/test_config_flow.py b/tests/components/kostal_plenticore/test_config_flow.py index 17ff8ef3e03..dc7f9014b06 100644 --- a/tests/components/kostal_plenticore/test_config_flow.py +++ b/tests/components/kostal_plenticore/test_config_flow.py @@ -2,7 +2,7 @@ import asyncio from unittest.mock import ANY, AsyncMock, MagicMock, patch -from kostal.plenticore import PlenticoreAuthenticationException +from pykoplenti import AuthenticationException from homeassistant import config_entries from homeassistant.components.kostal_plenticore.const import DOMAIN @@ -20,7 +20,7 @@ async def test_formx(hass): assert result["errors"] == {} with patch( - "homeassistant.components.kostal_plenticore.config_flow.PlenticoreApiClient" + "homeassistant.components.kostal_plenticore.config_flow.ApiClient" ) as mock_api_class, patch( "homeassistant.components.kostal_plenticore.async_setup_entry", return_value=True, @@ -32,7 +32,7 @@ async def test_formx(hass): return_value={"scb:network": {"Hostname": "scb"}} ) - # mock of the return instance of PlenticoreApiClient + # mock of the return instance of ApiClient mock_api = MagicMock() mock_api.__aenter__.return_value = mock_api_ctx mock_api.__aexit__ = AsyncMock() @@ -70,15 +70,15 @@ async def test_form_invalid_auth(hass): ) with patch( - "homeassistant.components.kostal_plenticore.config_flow.PlenticoreApiClient" + "homeassistant.components.kostal_plenticore.config_flow.ApiClient" ) as mock_api_class: # mock of the context manager instance mock_api_ctx = MagicMock() mock_api_ctx.login = AsyncMock( - side_effect=PlenticoreAuthenticationException(404, "invalid user"), + side_effect=AuthenticationException(404, "invalid user"), ) - # mock of the return instance of PlenticoreApiClient + # mock of the return instance of ApiClient mock_api = MagicMock() mock_api.__aenter__.return_value = mock_api_ctx mock_api.__aexit__.return_value = None @@ -104,7 +104,7 @@ async def test_form_cannot_connect(hass): ) with patch( - "homeassistant.components.kostal_plenticore.config_flow.PlenticoreApiClient" + "homeassistant.components.kostal_plenticore.config_flow.ApiClient" ) as mock_api_class: # mock of the context manager instance mock_api_ctx = MagicMock() @@ -112,7 +112,7 @@ async def test_form_cannot_connect(hass): side_effect=asyncio.TimeoutError(), ) - # mock of the return instance of PlenticoreApiClient + # mock of the return instance of ApiClient mock_api = MagicMock() mock_api.__aenter__.return_value = mock_api_ctx mock_api.__aexit__.return_value = None @@ -138,7 +138,7 @@ async def test_form_unexpected_error(hass): ) with patch( - "homeassistant.components.kostal_plenticore.config_flow.PlenticoreApiClient" + "homeassistant.components.kostal_plenticore.config_flow.ApiClient" ) as mock_api_class: # mock of the context manager instance mock_api_ctx = MagicMock() @@ -146,7 +146,7 @@ async def test_form_unexpected_error(hass): side_effect=Exception(), ) - # mock of the return instance of PlenticoreApiClient + # mock of the return instance of ApiClient mock_api = MagicMock() mock_api.__aenter__.return_value = mock_api_ctx mock_api.__aexit__.return_value = None diff --git a/tests/components/kostal_plenticore/test_diagnostics.py b/tests/components/kostal_plenticore/test_diagnostics.py index 1f249aa3798..14a25ff6c60 100644 --- a/tests/components/kostal_plenticore/test_diagnostics.py +++ b/tests/components/kostal_plenticore/test_diagnostics.py @@ -1,6 +1,6 @@ """Test Kostal Plenticore diagnostics.""" from aiohttp import ClientSession -from kostal.plenticore import SettingsData +from pykoplenti import SettingsData from homeassistant.components.diagnostics import REDACTED from homeassistant.components.kostal_plenticore.helper import Plenticore @@ -57,7 +57,7 @@ async def test_entry_diagnostics( }, "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)", + "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": [ diff --git a/tests/components/kostal_plenticore/test_number.py b/tests/components/kostal_plenticore/test_number.py index a27b880b5d7..baa6aa8c34e 100644 --- a/tests/components/kostal_plenticore/test_number.py +++ b/tests/components/kostal_plenticore/test_number.py @@ -4,7 +4,7 @@ from collections.abc import Generator from datetime import timedelta from unittest.mock import patch -from kostal.plenticore import PlenticoreApiClient, SettingsData +from pykoplenti import ApiClient, SettingsData import pytest from homeassistant.components.number import ( @@ -23,17 +23,17 @@ from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture -def mock_plenticore_client() -> Generator[PlenticoreApiClient, None, None]: - """Return a patched PlenticoreApiClient.""" +def mock_plenticore_client() -> Generator[ApiClient, None, None]: + """Return a patched ApiClient.""" with patch( - "homeassistant.components.kostal_plenticore.helper.PlenticoreApiClient", + "homeassistant.components.kostal_plenticore.helper.ApiClient", autospec=True, ) as plenticore_client_class: yield plenticore_client_class.return_value @pytest.fixture -def mock_get_setting_values(mock_plenticore_client: PlenticoreApiClient) -> list: +def mock_get_setting_values(mock_plenticore_client: ApiClient) -> list: """Add a setting value to the given Plenticore client. Returns a list with setting values which can be extended by test cases. @@ -88,7 +88,7 @@ def mock_get_setting_values(mock_plenticore_client: PlenticoreApiClient) -> list async def test_setup_all_entries( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_plenticore_client: PlenticoreApiClient, + mock_plenticore_client: ApiClient, mock_get_setting_values: list, entity_registry_enabled_by_default, ): @@ -107,7 +107,7 @@ async def test_setup_all_entries( async def test_setup_no_entries( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_plenticore_client: PlenticoreApiClient, + mock_plenticore_client: ApiClient, mock_get_setting_values: list, entity_registry_enabled_by_default, ): @@ -128,7 +128,7 @@ async def test_setup_no_entries( async def test_number_has_value( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_plenticore_client: PlenticoreApiClient, + mock_plenticore_client: ApiClient, mock_get_setting_values: list, entity_registry_enabled_by_default, ): @@ -153,7 +153,7 @@ async def test_number_has_value( async def test_number_is_unavailable( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_plenticore_client: PlenticoreApiClient, + mock_plenticore_client: ApiClient, mock_get_setting_values: list, entity_registry_enabled_by_default, ): @@ -174,7 +174,7 @@ async def test_number_is_unavailable( async def test_set_value( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - mock_plenticore_client: PlenticoreApiClient, + mock_plenticore_client: ApiClient, mock_get_setting_values: list, entity_registry_enabled_by_default, ): diff --git a/tests/components/kostal_plenticore/test_select.py b/tests/components/kostal_plenticore/test_select.py index 6023b015483..b892c0a457a 100644 --- a/tests/components/kostal_plenticore/test_select.py +++ b/tests/components/kostal_plenticore/test_select.py @@ -1,5 +1,5 @@ """Test the Kostal Plenticore Solar Inverter select platform.""" -from kostal.plenticore import SettingsData +from pykoplenti import SettingsData from homeassistant.components.kostal_plenticore.helper import Plenticore from homeassistant.core import HomeAssistant diff --git a/tests/components/lacrosse_view/__init__.py b/tests/components/lacrosse_view/__init__.py index bd4ccb17b17..66789508f05 100644 --- a/tests/components/lacrosse_view/__init__.py +++ b/tests/components/lacrosse_view/__init__.py @@ -41,3 +41,69 @@ TEST_UNSUPPORTED_SENSOR = Sensor( permissions={"read": True}, model="Test", ) +TEST_FLOAT_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["Temperature"], + location=Location(id="1", name="Test"), + data={"Temperature": {"values": [{"s": "2.3"}], "unit": "degrees_celsius"}}, + permissions={"read": True}, + model="Test", +) +TEST_STRING_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["WetDry"], + location=Location(id="1", name="Test"), + data={"WetDry": {"values": [{"s": "dry"}], "unit": "wet_dry"}}, + permissions={"read": True}, + model="Test", +) +TEST_ALREADY_FLOAT_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["HeatIndex"], + location=Location(id="1", name="Test"), + data={"HeatIndex": {"values": [{"s": 2.3}], "unit": "degrees_celsius"}}, + permissions={"read": True}, + model="Test", +) +TEST_ALREADY_INT_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["WindSpeed"], + location=Location(id="1", name="Test"), + data={"WindSpeed": {"values": [{"s": 2}], "unit": "degrees_celsius"}}, + permissions={"read": True}, + model="Test", +) +TEST_NO_FIELD_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["Temperature"], + location=Location(id="1", name="Test"), + data={}, + permissions={"read": True}, + model="Test", +) +TEST_MISSING_FIELD_DATA_SENSOR = Sensor( + name="Test", + device_id="1", + type="Test", + sensor_id="2", + sensor_field_names=["Temperature"], + location=Location(id="1", name="Test"), + data={"Temperature": None}, + permissions={"read": True}, + model="Test", +) diff --git a/tests/components/lacrosse_view/test_sensor.py b/tests/components/lacrosse_view/test_sensor.py index 0e102c2f3ef..b39ce7acc86 100644 --- a/tests/components/lacrosse_view/test_sensor.py +++ b/tests/components/lacrosse_view/test_sensor.py @@ -1,14 +1,24 @@ """Test the LaCrosse View sensors.""" +from typing import Any from unittest.mock import patch +from lacrosse_view import Sensor +import pytest + from homeassistant.components.lacrosse_view import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from . import ( MOCK_ENTRY_DATA, + TEST_ALREADY_FLOAT_SENSOR, + TEST_ALREADY_INT_SENSOR, + TEST_FLOAT_SENSOR, + TEST_MISSING_FIELD_DATA_SENSOR, + TEST_NO_FIELD_SENSOR, TEST_NO_PERMISSION_SENSOR, TEST_SENSOR, + TEST_STRING_SENSOR, TEST_UNSUPPORTED_SENSOR, ) @@ -71,3 +81,74 @@ async def test_field_not_supported(hass: HomeAssistant, caplog) -> None: assert entries[0].state == ConfigEntryState.LOADED assert hass.states.get("sensor.test_some_unsupported_field") is None assert "Unsupported sensor field" in caplog.text + + +@pytest.mark.parametrize( + "test_input,expected,entity_id", + [ + (TEST_FLOAT_SENSOR, "2.3", "temperature"), + (TEST_STRING_SENSOR, "dry", "wet_dry"), + (TEST_ALREADY_FLOAT_SENSOR, "-16.5", "heat_index"), + (TEST_ALREADY_INT_SENSOR, "2", "wind_speed"), + ], +) +async def test_field_types( + hass: HomeAssistant, test_input: Sensor, expected: Any, entity_id: str +) -> None: + """Test the different data types for fields.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[test_input], + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + assert hass.states.get(f"sensor.test_{entity_id}").state == expected + + +async def test_no_field(hass: HomeAssistant, caplog: Any) -> None: + """Test behavior when the expected field is not present.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_NO_FIELD_SENSOR], + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + assert hass.states.get("sensor.test_temperature").state == "unavailable" + + +async def test_field_data_missing(hass: HomeAssistant) -> None: + """Test behavior when field data is missing.""" + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY_DATA) + config_entry.add_to_hass(hass) + + with patch("lacrosse_view.LaCrosse.login", return_value=True), patch( + "lacrosse_view.LaCrosse.get_sensors", + return_value=[TEST_MISSING_FIELD_DATA_SENSOR], + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN] + entries = hass.config_entries.async_entries(DOMAIN) + assert entries + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + assert hass.states.get("sensor.test_temperature").state == "unknown" diff --git a/tests/components/lametric/test_config_flow.py b/tests/components/lametric/test_config_flow.py index a23b50c9813..181c25955f1 100644 --- a/tests/components/lametric/test_config_flow.py +++ b/tests/components/lametric/test_config_flow.py @@ -60,14 +60,12 @@ async def test_full_cloud_import_flow_multiple_devices( assert result.get("type") == FlowResultType.MENU assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" assert result.get("menu_options") == ["pick_implementation", "manual_entry"] - assert "flow_id" in result flow_id = result["flow_id"] result2 = await hass.config_entries.flow.async_configure( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -142,14 +140,12 @@ async def test_full_cloud_import_flow_single_device( assert result.get("type") == FlowResultType.MENU assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" assert result.get("menu_options") == ["pick_implementation", "manual_entry"] - assert "flow_id" in result flow_id = result["flow_id"] result2 = await hass.config_entries.flow.async_configure( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -218,7 +214,6 @@ async def test_full_manual( assert result.get("type") == FlowResultType.MENU assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" assert result.get("menu_options") == ["pick_implementation", "manual_entry"] - assert "flow_id" in result flow_id = result["flow_id"] result2 = await hass.config_entries.flow.async_configure( @@ -264,14 +259,12 @@ async def test_full_ssdp_with_cloud_import( assert result.get("type") == FlowResultType.MENU assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" assert result.get("menu_options") == ["pick_implementation", "manual_entry"] - assert "flow_id" in result flow_id = result["flow_id"] result2 = await hass.config_entries.flow.async_configure( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -335,7 +328,6 @@ async def test_full_ssdp_manual_entry( assert result.get("type") == FlowResultType.MENU assert result.get("step_id") == "choice_enter_manual_or_fetch_cloud" assert result.get("menu_options") == ["pick_implementation", "manual_entry"] - assert "flow_id" in result flow_id = result["flow_id"] result2 = await hass.config_entries.flow.async_configure( @@ -410,14 +402,12 @@ async def test_cloud_import_updates_existing_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert "flow_id" in result flow_id = result["flow_id"] await hass.config_entries.flow.async_configure( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -466,7 +456,6 @@ async def test_manual_updates_existing_entry( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert "flow_id" in result flow_id = result["flow_id"] await hass.config_entries.flow.async_configure( @@ -519,14 +508,12 @@ async def test_cloud_abort_no_devices( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert "flow_id" in result flow_id = result["flow_id"] await hass.config_entries.flow.async_configure( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -576,7 +563,6 @@ async def test_manual_errors( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert "flow_id" in result flow_id = result["flow_id"] await hass.config_entries.flow.async_configure( @@ -640,14 +626,12 @@ async def test_cloud_errors( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert "flow_id" in result flow_id = result["flow_id"] await hass.config_entries.flow.async_configure( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -773,14 +757,12 @@ async def test_reauth_cloud_import( data=mock_config_entry.data, ) - assert "flow_id" in result flow_id = result["flow_id"] await hass.config_entries.flow.async_configure( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -840,14 +822,12 @@ async def test_reauth_cloud_abort_device_not_found( data=mock_config_entry.data, ) - assert "flow_id" in result flow_id = result["flow_id"] await hass.config_entries.flow.async_configure( flow_id, user_input={"next_step_id": "pick_implementation"} ) - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -897,7 +877,6 @@ async def test_reauth_manual( data=mock_config_entry.data, ) - assert "flow_id" in result flow_id = result["flow_id"] await hass.config_entries.flow.async_configure( diff --git a/tests/components/lametric/test_notify.py b/tests/components/lametric/test_notify.py index 3b581c81e75..7d43e7ba9b0 100644 --- a/tests/components/lametric/test_notify.py +++ b/tests/components/lametric/test_notify.py @@ -35,7 +35,9 @@ async def test_notification_defaults( NOTIFY_DOMAIN, NOTIFY_SERVICE, { - ATTR_MESSAGE: "Try not to become a man of success. Rather become a man of value", + ATTR_MESSAGE: ( + "Try not to become a man of success. Rather become a man of value" + ), }, blocking=True, ) @@ -118,7 +120,7 @@ async def test_notification_error( NOTIFY_DOMAIN, NOTIFY_SERVICE, { - ATTR_MESSAGE: "It's failure that gives you the proper perspective on success", + ATTR_MESSAGE: "It's failure that gives you the proper perspective", }, blocking=True, ) diff --git a/tests/components/landisgyr_heat_meter/test_sensor.py b/tests/components/landisgyr_heat_meter/test_sensor.py index cbaca71e52f..6721ae00a23 100644 --- a/tests/components/landisgyr_heat_meter/test_sensor.py +++ b/tests/components/landisgyr_heat_meter/test_sensor.py @@ -19,8 +19,8 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - ENERGY_MEGA_WATT_HOUR, - VOLUME_CUBIC_METERS, + UnitOfEnergy, + UnitOfVolume, ) from homeassistant.core import CoreState, State from homeassistant.helpers import entity_registry @@ -82,14 +82,14 @@ async def test_create_sensors(mock_heat_meter, hass): state = hass.states.get("sensor.heat_meter_heat_usage") assert state assert state.state == "34.16669" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_MEGA_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.MEGA_WATT_HOUR assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY state = hass.states.get("sensor.heat_meter_volume_usage") assert state assert state.state == "456" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.CUBIC_METERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL state = hass.states.get("sensor.heat_meter_device_number") @@ -122,13 +122,13 @@ async def test_restore_state(mock_heat_meter, hass): "34167", attributes={ ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: ENERGY_MEGA_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.MEGA_WATT_HOUR, ATTR_STATE_CLASS: SensorStateClass.TOTAL, }, ), { "native_value": 34167, - "native_unit_of_measurement": ENERGY_MEGA_WATT_HOUR, + "native_unit_of_measurement": UnitOfEnergy.MEGA_WATT_HOUR, "icon": "mdi:fire", "last_reset": last_reset, }, @@ -139,13 +139,13 @@ async def test_restore_state(mock_heat_meter, hass): "456", attributes={ ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: VOLUME_CUBIC_METERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfVolume.CUBIC_METERS, ATTR_STATE_CLASS: SensorStateClass.TOTAL, }, ), { "native_value": 456, - "native_unit_of_measurement": VOLUME_CUBIC_METERS, + "native_unit_of_measurement": UnitOfVolume.CUBIC_METERS, "icon": "mdi:fire", "last_reset": last_reset, }, @@ -183,13 +183,13 @@ async def test_restore_state(mock_heat_meter, hass): state = hass.states.get("sensor.heat_meter_heat_usage") assert state assert state.state == "34167" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_MEGA_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.MEGA_WATT_HOUR assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL state = hass.states.get("sensor.heat_meter_volume_usage") assert state assert state.state == "456" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_CUBIC_METERS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.CUBIC_METERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL state = hass.states.get("sensor.heat_meter_device_number") diff --git a/tests/components/lcn/test_sensor.py b/tests/components/lcn/test_sensor.py index 4b6c0beb7e2..b9bb91cbb9a 100644 --- a/tests/components/lcn/test_sensor.py +++ b/tests/components/lcn/test_sensor.py @@ -8,7 +8,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.helpers import entity_registry as er @@ -35,11 +35,11 @@ async def test_entity_state(hass, lcn_connection): """Test state of entity.""" state = hass.states.get(SENSOR_VAR1) assert state - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS state = hass.states.get(SENSOR_SETPOINT1) assert state - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS state = hass.states.get(SENSOR_LED6) assert state diff --git a/tests/components/ld2410_ble/__init__.py b/tests/components/ld2410_ble/__init__.py new file mode 100644 index 00000000000..2abb955793d --- /dev/null +++ b/tests/components/ld2410_ble/__init__.py @@ -0,0 +1,37 @@ +"""Tests for the LD2410 BLE Bluetooth integration.""" +from bleak.backends.device import BLEDevice + +from homeassistant.components.bluetooth import BluetoothServiceInfoBleak + +from tests.components.bluetooth import generate_advertisement_data + +LD2410_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak( + name="HLK-LD2410B_EEFF", + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + manufacturer_data={}, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="HLK-LD2410B_EEFF"), + advertisement=generate_advertisement_data(), + time=0, + connectable=True, +) + +NOT_LD2410_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak( + name="Not", + address="AA:BB:CC:DD:EE:FF", + rssi=-60, + manufacturer_data={ + 33: b"\x00\x00\xd1\xf0b;\xd8\x1dE\xd6\xba\xeeL\xdd]\xf5\xb2\xe9", + 21: b"\x061\x00Z\x8f\x93\xb2\xec\x85\x06\x00i\x00\x02\x02Q\xed\x1d\xf0", + }, + service_uuids=[], + service_data={}, + source="local", + device=BLEDevice(address="AA:BB:CC:DD:EE:FF", name="Aug"), + advertisement=generate_advertisement_data(), + time=0, + connectable=True, +) diff --git a/tests/components/ld2410_ble/conftest.py b/tests/components/ld2410_ble/conftest.py new file mode 100644 index 00000000000..58dca37ce83 --- /dev/null +++ b/tests/components/ld2410_ble/conftest.py @@ -0,0 +1,8 @@ +"""ld2410_ble session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/ld2410_ble/test_config_flow.py b/tests/components/ld2410_ble/test_config_flow.py new file mode 100644 index 00000000000..dab7c4bd5d9 --- /dev/null +++ b/tests/components/ld2410_ble/test_config_flow.py @@ -0,0 +1,222 @@ +"""Test the LD2410 BLE Bluetooth config flow.""" +from unittest.mock import patch + +from bleak import BleakError + +from homeassistant import config_entries +from homeassistant.components.ld2410_ble.const import DOMAIN +from homeassistant.const import CONF_ADDRESS +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from . import LD2410_BLE_DISCOVERY_INFO, NOT_LD2410_BLE_DISCOVERY_INFO + +from tests.common import MockConfigEntry + + +async def test_user_step_success(hass: HomeAssistant) -> None: + """Test user step success path.""" + with patch( + "homeassistant.components.ld2410_ble.config_flow.async_discovered_service_info", + return_value=[NOT_LD2410_BLE_DISCOVERY_INFO, LD2410_BLE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", + ), patch( + "homeassistant.components.ld2410_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == LD2410_BLE_DISCOVERY_INFO.name + assert result2["data"] == { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + } + assert result2["result"].unique_id == LD2410_BLE_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_step_no_devices_found(hass: HomeAssistant) -> None: + """Test user step with no devices found.""" + with patch( + "homeassistant.components.ld2410_ble.config_flow.async_discovered_service_info", + return_value=[NOT_LD2410_BLE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_unconfigured_devices" + + +async def test_user_step_no_new_devices_found(hass: HomeAssistant) -> None: + """Test user step with only existing devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + }, + unique_id=LD2410_BLE_DISCOVERY_INFO.address, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.ld2410_ble.config_flow.async_discovered_service_info", + return_value=[LD2410_BLE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_unconfigured_devices" + + +async def test_user_step_cannot_connect(hass: HomeAssistant) -> None: + """Test user step and we cannot connect.""" + with patch( + "homeassistant.components.ld2410_ble.config_flow.async_discovered_service_info", + return_value=[LD2410_BLE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", + side_effect=BleakError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + with patch( + "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", + ), patch( + "homeassistant.components.ld2410_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == LD2410_BLE_DISCOVERY_INFO.name + assert result3["data"] == { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + } + assert result3["result"].unique_id == LD2410_BLE_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_step_unknown_exception(hass: HomeAssistant) -> None: + """Test user step with an unknown exception.""" + with patch( + "homeassistant.components.ld2410_ble.config_flow.async_discovered_service_info", + return_value=[NOT_LD2410_BLE_DISCOVERY_INFO, LD2410_BLE_DISCOVERY_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", + side_effect=RuntimeError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "unknown"} + + with patch( + "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", + ), patch( + "homeassistant.components.ld2410_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result3["type"] == FlowResultType.CREATE_ENTRY + assert result3["title"] == LD2410_BLE_DISCOVERY_INFO.name + assert result3["data"] == { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + } + assert result3["result"].unique_id == LD2410_BLE_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_bluetooth_step_success(hass: HomeAssistant) -> None: + """Test bluetooth step success path.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=LD2410_BLE_DISCOVERY_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.ld2410_ble.config_flow.LD2410BLE.initialise", + ), patch( + "homeassistant.components.ld2410_ble.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == LD2410_BLE_DISCOVERY_INFO.name + assert result2["data"] == { + CONF_ADDRESS: LD2410_BLE_DISCOVERY_INFO.address, + } + assert result2["result"].unique_id == LD2410_BLE_DISCOVERY_INFO.address + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/light/test_intent.py b/tests/components/light/test_intent.py index 0c837a49c42..57f90bb297c 100644 --- a/tests/components/light/test_intent.py +++ b/tests/components/light/test_intent.py @@ -2,7 +2,7 @@ from homeassistant.components import light from homeassistant.components.light import ATTR_SUPPORTED_COLOR_MODES, ColorMode, intent from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON -from homeassistant.helpers.intent import IntentHandleError, async_handle +from homeassistant.helpers.intent import async_handle from tests.common import async_mock_service @@ -16,16 +16,14 @@ async def test_intent_set_color(hass): calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) await intent.async_setup_intents(hass) - result = await async_handle( + await async_handle( hass, "test", intent.INTENT_SET, - {"name": {"value": "Hello"}, "color": {"value": "blue"}}, + {"name": {"value": "Hello 2"}, "color": {"value": "blue"}}, ) await hass.async_block_till_done() - assert result.speech["plain"]["speech"] == "Changed hello 2 to the color blue" - assert len(calls) == 1 call = calls[0] assert call.domain == light.DOMAIN @@ -40,17 +38,16 @@ async def test_intent_set_color_tests_feature(hass): calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) await intent.async_setup_intents(hass) - try: - await async_handle( - hass, - "test", - intent.INTENT_SET, - {"name": {"value": "Hello"}, "color": {"value": "blue"}}, - ) - assert False, "handling intent should have raised" - except IntentHandleError as err: - assert str(err) == "Entity hello does not support changing colors" + response = await async_handle( + hass, + "test", + intent.INTENT_SET, + {"name": {"value": "Hello"}, "color": {"value": "blue"}}, + ) + # Response should contain one failed target + assert len(response.success_results) == 0 + assert len(response.failed_results) == 1 assert len(calls) == 0 @@ -63,23 +60,18 @@ async def test_intent_set_color_and_brightness(hass): calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) await intent.async_setup_intents(hass) - result = await async_handle( + await async_handle( hass, "test", intent.INTENT_SET, { - "name": {"value": "Hello"}, + "name": {"value": "Hello 2"}, "color": {"value": "blue"}, "brightness": {"value": "20"}, }, ) await hass.async_block_till_done() - assert ( - result.speech["plain"]["speech"] - == "Changed hello 2 to the color blue and 20% brightness" - ) - assert len(calls) == 1 call = calls[0] assert call.domain == light.DOMAIN diff --git a/tests/components/litejet/conftest.py b/tests/components/litejet/conftest.py index 00b1eb92190..4484c198d3c 100644 --- a/tests/components/litejet/conftest.py +++ b/tests/components/litejet/conftest.py @@ -1,6 +1,6 @@ """Fixtures for LiteJet testing.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest @@ -12,13 +12,13 @@ def mock_litejet(): """Mock LiteJet system.""" with patch("pylitejet.LiteJet") as mock_pylitejet: - def get_load_name(number): + async def get_load_name(number): return f"Mock Load #{number}" - def get_scene_name(number): + async def get_scene_name(number): return f"Mock Scene #{number}" - def get_switch_name(number): + async def get_switch_name(number): return f"Mock Switch #{number}" mock_lj = mock_pylitejet.return_value @@ -45,18 +45,29 @@ def mock_litejet(): mock_lj.on_load_activated.side_effect = on_load_activated mock_lj.on_load_deactivated.side_effect = on_load_deactivated + mock_lj.open = AsyncMock() + mock_lj.close = AsyncMock() + mock_lj.loads.return_value = range(1, 3) - mock_lj.get_load_name.side_effect = get_load_name - mock_lj.get_load_level.return_value = 0 + mock_lj.get_load_name = AsyncMock(side_effect=get_load_name) + mock_lj.get_load_level = AsyncMock(return_value=0) + mock_lj.activate_load = AsyncMock() + mock_lj.activate_load_at = AsyncMock() + mock_lj.deactivate_load = AsyncMock() mock_lj.button_switches.return_value = range(1, 3) mock_lj.all_switches.return_value = range(1, 6) - mock_lj.get_switch_name.side_effect = get_switch_name + mock_lj.get_switch_name = AsyncMock(side_effect=get_switch_name) + mock_lj.press_switch = AsyncMock() + mock_lj.release_switch = AsyncMock() mock_lj.scenes.return_value = range(1, 3) - mock_lj.get_scene_name.side_effect = get_scene_name + mock_lj.get_scene_name = AsyncMock(side_effect=get_scene_name) + mock_lj.activate_scene = AsyncMock() + mock_lj.deactivate_scene = AsyncMock() mock_lj.start_time = dt_util.utcnow() mock_lj.last_delta = timedelta(0) + mock_lj.connected = True yield mock_lj diff --git a/tests/components/litejet/test_diagnostics.py b/tests/components/litejet/test_diagnostics.py new file mode 100644 index 00000000000..188acc8711e --- /dev/null +++ b/tests/components/litejet/test_diagnostics.py @@ -0,0 +1,19 @@ +"""The tests for the litejet component.""" +from . import async_init_integration + +from tests.components.diagnostics import get_diagnostics_for_config_entry + + +async def test_diagnostics(hass, hass_client, mock_litejet): + """Test getting the LiteJet diagnostics.""" + + config_entry = await async_init_integration(hass) + + diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) + + assert diag == { + "loads": [1, 2], + "button_switches": [1, 2], + "scenes": [1, 2], + "connected": True, + } diff --git a/tests/components/litejet/test_light.py b/tests/components/litejet/test_light.py index 86b3dd84367..ca80df2b6dd 100644 --- a/tests/components/litejet/test_light.py +++ b/tests/components/litejet/test_light.py @@ -113,7 +113,7 @@ async def test_activated_event(hass, mock_litejet): # Light 1 mock_litejet.get_load_level.return_value = 99 mock_litejet.get_load_level.reset_mock() - mock_litejet.load_activated_callbacks[ENTITY_LIGHT_NUMBER]() + mock_litejet.load_activated_callbacks[ENTITY_LIGHT_NUMBER](99) await hass.async_block_till_done() mock_litejet.get_load_level.assert_called_once_with(ENTITY_LIGHT_NUMBER) @@ -128,7 +128,7 @@ async def test_activated_event(hass, mock_litejet): mock_litejet.get_load_level.return_value = 40 mock_litejet.get_load_level.reset_mock() - mock_litejet.load_activated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]() + mock_litejet.load_activated_callbacks[ENTITY_OTHER_LIGHT_NUMBER](40) await hass.async_block_till_done() mock_litejet.get_load_level.assert_called_once_with(ENTITY_OTHER_LIGHT_NUMBER) @@ -147,7 +147,7 @@ async def test_deactivated_event(hass, mock_litejet): # Initial state is on. mock_litejet.get_load_level.return_value = 99 - mock_litejet.load_activated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]() + mock_litejet.load_activated_callbacks[ENTITY_OTHER_LIGHT_NUMBER](99) await hass.async_block_till_done() assert light.is_on(hass, ENTITY_OTHER_LIGHT) @@ -157,7 +157,7 @@ async def test_deactivated_event(hass, mock_litejet): mock_litejet.get_load_level.reset_mock() mock_litejet.get_load_level.return_value = 0 - mock_litejet.load_deactivated_callbacks[ENTITY_OTHER_LIGHT_NUMBER]() + mock_litejet.load_deactivated_callbacks[ENTITY_OTHER_LIGHT_NUMBER](0) await hass.async_block_till_done() # (Requesting the level is not strictly needed with a deactivated diff --git a/tests/components/litterrobot/test_sensor.py b/tests/components/litterrobot/test_sensor.py index beed13c75dd..9586e7cdbfc 100644 --- a/tests/components/litterrobot/test_sensor.py +++ b/tests/components/litterrobot/test_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock import pytest from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN, SensorDeviceClass -from homeassistant.const import MASS_POUNDS, PERCENTAGE, STATE_UNKNOWN +from homeassistant.const import PERCENTAGE, STATE_UNKNOWN, UnitOfMass from homeassistant.core import HomeAssistant from .conftest import setup_integration @@ -92,7 +92,7 @@ async def test_litter_robot_sensor( assert sensor.attributes["unit_of_measurement"] == PERCENTAGE sensor = hass.states.get("sensor.test_pet_weight") assert sensor.state == "12.0" - assert sensor.attributes["unit_of_measurement"] == MASS_POUNDS + assert sensor.attributes["unit_of_measurement"] == UnitOfMass.POUNDS async def test_feeder_robot_sensor( diff --git a/tests/components/litterrobot/test_switch.py b/tests/components/litterrobot/test_switch.py index e7ca5747fb1..5ca40026484 100644 --- a/tests/components/litterrobot/test_switch.py +++ b/tests/components/litterrobot/test_switch.py @@ -62,9 +62,7 @@ async def test_on_off_commands( for service, new_state, new_value in services: count += 1 await hass.services.async_call(PLATFORM_DOMAIN, service, data, blocking=True) - robot._update_data( # pylint:disable=protected-access - {updated_field: new_value}, partial=True - ) + robot._update_data({updated_field: new_value}, partial=True) assert getattr(robot, robot_command).call_count == count assert (state := hass.states.get(entity_id)) diff --git a/tests/components/litterrobot/test_update.py b/tests/components/litterrobot/test_update.py index 4940ec64824..fd5bf1d181e 100644 --- a/tests/components/litterrobot/test_update.py +++ b/tests/components/litterrobot/test_update.py @@ -93,9 +93,7 @@ async def test_robot_with_update_already_in_progress( ): """Tests the update entity was set up.""" robot: LitterRobot4 = mock_account_with_litterrobot_4.robots[0] - robot._update_data( # pylint:disable=protected-access - {"isFirmwareUpdateTriggered": True}, partial=True - ) + robot._update_data({"isFirmwareUpdateTriggered": True}, partial=True) entry = await setup_integration( hass, mock_account_with_litterrobot_4, PLATFORM_DOMAIN diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index f288ebc4c87..95976604670 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -24,8 +24,7 @@ import homeassistant.helpers.entity_registry as er from .common import VACUUM_ENTITY_ID from .conftest import setup_integration -VACUUM_UNIQUE_ID_OLD = "LR3C012345-Litter Box" -VACUUM_UNIQUE_ID_NEW = "LR3C012345-litter_box" +VACUUM_UNIQUE_ID = "LR3C012345-litter_box" COMPONENT_SERVICE_DOMAIN = { SERVICE_SET_SLEEP_MODE: DOMAIN, @@ -36,15 +35,14 @@ async def test_vacuum(hass: HomeAssistant, mock_account: MagicMock) -> None: """Tests the vacuum entity was set up.""" ent_reg = er.async_get(hass) - # Create entity entry to migrate to new unique ID ent_reg.async_get_or_create( PLATFORM_DOMAIN, DOMAIN, - VACUUM_UNIQUE_ID_OLD, + VACUUM_UNIQUE_ID, suggested_object_id=VACUUM_ENTITY_ID.replace(PLATFORM_DOMAIN, ""), ) ent_reg_entry = ent_reg.async_get(VACUUM_ENTITY_ID) - assert ent_reg_entry.unique_id == VACUUM_UNIQUE_ID_OLD + assert ent_reg_entry.unique_id == VACUUM_UNIQUE_ID await setup_integration(hass, mock_account, PLATFORM_DOMAIN) assert len(ent_reg.entities) == 1 @@ -56,7 +54,7 @@ async def test_vacuum(hass: HomeAssistant, mock_account: MagicMock) -> None: assert vacuum.attributes["is_sleeping"] is False ent_reg_entry = ent_reg.async_get(VACUUM_ENTITY_ID) - assert ent_reg_entry.unique_id == VACUUM_UNIQUE_ID_NEW + assert ent_reg_entry.unique_id == VACUUM_UNIQUE_ID async def test_vacuum_status_when_sleeping( diff --git a/tests/components/local_calendar/test_calendar.py b/tests/components/local_calendar/test_calendar.py index 3cbbf6a19ad..21aa39a4d7a 100644 --- a/tests/components/local_calendar/test_calendar.py +++ b/tests/components/local_calendar/test_calendar.py @@ -960,3 +960,39 @@ async def test_update_invalid_event_id( assert not resp.get("success") assert "error" in resp assert resp.get("error").get("code") == "failed" + + +async def test_create_event_service( + hass: HomeAssistant, setup_integration: None, get_events: GetEventsFn +): + """Test creating an event using the create_event service.""" + + await hass.services.async_call( + "calendar", + "create_event", + { + "start_date_time": "1997-07-14T17:00:00+00:00", + "end_date_time": "1997-07-15T04:00:00+00:00", + "summary": "Bastille Day Party", + }, + target={"entity_id": TEST_ENTITY}, + blocking=True, + ) + + events = await get_events("1997-07-14T00:00:00Z", "1997-07-16T00:00:00Z") + assert list(map(event_fields, events)) == [ + { + "summary": "Bastille Day Party", + "start": {"dateTime": "1997-07-14T11:00:00-06:00"}, + "end": {"dateTime": "1997-07-14T22:00:00-06:00"}, + } + ] + + events = await get_events("1997-07-13T00:00:00Z", "1997-07-14T18:00:00Z") + assert list(map(event_fields, events)) == [ + { + "summary": "Bastille Day Party", + "start": {"dateTime": "1997-07-14T11:00:00-06:00"}, + "end": {"dateTime": "1997-07-14T22:00:00-06:00"}, + } + ] diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index a0af04145b0..4c9adc8cd69 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -12,8 +12,6 @@ from homeassistant.config import async_process_ha_core_config from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component -# pylint: disable=redefined-outer-name - @pytest.fixture(autouse=True) def mock_dev_track(mock_device_tracker_conf): diff --git a/tests/components/lock/test_init.py b/tests/components/lock/test_init.py new file mode 100644 index 00000000000..4943d63c6ed --- /dev/null +++ b/tests/components/lock/test_init.py @@ -0,0 +1,152 @@ +"""The tests for the lock component.""" +from __future__ import annotations + +from typing import Any +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.lock import ( + ATTR_CODE, + DOMAIN, + SERVICE_LOCK, + SERVICE_OPEN, + SERVICE_UNLOCK, + STATE_JAMMED, + STATE_LOCKED, + STATE_LOCKING, + STATE_UNLOCKED, + STATE_UNLOCKING, + LockEntity, + LockEntityFeature, + _async_lock, + _async_open, + _async_unlock, +) +from homeassistant.core import HomeAssistant, ServiceCall + + +class MockLockEntity(LockEntity): + """Mock lock to use in tests.""" + + def __init__( + self, + code_format: str | None = None, + supported_features: LockEntityFeature = LockEntityFeature(0), + ) -> None: + """Initialize mock lock entity.""" + self._attr_supported_features = supported_features + self.calls_open = MagicMock() + if code_format is not None: + self._attr_code_format = code_format + + async def async_lock(self, **kwargs: Any) -> None: + """Lock the lock.""" + self._attr_is_locking = False + self._attr_is_locked = True + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock the lock.""" + self._attr_is_unlocking = False + self._attr_is_locked = False + + async def async_open(self, **kwargs: Any) -> None: + """Open the door latch.""" + self.calls_open(kwargs) + + +async def test_lock_default(hass: HomeAssistant) -> None: + """Test lock entity with defaults.""" + lock = MockLockEntity() + lock.hass = hass + + assert lock.code_format is None + assert lock.state is None + + +async def test_lock_states(hass: HomeAssistant) -> None: + """Test lock entity states.""" + # pylint: disable=protected-access + + lock = MockLockEntity() + lock.hass = hass + + assert lock.state is None + + lock._attr_is_locking = True + assert lock.is_locking + assert lock.state == STATE_LOCKING + + await _async_lock(lock, ServiceCall(DOMAIN, SERVICE_LOCK, {})) + assert lock.is_locked + assert lock.state == STATE_LOCKED + + lock._attr_is_unlocking = True + assert lock.is_unlocking + assert lock.state == STATE_UNLOCKING + + await _async_unlock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {})) + assert not lock.is_locked + assert lock.state == STATE_UNLOCKED + + lock._attr_is_jammed = True + assert lock.is_jammed + assert lock.state == STATE_JAMMED + assert not lock.is_locked + + +async def test_lock_open_with_code(hass: HomeAssistant) -> None: + """Test lock entity with open service.""" + lock = MockLockEntity( + code_format=r"^\d{4}$", supported_features=LockEntityFeature.OPEN + ) + lock.hass = hass + + assert lock.state_attributes == {"code_format": r"^\d{4}$"} + + with pytest.raises(ValueError): + await _async_open(lock, ServiceCall(DOMAIN, SERVICE_OPEN, {})) + with pytest.raises(ValueError): + await _async_open(lock, ServiceCall(DOMAIN, SERVICE_OPEN, {ATTR_CODE: ""})) + with pytest.raises(ValueError): + await _async_open(lock, ServiceCall(DOMAIN, SERVICE_OPEN, {ATTR_CODE: "HELLO"})) + await _async_open(lock, ServiceCall(DOMAIN, SERVICE_OPEN, {ATTR_CODE: "1234"})) + assert lock.calls_open.call_count == 1 + + +async def test_lock_lock_with_code(hass: HomeAssistant) -> None: + """Test lock entity with open service.""" + lock = MockLockEntity(code_format=r"^\d{4}$") + lock.hass = hass + + await _async_unlock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {ATTR_CODE: "1234"})) + assert not lock.is_locked + + with pytest.raises(ValueError): + await _async_lock(lock, ServiceCall(DOMAIN, SERVICE_LOCK, {})) + with pytest.raises(ValueError): + await _async_lock(lock, ServiceCall(DOMAIN, SERVICE_LOCK, {ATTR_CODE: ""})) + with pytest.raises(ValueError): + await _async_lock(lock, ServiceCall(DOMAIN, SERVICE_LOCK, {ATTR_CODE: "HELLO"})) + await _async_lock(lock, ServiceCall(DOMAIN, SERVICE_LOCK, {ATTR_CODE: "1234"})) + assert lock.is_locked + + +async def test_lock_unlock_with_code(hass: HomeAssistant) -> None: + """Test unlock entity with open service.""" + lock = MockLockEntity(code_format=r"^\d{4}$") + lock.hass = hass + + await _async_lock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {ATTR_CODE: "1234"})) + assert lock.is_locked + + with pytest.raises(ValueError): + await _async_unlock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {})) + with pytest.raises(ValueError): + await _async_unlock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {ATTR_CODE: ""})) + with pytest.raises(ValueError): + await _async_unlock( + lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {ATTR_CODE: "HELLO"}) + ) + await _async_unlock(lock, ServiceCall(DOMAIN, SERVICE_UNLOCK, {ATTR_CODE: "1234"})) + assert not lock.is_locked diff --git a/tests/components/logbook/common.py b/tests/components/logbook/common.py index a41f983bfed..e6bce9e6fbc 100644 --- a/tests/components/logbook/common.py +++ b/tests/components/logbook/common.py @@ -27,6 +27,7 @@ class MockRow: self.shared_data = json.dumps(data, cls=JSONEncoder) self.data = data self.time_fired = dt_util.utcnow() + self.time_fired_ts = dt_util.utc_to_timestamp(self.time_fired) self.context_parent_id = context.parent_id if context else None self.context_user_id = context.user_id if context else None self.context_id = context.id if context else None diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 366b4b30ed5..d241ba74949 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -1,11 +1,11 @@ """The tests for the logbook component.""" -# pylint: disable=protected-access,invalid-name +# pylint: disable=invalid-name import asyncio import collections +from collections.abc import Callable from datetime import datetime, timedelta from http import HTTPStatus import json -from typing import Callable from unittest.mock import Mock, patch import pytest @@ -16,7 +16,7 @@ from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED from homeassistant.components.logbook.models import LazyEventPartialState from homeassistant.components.logbook.processor import EventProcessor -from homeassistant.components.logbook.queries.common import PSUEDO_EVENT_STATE_CHANGED +from homeassistant.components.logbook.queries.common import PSEUDO_EVENT_STATE_CHANGED from homeassistant.components.script import EVENT_SCRIPT_STARTED from homeassistant.components.sensor import SensorStateClass from homeassistant.const import ( @@ -313,16 +313,17 @@ def create_state_changed_event_from_old_new( row = collections.namedtuple( "Row", [ - "event_type" - "event_data" - "time_fired" - "context_id" - "context_user_id" - "context_parent_id" - "state" - "entity_id" - "domain" - "attributes" + "event_type", + "event_data", + "time_fired", + "time_fired_ts", + "context_id", + "context_user_id", + "context_parent_id", + "state", + "entity_id", + "domain", + "attributes", "state_id", "old_state_id", "shared_attrs", @@ -331,12 +332,13 @@ def create_state_changed_event_from_old_new( ], ) - row.event_type = PSUEDO_EVENT_STATE_CHANGED + row.event_type = PSEUDO_EVENT_STATE_CHANGED row.event_data = "{}" row.shared_data = "{}" row.attributes = attributes_json row.shared_attrs = attributes_json row.time_fired = event_time_fired + row.time_fired_ts = dt_util.utc_to_timestamp(event_time_fired) row.state = new_state and new_state.get("state") row.entity_id = entity_id row.domain = entity_id and ha.split_entity_id(entity_id)[0] diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index 91d1a95f75b..805213cf3bf 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -2303,6 +2303,11 @@ async def test_recorder_is_far_behind(recorder_mock, hass, hass_ws_client, caplo hass.bus.async_fire("mock_event", {"device_id": device.id, "message": "1"}) await hass.async_block_till_done() + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) + assert msg["id"] == 7 + assert msg["type"] == "event" + assert msg["event"]["events"] == [] + msg = await asyncio.wait_for(websocket_client.receive_json(), 2) assert msg["id"] == 7 assert msg["type"] == "event" diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index 2f1d69b8d47..b07c1ef2c3c 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -38,9 +38,7 @@ def init_config_flow(hass): sensors=None, ) flow = config_flow.LogiCircleFlowHandler() - flow._get_authorization_url = Mock( # pylint: disable=protected-access - return_value="http://example.com" - ) + flow._get_authorization_url = Mock(return_value="http://example.com") flow.hass = hass return flow @@ -59,9 +57,7 @@ def mock_logi_circle(): yield LogiCircle -async def test_step_import( - hass, mock_logi_circle # pylint: disable=redefined-outer-name -): +async def test_step_import(hass, mock_logi_circle): """Test that we trigger import when configuring with client.""" flow = init_config_flow(hass) @@ -70,9 +66,7 @@ async def test_step_import( assert result["step_id"] == "auth" -async def test_full_flow_implementation( - hass, mock_logi_circle # pylint: disable=redefined-outer-name -): +async def test_full_flow_implementation(hass, mock_logi_circle): """Test registering an implementation and finishing flow works.""" config_flow.register_flow_implementation( hass, @@ -153,9 +147,7 @@ async def test_abort_if_already_setup(hass): (AuthorizationFailed, "invalid_auth"), ], ) -async def test_abort_if_authorize_fails( - hass, mock_logi_circle, side_effect, error -): # pylint: disable=redefined-outer-name +async def test_abort_if_authorize_fails(hass, mock_logi_circle, side_effect, error): """Test we abort if authorizing fails.""" flow = init_config_flow(hass) mock_logi_circle.authorize.side_effect = side_effect @@ -177,9 +169,7 @@ async def test_not_pick_implementation_if_only_one(hass): assert result["step_id"] == "auth" -async def test_gen_auth_url( - hass, mock_logi_circle -): # pylint: disable=redefined-outer-name +async def test_gen_auth_url(hass, mock_logi_circle): """Test generating authorize URL from Logi Circle API.""" config_flow.register_flow_implementation( hass, @@ -195,7 +185,7 @@ async def test_gen_auth_url( flow.flow_impl = "test-auth-url" await async_setup_component(hass, "http", {}) - result = flow._get_authorization_url() # pylint: disable=protected-access + result = flow._get_authorization_url() assert result == "http://authorize.url" @@ -207,9 +197,7 @@ async def test_callback_view_rejects_missing_code(hass): assert resp.status == HTTPStatus.BAD_REQUEST -async def test_callback_view_accepts_code( - hass, mock_logi_circle -): # pylint: disable=redefined-outer-name +async def test_callback_view_accepts_code(hass, mock_logi_circle): """Test the auth callback view handles requests with auth code.""" init_config_flow(hass) view = LogiCircleAuthCallbackView() diff --git a/tests/components/luftdaten/test_config_flow.py b/tests/components/luftdaten/test_config_flow.py index 25d41c0c2d0..d98e415482d 100644 --- a/tests/components/luftdaten/test_config_flow.py +++ b/tests/components/luftdaten/test_config_flow.py @@ -25,7 +25,6 @@ async def test_duplicate_error( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -46,7 +45,6 @@ async def test_communication_error( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_luftdaten_config_flow.get_data.side_effect = LuftdatenConnectionError result2 = await hass.config_entries.flow.async_configure( @@ -57,7 +55,6 @@ async def test_communication_error( assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {CONF_SENSOR_ID: "cannot_connect"} - assert "flow_id" in result2 mock_luftdaten_config_flow.get_data.side_effect = None result3 = await hass.config_entries.flow.async_configure( @@ -83,7 +80,6 @@ async def test_invalid_sensor( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_luftdaten_config_flow.validate_sensor.return_value = False result2 = await hass.config_entries.flow.async_configure( @@ -94,7 +90,6 @@ async def test_invalid_sensor( assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {CONF_SENSOR_ID: "invalid_sensor"} - assert "flow_id" in result2 mock_luftdaten_config_flow.validate_sensor.return_value = True result3 = await hass.config_entries.flow.async_configure( @@ -122,7 +117,6 @@ async def test_step_user( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/luftdaten/test_sensor.py b/tests/components/luftdaten/test_sensor.py index f3d0f1c0b1f..e9e86fd9f1b 100644 --- a/tests/components/luftdaten/test_sensor.py +++ b/tests/components/luftdaten/test_sensor.py @@ -12,8 +12,8 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, PERCENTAGE, - PRESSURE_PA, - TEMP_CELSIUS, + UnitOfPressure, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -40,7 +40,7 @@ async def test_luftdaten_sensors( assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 Temperature" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert ATTR_ICON not in state.attributes entry = entity_registry.async_get("sensor.sensor_12345_humidity") @@ -68,7 +68,7 @@ async def test_luftdaten_sensors( assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 Pressure" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PA + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.PA assert ATTR_ICON not in state.attributes entry = entity_registry.async_get("sensor.sensor_12345_pressure_at_sealevel") @@ -84,7 +84,7 @@ async def test_luftdaten_sensors( ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_PA + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.PA assert ATTR_ICON not in state.attributes entry = entity_registry.async_get("sensor.sensor_12345_pm10") diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index 7faa63c2b1b..ddad56c0468 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -120,7 +120,6 @@ async def test_reauthentication_flow( result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/matter/conftest.py b/tests/components/matter/conftest.py index 486e2fd26ac..ad66bb7f6a9 100644 --- a/tests/components/matter/conftest.py +++ b/tests/components/matter/conftest.py @@ -27,7 +27,6 @@ async def matter_client_fixture() -> AsyncGenerator[MagicMock, None]: async def connect() -> None: """Mock connect.""" await asyncio.sleep(0) - client.connected = True async def listen(init_ready: asyncio.Event | None) -> None: """Mock listen.""" diff --git a/tests/components/matter/fixtures/config_entry_diagnostics.json b/tests/components/matter/fixtures/config_entry_diagnostics.json new file mode 100644 index 00000000000..13a4e7d26a5 --- /dev/null +++ b/tests/components/matter/fixtures/config_entry_diagnostics.json @@ -0,0 +1,4241 @@ +{ + "info": { + "fabric_id": 1, + "compressed_fabric_id": 1234, + "schema_version": 1, + "sdk_version": "2022.12.0", + "wifi_credentials_set": true, + "thread_credentials_set": false + }, + "nodes": [ + { + "node_id": 5, + "date_commissioned": "2023-01-16T21:07:57.508440", + "last_interview": "2023-01-16T21:07:57.508448", + "interview_version": 1, + "attributes": { + "0/4/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.NameSupport", + "attribute_name": "NameSupport", + "value": 128 + }, + "0/4/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "0/4/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "0/4/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [0, 1, 2, 3] + }, + "0/4/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 3, 4, 5] + }, + "0/4/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "0/29/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.DeviceTypeList", + "attribute_name": "DeviceTypeList", + "value": [ + { + "type": 22, + "revision": 1 + } + ] + }, + "0/29/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ServerList", + "attribute_name": "ServerList", + "value": [ + 4, 29, 31, 40, 42, 43, 44, 48, 49, 50, 51, 52, 53, 54, 55, 59, 60, + 62, 63, 64, 65 + ] + }, + "0/29/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClientList", + "attribute_name": "ClientList", + "value": [41] + }, + "0/29/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.PartsList", + "attribute_name": "PartsList", + "value": [1] + }, + "0/29/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/29/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/29/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/29/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/29/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/31/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.Acl", + "attribute_name": "Acl", + "value": [ + { + "privilege": 5, + "authMode": 2, + "subjects": [112233], + "targets": null, + "fabricIndex": 1 + } + ] + }, + "0/31/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.Extension", + "attribute_name": "Extension", + "value": [] + }, + "0/31/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.SubjectsPerAccessControlEntry", + "attribute_name": "SubjectsPerAccessControlEntry", + "value": 4 + }, + "0/31/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.TargetsPerAccessControlEntry", + "attribute_name": "TargetsPerAccessControlEntry", + "value": 3 + }, + "0/31/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AccessControlEntriesPerFabric", + "attribute_name": "AccessControlEntriesPerFabric", + "value": 3 + }, + "0/31/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/31/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/31/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/31/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/31/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533] + }, + "0/40/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", + "attribute_name": "DataModelRevision", + "value": 1 + }, + "0/40/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", + "attribute_name": "VendorName", + "value": "Nabu Casa" + }, + "0/40/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", + "attribute_name": "VendorID", + "value": 65521 + }, + "0/40/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", + "attribute_name": "ProductName", + "value": "M5STAMP Lighting App" + }, + "0/40/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", + "attribute_name": "ProductID", + "value": 32768 + }, + "0/40/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", + "attribute_name": "NodeLabel", + "value": "" + }, + "0/40/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", + "attribute_name": "Location", + "value": "XX" + }, + "0/40/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", + "attribute_name": "HardwareVersion", + "value": 0 + }, + "0/40/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", + "attribute_name": "HardwareVersionString", + "value": "v1.0" + }, + "0/40/9": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", + "attribute_name": "SoftwareVersion", + "value": 1 + }, + "0/40/10": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", + "attribute_name": "SoftwareVersionString", + "value": "v1.0" + }, + "0/40/11": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", + "attribute_name": "ManufacturingDate", + "value": "20200101" + }, + "0/40/12": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", + "attribute_name": "PartNumber", + "value": "" + }, + "0/40/13": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 13, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", + "attribute_name": "ProductURL", + "value": "" + }, + "0/40/14": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 14, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", + "attribute_name": "ProductLabel", + "value": "" + }, + "0/40/15": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", + "attribute_name": "SerialNumber", + "value": "TEST_SN" + }, + "0/40/16": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", + "attribute_name": "LocalConfigDisabled", + "value": false + }, + "0/40/17": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", + "attribute_name": "Reachable", + "value": true + }, + "0/40/18": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", + "attribute_name": "UniqueID", + "value": "869D5F986B588B29" + }, + "0/40/19": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", + "attribute_name": "CapabilityMinima", + "value": { + "caseSessionsPerFabric": 3, + "subscriptionsPerFabric": 3 + } + }, + "0/40/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/40/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/40/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/40/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/40/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 65528, 65529, 65531, 65532, 65533 + ] + }, + "0/42/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.DefaultOtaProviders", + "attribute_name": "DefaultOtaProviders", + "value": [] + }, + "0/42/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdatePossible", + "attribute_name": "UpdatePossible", + "value": true + }, + "0/42/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdateState", + "attribute_name": "UpdateState", + "value": 0 + }, + "0/42/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdateStateProgress", + "attribute_name": "UpdateStateProgress", + "value": 0 + }, + "0/42/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/42/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/42/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/42/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/42/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/43/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.ActiveLocale", + "attribute_name": "ActiveLocale", + "value": "en-US" + }, + "0/43/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.SupportedLocales", + "attribute_name": "SupportedLocales", + "value": [ + "en-US", + "de-DE", + "fr-FR", + "en-GB", + "es-ES", + "zh-CN", + "it-IT", + "ja-JP" + ] + }, + "0/43/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/43/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/43/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/43/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/43/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 65528, 65529, 65531, 65532, 65533] + }, + "0/44/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.HourFormat", + "attribute_name": "HourFormat", + "value": 0 + }, + "0/44/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.ActiveCalendarType", + "attribute_name": "ActiveCalendarType", + "value": 0 + }, + "0/44/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.SupportedCalendarTypes", + "attribute_name": "SupportedCalendarTypes", + "value": [0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 7] + }, + "0/44/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/44/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/44/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/44/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/44/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + }, + "0/48/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.Breadcrumb", + "attribute_name": "Breadcrumb", + "value": 0 + }, + "0/48/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.BasicCommissioningInfo", + "attribute_name": "BasicCommissioningInfo", + "value": { + "failSafeExpiryLengthSeconds": 60, + "maxCumulativeFailsafeSeconds": 900 + } + }, + "0/48/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.RegulatoryConfig", + "attribute_name": "RegulatoryConfig", + "value": 0 + }, + "0/48/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.LocationCapability", + "attribute_name": "LocationCapability", + "value": 0 + }, + "0/48/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.SupportsConcurrentConnection", + "attribute_name": "SupportsConcurrentConnection", + "value": true + }, + "0/48/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/48/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/48/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 3, 5] + }, + "0/48/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4] + }, + "0/48/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533] + }, + "0/49/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.MaxNetworks", + "attribute_name": "MaxNetworks", + "value": 1 + }, + "0/49/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.Networks", + "attribute_name": "Networks", + "value": [] + }, + "0/49/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ScanMaxTimeSeconds", + "attribute_name": "ScanMaxTimeSeconds", + "value": 10 + }, + "0/49/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ConnectMaxTimeSeconds", + "attribute_name": "ConnectMaxTimeSeconds", + "value": 30 + }, + "0/49/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.InterfaceEnabled", + "attribute_name": "InterfaceEnabled", + "value": true + }, + "0/49/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastNetworkingStatus", + "attribute_name": "LastNetworkingStatus", + "value": 0 + }, + "0/49/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastNetworkID", + "attribute_name": "LastNetworkID", + "value": "bVdMQU4yLjQ=" + }, + "0/49/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastConnectErrorValue", + "attribute_name": "LastConnectErrorValue", + "value": null + }, + "0/49/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "0/49/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/49/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 5, 7] + }, + "0/49/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4, 6, 8] + }, + "0/49/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533] + }, + "0/50/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/50/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/50/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1] + }, + "0/50/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/50/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [65528, 65529, 65531, 65532, 65533] + }, + "0/51/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.NetworkInterfaces", + "attribute_name": "NetworkInterfaces", + "value": [ + { + "name": "WIFI_STA_DEF", + "isOperational": true, + "offPremiseServicesReachableIPv4": null, + "offPremiseServicesReachableIPv6": null, + "hardwareAddress": "YFX5V0js", + "IPv4Addresses": ["wKgBIw=="], + "IPv6Addresses": ["/oAAAAAAAABiVfn//ldI7A=="], + "type": 1 + } + ] + }, + "0/51/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.RebootCount", + "attribute_name": "RebootCount", + "value": 3 + }, + "0/51/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.UpTime", + "attribute_name": "UpTime", + "value": 213 + }, + "0/51/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.TotalOperationalHours", + "attribute_name": "TotalOperationalHours", + "value": 0 + }, + "0/51/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.BootReasons", + "attribute_name": "BootReasons", + "value": 1 + }, + "0/51/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveHardwareFaults", + "attribute_name": "ActiveHardwareFaults", + "value": [] + }, + "0/51/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveRadioFaults", + "attribute_name": "ActiveRadioFaults", + "value": [] + }, + "0/51/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveNetworkFaults", + "attribute_name": "ActiveNetworkFaults", + "value": [] + }, + "0/51/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.TestEventTriggersEnabled", + "attribute_name": "TestEventTriggersEnabled", + "value": false + }, + "0/51/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/51/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/51/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/51/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/51/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533 + ] + }, + "0/52/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.ThreadMetrics", + "attribute_name": "ThreadMetrics", + "value": [] + }, + "0/52/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapFree", + "attribute_name": "CurrentHeapFree", + "value": 166660 + }, + "0/52/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapUsed", + "attribute_name": "CurrentHeapUsed", + "value": 86332 + }, + "0/52/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapHighWatermark", + "attribute_name": "CurrentHeapHighWatermark", + "value": 99208 + }, + "0/52/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/52/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/52/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/52/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/52/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/53/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Channel", + "attribute_name": "Channel", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RoutingRole", + "attribute_name": "RoutingRole", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.NetworkName", + "attribute_name": "NetworkName", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PanId", + "attribute_name": "PanId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ExtendedPanId", + "attribute_name": "ExtendedPanId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.MeshLocalPrefix", + "attribute_name": "MeshLocalPrefix", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.NeighborTableList", + "attribute_name": "NeighborTableList", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RouteTableList", + "attribute_name": "RouteTableList", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/9": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PartitionId", + "attribute_name": "PartitionId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/10": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Weighting", + "attribute_name": "Weighting", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/11": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.DataVersion", + "attribute_name": "DataVersion", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/12": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.StableDataVersion", + "attribute_name": "StableDataVersion", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/13": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 13, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.LeaderRouterId", + "attribute_name": "LeaderRouterId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/14": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 14, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.DetachedRoleCount", + "attribute_name": "DetachedRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/15": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ChildRoleCount", + "attribute_name": "ChildRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/16": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RouterRoleCount", + "attribute_name": "RouterRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/17": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.LeaderRoleCount", + "attribute_name": "LeaderRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/18": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AttachAttemptCount", + "attribute_name": "AttachAttemptCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/19": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PartitionIdChangeCount", + "attribute_name": "PartitionIdChangeCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/20": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 20, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.BetterPartitionAttachAttemptCount", + "attribute_name": "BetterPartitionAttachAttemptCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/21": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 21, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ParentChangeCount", + "attribute_name": "ParentChangeCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/22": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 22, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxTotalCount", + "attribute_name": "TxTotalCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/23": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 23, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxUnicastCount", + "attribute_name": "TxUnicastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/24": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 24, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBroadcastCount", + "attribute_name": "TxBroadcastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/25": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 25, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxAckRequestedCount", + "attribute_name": "TxAckRequestedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/26": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 26, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxAckedCount", + "attribute_name": "TxAckedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/27": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 27, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxNoAckRequestedCount", + "attribute_name": "TxNoAckRequestedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/28": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 28, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDataCount", + "attribute_name": "TxDataCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/29": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 29, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDataPollCount", + "attribute_name": "TxDataPollCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/30": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 30, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBeaconCount", + "attribute_name": "TxBeaconCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/31": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 31, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBeaconRequestCount", + "attribute_name": "TxBeaconRequestCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/32": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 32, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxOtherCount", + "attribute_name": "TxOtherCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/33": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 33, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxRetryCount", + "attribute_name": "TxRetryCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/34": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 34, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDirectMaxRetryExpiryCount", + "attribute_name": "TxDirectMaxRetryExpiryCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/35": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 35, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxIndirectMaxRetryExpiryCount", + "attribute_name": "TxIndirectMaxRetryExpiryCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/36": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 36, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrCcaCount", + "attribute_name": "TxErrCcaCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/37": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 37, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrAbortCount", + "attribute_name": "TxErrAbortCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/38": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 38, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrBusyChannelCount", + "attribute_name": "TxErrBusyChannelCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/39": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 39, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxTotalCount", + "attribute_name": "RxTotalCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/40": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 40, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxUnicastCount", + "attribute_name": "RxUnicastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/41": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 41, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBroadcastCount", + "attribute_name": "RxBroadcastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/42": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 42, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDataCount", + "attribute_name": "RxDataCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/43": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 43, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDataPollCount", + "attribute_name": "RxDataPollCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/44": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 44, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBeaconCount", + "attribute_name": "RxBeaconCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/45": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 45, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBeaconRequestCount", + "attribute_name": "RxBeaconRequestCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/46": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 46, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxOtherCount", + "attribute_name": "RxOtherCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/47": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 47, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxAddressFilteredCount", + "attribute_name": "RxAddressFilteredCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/48": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 48, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDestAddrFilteredCount", + "attribute_name": "RxDestAddrFilteredCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/49": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 49, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDuplicatedCount", + "attribute_name": "RxDuplicatedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/50": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 50, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrNoFrameCount", + "attribute_name": "RxErrNoFrameCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/51": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 51, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrUnknownNeighborCount", + "attribute_name": "RxErrUnknownNeighborCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/52": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 52, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrInvalidSrcAddrCount", + "attribute_name": "RxErrInvalidSrcAddrCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/53": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 53, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrSecCount", + "attribute_name": "RxErrSecCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/54": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 54, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrFcsCount", + "attribute_name": "RxErrFcsCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/55": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 55, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrOtherCount", + "attribute_name": "RxErrOtherCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/56": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 56, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ActiveTimestamp", + "attribute_name": "ActiveTimestamp", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/57": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 57, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PendingTimestamp", + "attribute_name": "PendingTimestamp", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/58": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 58, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Delay", + "attribute_name": "Delay", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/59": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 59, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.SecurityPolicy", + "attribute_name": "SecurityPolicy", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/60": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 60, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ChannelPage0Mask", + "attribute_name": "ChannelPage0Mask", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/61": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 61, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.OperationalDatasetComponents", + "attribute_name": "OperationalDatasetComponents", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/62": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 62, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ActiveNetworkFaultsList", + "attribute_name": "ActiveNetworkFaultsList", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 15 + }, + "0/53/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/53/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/53/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/53/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, + 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, + 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 65528, 65529, 65531, 65532, + 65533 + ] + }, + "0/54/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.Bssid", + "attribute_name": "Bssid", + "value": "BKFR27h1" + }, + "0/54/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.SecurityType", + "attribute_name": "SecurityType", + "value": 4 + }, + "0/54/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.WiFiVersion", + "attribute_name": "WiFiVersion", + "value": 3 + }, + "0/54/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.ChannelNumber", + "attribute_name": "ChannelNumber", + "value": 3 + }, + "0/54/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.Rssi", + "attribute_name": "Rssi", + "value": -56 + }, + "0/54/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.BeaconLostCount", + "attribute_name": "BeaconLostCount", + "value": null + }, + "0/54/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.BeaconRxCount", + "attribute_name": "BeaconRxCount", + "value": null + }, + "0/54/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketMulticastRxCount", + "attribute_name": "PacketMulticastRxCount", + "value": null + }, + "0/54/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketMulticastTxCount", + "attribute_name": "PacketMulticastTxCount", + "value": null + }, + "0/54/9": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketUnicastRxCount", + "attribute_name": "PacketUnicastRxCount", + "value": null + }, + "0/54/10": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketUnicastTxCount", + "attribute_name": "PacketUnicastTxCount", + "value": null + }, + "0/54/11": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.CurrentMaxRate", + "attribute_name": "CurrentMaxRate", + "value": null + }, + "0/54/12": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": null + }, + "0/54/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "0/54/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/54/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/54/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/54/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 65528, 65529, 65531, + 65532, 65533 + ] + }, + "0/55/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PHYRate", + "attribute_name": "PHYRate", + "value": null + }, + "0/55/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.FullDuplex", + "attribute_name": "FullDuplex", + "value": null + }, + "0/55/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PacketRxCount", + "attribute_name": "PacketRxCount", + "value": 0 + }, + "0/55/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PacketTxCount", + "attribute_name": "PacketTxCount", + "value": 0 + }, + "0/55/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.TxErrCount", + "attribute_name": "TxErrCount", + "value": 0 + }, + "0/55/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.CollisionCount", + "attribute_name": "CollisionCount", + "value": 0 + }, + "0/55/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": 0 + }, + "0/55/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.CarrierDetect", + "attribute_name": "CarrierDetect", + "value": null + }, + "0/55/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.TimeSinceReset", + "attribute_name": "TimeSinceReset", + "value": 0 + }, + "0/55/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "0/55/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/55/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/55/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/55/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533 + ] + }, + "0/59/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/59/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/59/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/59/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/59/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [65528, 65529, 65531, 65532, 65533] + }, + "0/60/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.WindowStatus", + "attribute_name": "WindowStatus", + "value": 0 + }, + "0/60/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AdminFabricIndex", + "attribute_name": "AdminFabricIndex", + "value": null + }, + "0/60/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AdminVendorId", + "attribute_name": "AdminVendorId", + "value": null + }, + "0/60/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/60/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/60/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/60/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2] + }, + "0/60/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + }, + "0/62/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.NOCs", + "attribute_name": "NOCs", + "value": [ + { + "noc": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVASQRBRgkBwEkCAEwCUEEELwf3lni0ez0mRGa/z9gFtuTfn3Gpnsq/rBvQmpgjxqgC0RNcZmHfAm176H0j6ENQrnc1RhkKA5qiJtEgzQF4DcKNQEoARgkAgE2AwQCBAEYMAQURdGBtNYpheXbKDo2Od5OLDCytacwBRQc+rrVsNzRFL1V9i4OFnGKrwIajRgwC0AG9mdYqL5WJ0jKIBcEzeWQbo8xg6sFv0ANmq0KSpMbfqVvw8Y39XEOQ6B8v+JCXSGMpdPC0nbVQKuv/pKUvJoTGA==", + "icac": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEWYzjmQq/3zCbWfMKR0asASVnOBOkNAzdwdW1X6sC0zA5m3DhGRMEff09ZqHDZi/o6CW+I+rEGNEyW+00/M84azcKNQEpARgkAmAwBBQc+rrVsNzRFL1V9i4OFnGKrwIajTAFFI6CuLTopCFiBYeGuUcP8Ak5Jo3gGDALQDYMHSAwxZPP4TFqIGot2vm5+Wir58quxbojkWwyT9l8eat6f9sJmjTZ0VLggTwAWvY+IVm82YuMzTPxmkNWxVIY", + "fabricIndex": 1 + } + ] + }, + "0/62/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.Fabrics", + "attribute_name": "Fabrics", + "value": [ + { + "rootPublicKey": "BALNCzn2XOp1NrwszT+LOLYT+tM76+Pob8AIOFl9+0UWFsLp4ZHUainZZMJQIAHxv39srVUYW0+nacFcjHTzNHw=", + "vendorId": 65521, + "fabricId": 1, + "nodeId": 5, + "label": "", + "fabricIndex": 1 + } + ] + }, + "0/62/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.SupportedFabrics", + "attribute_name": "SupportedFabrics", + "value": 5 + }, + "0/62/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.CommissionedFabrics", + "attribute_name": "CommissionedFabrics", + "value": 1 + }, + "0/62/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.TrustedRootCertificates", + "attribute_name": "TrustedRootCertificates", + "value": [ + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEAs0LOfZc6nU2vCzNP4s4thP60zvr4+hvwAg4WX37RRYWwunhkdRqKdlkwlAgAfG/f2ytVRhbT6dpwVyMdPM0fDcKNQEpARgkAmAwBBSOgri06KQhYgWHhrlHD/AJOSaN4DAFFI6CuLTopCFiBYeGuUcP8Ak5Jo3gGDALQGxeigcXo8H7pmRHCOma3uT688xoreaDwV8JfFUMUnHUvqg+2GNzFtvfD6MkDaYVPghsXjITZLv5qsHhrUaIO7QY" + ] + }, + "0/62/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.CurrentFabricIndex", + "attribute_name": "CurrentFabricIndex", + "value": 1 + }, + "0/62/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/62/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/62/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 3, 5, 8] + }, + "0/62/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4, 6, 7, 9, 10, 11] + }, + "0/62/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533] + }, + "0/63/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.GroupKeyMap", + "attribute_name": "GroupKeyMap", + "value": [] + }, + "0/63/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.GroupTable", + "attribute_name": "GroupTable", + "value": [] + }, + "0/63/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.MaxGroupsPerFabric", + "attribute_name": "MaxGroupsPerFabric", + "value": 3 + }, + "0/63/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.MaxGroupKeysPerFabric", + "attribute_name": "MaxGroupKeysPerFabric", + "value": 3 + }, + "0/63/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/63/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/63/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [2, 5] + }, + "0/63/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 3, 4] + }, + "0/63/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/64/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.LabelList", + "attribute_name": "LabelList", + "value": [ + { + "label": "room", + "value": "bedroom 2" + }, + { + "label": "orientation", + "value": "North" + }, + { + "label": "floor", + "value": "2" + }, + { + "label": "direction", + "value": "up" + } + ] + }, + "0/64/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/64/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/64/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/64/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/64/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "0/65/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.LabelList", + "attribute_name": "LabelList", + "value": [] + }, + "0/65/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/65/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/65/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/65/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/65/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "1/3/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.IdentifyTime", + "attribute_name": "IdentifyTime", + "value": 0 + }, + "1/3/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.IdentifyType", + "attribute_name": "IdentifyType", + "value": 0 + }, + "1/3/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/3/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "1/3/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/3/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 64] + }, + "1/3/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 65528, 65529, 65531, 65532, 65533] + }, + "1/4/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.NameSupport", + "attribute_name": "NameSupport", + "value": 128 + }, + "1/4/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "1/4/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "1/4/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [0, 1, 2, 3] + }, + "1/4/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 3, 4, 5] + }, + "1/4/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "1/6/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.OnOff", + "attribute_name": "OnOff", + "value": false + }, + "1/6/16384": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16384, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.GlobalSceneControl", + "attribute_name": "GlobalSceneControl", + "value": true + }, + "1/6/16385": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16385, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.OnTime", + "attribute_name": "OnTime", + "value": 0 + }, + "1/6/16386": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16386, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.OffWaitTime", + "attribute_name": "OffWaitTime", + "value": 0 + }, + "1/6/16387": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16387, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.StartUpOnOff", + "attribute_name": "StartUpOnOff", + "value": null + }, + "1/6/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "1/6/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "1/6/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/6/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 64, 65, 66] + }, + "1/6/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 16384, 16385, 16386, 16387, 65528, 65529, 65531, 65532, 65533 + ] + }, + "1/8/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.CurrentLevel", + "attribute_name": "CurrentLevel", + "value": 254 + }, + "1/8/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.RemainingTime", + "attribute_name": "RemainingTime", + "value": 0 + }, + "1/8/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MinLevel", + "attribute_name": "MinLevel", + "value": 1 + }, + "1/8/3": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MaxLevel", + "attribute_name": "MaxLevel", + "value": 254 + }, + "1/8/4": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.CurrentFrequency", + "attribute_name": "CurrentFrequency", + "value": 0 + }, + "1/8/5": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MinFrequency", + "attribute_name": "MinFrequency", + "value": 0 + }, + "1/8/6": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MaxFrequency", + "attribute_name": "MaxFrequency", + "value": 0 + }, + "1/8/15": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.Options", + "attribute_name": "Options", + "value": 0 + }, + "1/8/16": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OnOffTransitionTime", + "attribute_name": "OnOffTransitionTime", + "value": 0 + }, + "1/8/17": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OnLevel", + "attribute_name": "OnLevel", + "value": null + }, + "1/8/18": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OnTransitionTime", + "attribute_name": "OnTransitionTime", + "value": 0 + }, + "1/8/19": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OffTransitionTime", + "attribute_name": "OffTransitionTime", + "value": 0 + }, + "1/8/20": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 20, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.DefaultMoveRate", + "attribute_name": "DefaultMoveRate", + "value": 50 + }, + "1/8/16384": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 16384, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.StartUpCurrentLevel", + "attribute_name": "StartUpCurrentLevel", + "value": null + }, + "1/8/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "1/8/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 5 + }, + "1/8/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/8/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 3, 4, 5, 6, 7] + }, + "1/8/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 15, 16, 17, 18, 19, 20, 16384, 65528, 65529, + 65531, 65532, 65533 + ] + }, + "1/29/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.DeviceTypeList", + "attribute_name": "DeviceTypeList", + "value": [ + { + "type": 257, + "revision": 1 + } + ] + }, + "1/29/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ServerList", + "attribute_name": "ServerList", + "value": [3, 4, 6, 8, 29, 768, 1030] + }, + "1/29/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClientList", + "attribute_name": "ClientList", + "value": [] + }, + "1/29/3": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.PartsList", + "attribute_name": "PartsList", + "value": [] + }, + "1/29/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/29/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "1/29/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/29/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "1/29/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "1/768/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentHue", + "attribute_name": "CurrentHue", + "value": 0 + }, + "1/768/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentSaturation", + "attribute_name": "CurrentSaturation", + "value": 0 + }, + "1/768/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.RemainingTime", + "attribute_name": "RemainingTime", + "value": 0 + }, + "1/768/3": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentX", + "attribute_name": "CurrentX", + "value": 24939 + }, + "1/768/4": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentY", + "attribute_name": "CurrentY", + "value": 24701 + }, + "1/768/7": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorTemperatureMireds", + "attribute_name": "ColorTemperatureMireds", + "value": 0 + }, + "1/768/8": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorMode", + "attribute_name": "ColorMode", + "value": 2 + }, + "1/768/15": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.Options", + "attribute_name": "Options", + "value": 0 + }, + "1/768/16": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.NumberOfPrimaries", + "attribute_name": "NumberOfPrimaries", + "value": 0 + }, + "1/768/16384": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16384, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.EnhancedCurrentHue", + "attribute_name": "EnhancedCurrentHue", + "value": 0 + }, + "1/768/16385": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16385, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.EnhancedColorMode", + "attribute_name": "EnhancedColorMode", + "value": 2 + }, + "1/768/16386": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16386, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopActive", + "attribute_name": "ColorLoopActive", + "value": 0 + }, + "1/768/16387": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16387, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopDirection", + "attribute_name": "ColorLoopDirection", + "value": 0 + }, + "1/768/16388": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16388, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopTime", + "attribute_name": "ColorLoopTime", + "value": 25 + }, + "1/768/16389": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16389, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopStartEnhancedHue", + "attribute_name": "ColorLoopStartEnhancedHue", + "value": 8960 + }, + "1/768/16390": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16390, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopStoredEnhancedHue", + "attribute_name": "ColorLoopStoredEnhancedHue", + "value": 0 + }, + "1/768/16394": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16394, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorCapabilities", + "attribute_name": "ColorCapabilities", + "value": 31 + }, + "1/768/16395": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16395, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorTempPhysicalMinMireds", + "attribute_name": "ColorTempPhysicalMinMireds", + "value": 0 + }, + "1/768/16396": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16396, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorTempPhysicalMaxMireds", + "attribute_name": "ColorTempPhysicalMaxMireds", + "value": 65279 + }, + "1/768/16397": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16397, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CoupleColorTempToLevelMinMireds", + "attribute_name": "CoupleColorTempToLevelMinMireds", + "value": 0 + }, + "1/768/16400": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16400, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.StartUpColorTemperatureMireds", + "attribute_name": "StartUpColorTemperatureMireds", + "value": 0 + }, + "1/768/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 31 + }, + "1/768/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 5 + }, + "1/768/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/768/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 64, 65, 66, 67, 68, 71, 75, 76 + ] + }, + "1/768/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 7, 8, 15, 16, 16384, 16385, 16386, 16387, 16388, + 16389, 16390, 16394, 16395, 16396, 16397, 16400, 65528, 65529, + 65531, 65532, 65533 + ] + }, + "1/1030/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.Occupancy", + "attribute_name": "Occupancy", + "value": 0 + }, + "1/1030/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.OccupancySensorType", + "attribute_name": "OccupancySensorType", + "value": 0 + }, + "1/1030/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.OccupancySensorTypeBitmap", + "attribute_name": "OccupancySensorTypeBitmap", + "value": 1 + }, + "1/1030/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/1030/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 3 + }, + "1/1030/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/1030/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "1/1030/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + } + }, + "endpoints": [0, 1], + "root_device_type_instance": { + "__type": "", + "repr": "" + }, + "aggregator_device_type_instance": null, + "device_type_instances": [ + { + "__type": "", + "repr": "" + } + ], + "node_devices": [ + { + "__type": "", + "repr": "" + } + ] + } + ], + "events": [] +} diff --git a/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json b/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json new file mode 100644 index 00000000000..8f798a50467 --- /dev/null +++ b/tests/components/matter/fixtures/config_entry_diagnostics_redacted.json @@ -0,0 +1,4243 @@ +{ + "server": { + "info": { + "fabric_id": 1, + "compressed_fabric_id": 1234, + "schema_version": 1, + "sdk_version": "2022.12.0", + "wifi_credentials_set": true, + "thread_credentials_set": false + }, + "nodes": [ + { + "node_id": 5, + "date_commissioned": "2023-01-16T21:07:57.508440", + "last_interview": "2023-01-16T21:07:57.508448", + "interview_version": 1, + "attributes": { + "0/4/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.NameSupport", + "attribute_name": "NameSupport", + "value": 128 + }, + "0/4/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "0/4/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "0/4/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [0, 1, 2, 3] + }, + "0/4/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 3, 4, 5] + }, + "0/4/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "0/29/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.DeviceTypeList", + "attribute_name": "DeviceTypeList", + "value": [ + { + "type": 22, + "revision": 1 + } + ] + }, + "0/29/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ServerList", + "attribute_name": "ServerList", + "value": [ + 4, 29, 31, 40, 42, 43, 44, 48, 49, 50, 51, 52, 53, 54, 55, 59, 60, + 62, 63, 64, 65 + ] + }, + "0/29/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClientList", + "attribute_name": "ClientList", + "value": [41] + }, + "0/29/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.PartsList", + "attribute_name": "PartsList", + "value": [1] + }, + "0/29/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/29/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/29/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/29/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/29/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/31/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.Acl", + "attribute_name": "Acl", + "value": [ + { + "privilege": 5, + "authMode": 2, + "subjects": [112233], + "targets": null, + "fabricIndex": 1 + } + ] + }, + "0/31/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.Extension", + "attribute_name": "Extension", + "value": [] + }, + "0/31/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.SubjectsPerAccessControlEntry", + "attribute_name": "SubjectsPerAccessControlEntry", + "value": 4 + }, + "0/31/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.TargetsPerAccessControlEntry", + "attribute_name": "TargetsPerAccessControlEntry", + "value": 3 + }, + "0/31/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AccessControlEntriesPerFabric", + "attribute_name": "AccessControlEntriesPerFabric", + "value": 3 + }, + "0/31/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/31/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/31/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/31/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/31/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533] + }, + "0/40/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", + "attribute_name": "DataModelRevision", + "value": 1 + }, + "0/40/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", + "attribute_name": "VendorName", + "value": "Nabu Casa" + }, + "0/40/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", + "attribute_name": "VendorID", + "value": 65521 + }, + "0/40/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", + "attribute_name": "ProductName", + "value": "M5STAMP Lighting App" + }, + "0/40/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", + "attribute_name": "ProductID", + "value": 32768 + }, + "0/40/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", + "attribute_name": "NodeLabel", + "value": "" + }, + "0/40/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", + "attribute_name": "Location", + "value": "**REDACTED**" + }, + "0/40/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", + "attribute_name": "HardwareVersion", + "value": 0 + }, + "0/40/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", + "attribute_name": "HardwareVersionString", + "value": "v1.0" + }, + "0/40/9": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", + "attribute_name": "SoftwareVersion", + "value": 1 + }, + "0/40/10": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", + "attribute_name": "SoftwareVersionString", + "value": "v1.0" + }, + "0/40/11": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", + "attribute_name": "ManufacturingDate", + "value": "20200101" + }, + "0/40/12": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", + "attribute_name": "PartNumber", + "value": "" + }, + "0/40/13": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 13, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", + "attribute_name": "ProductURL", + "value": "" + }, + "0/40/14": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 14, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", + "attribute_name": "ProductLabel", + "value": "" + }, + "0/40/15": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", + "attribute_name": "SerialNumber", + "value": "TEST_SN" + }, + "0/40/16": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", + "attribute_name": "LocalConfigDisabled", + "value": false + }, + "0/40/17": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", + "attribute_name": "Reachable", + "value": true + }, + "0/40/18": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", + "attribute_name": "UniqueID", + "value": "869D5F986B588B29" + }, + "0/40/19": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", + "attribute_name": "CapabilityMinima", + "value": { + "caseSessionsPerFabric": 3, + "subscriptionsPerFabric": 3 + } + }, + "0/40/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/40/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/40/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/40/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/40/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 65528, 65529, 65531, 65532, 65533 + ] + }, + "0/42/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.DefaultOtaProviders", + "attribute_name": "DefaultOtaProviders", + "value": [] + }, + "0/42/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdatePossible", + "attribute_name": "UpdatePossible", + "value": true + }, + "0/42/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdateState", + "attribute_name": "UpdateState", + "value": 0 + }, + "0/42/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdateStateProgress", + "attribute_name": "UpdateStateProgress", + "value": 0 + }, + "0/42/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/42/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/42/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/42/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/42/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/43/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.ActiveLocale", + "attribute_name": "ActiveLocale", + "value": "en-US" + }, + "0/43/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.SupportedLocales", + "attribute_name": "SupportedLocales", + "value": [ + "en-US", + "de-DE", + "fr-FR", + "en-GB", + "es-ES", + "zh-CN", + "it-IT", + "ja-JP" + ] + }, + "0/43/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/43/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/43/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/43/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/43/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 65528, 65529, 65531, 65532, 65533] + }, + "0/44/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.HourFormat", + "attribute_name": "HourFormat", + "value": 0 + }, + "0/44/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.ActiveCalendarType", + "attribute_name": "ActiveCalendarType", + "value": 0 + }, + "0/44/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.SupportedCalendarTypes", + "attribute_name": "SupportedCalendarTypes", + "value": [0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 7] + }, + "0/44/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/44/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/44/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/44/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/44/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + }, + "0/48/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.Breadcrumb", + "attribute_name": "Breadcrumb", + "value": 0 + }, + "0/48/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.BasicCommissioningInfo", + "attribute_name": "BasicCommissioningInfo", + "value": { + "failSafeExpiryLengthSeconds": 60, + "maxCumulativeFailsafeSeconds": 900 + } + }, + "0/48/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.RegulatoryConfig", + "attribute_name": "RegulatoryConfig", + "value": 0 + }, + "0/48/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.LocationCapability", + "attribute_name": "LocationCapability", + "value": 0 + }, + "0/48/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.SupportsConcurrentConnection", + "attribute_name": "SupportsConcurrentConnection", + "value": true + }, + "0/48/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/48/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/48/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 3, 5] + }, + "0/48/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4] + }, + "0/48/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533] + }, + "0/49/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.MaxNetworks", + "attribute_name": "MaxNetworks", + "value": 1 + }, + "0/49/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.Networks", + "attribute_name": "Networks", + "value": [] + }, + "0/49/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ScanMaxTimeSeconds", + "attribute_name": "ScanMaxTimeSeconds", + "value": 10 + }, + "0/49/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ConnectMaxTimeSeconds", + "attribute_name": "ConnectMaxTimeSeconds", + "value": 30 + }, + "0/49/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.InterfaceEnabled", + "attribute_name": "InterfaceEnabled", + "value": true + }, + "0/49/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastNetworkingStatus", + "attribute_name": "LastNetworkingStatus", + "value": 0 + }, + "0/49/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastNetworkID", + "attribute_name": "LastNetworkID", + "value": "bVdMQU4yLjQ=" + }, + "0/49/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastConnectErrorValue", + "attribute_name": "LastConnectErrorValue", + "value": null + }, + "0/49/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "0/49/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/49/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 5, 7] + }, + "0/49/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4, 6, 8] + }, + "0/49/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533] + }, + "0/50/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/50/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/50/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1] + }, + "0/50/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/50/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [65528, 65529, 65531, 65532, 65533] + }, + "0/51/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.NetworkInterfaces", + "attribute_name": "NetworkInterfaces", + "value": [ + { + "name": "WIFI_STA_DEF", + "isOperational": true, + "offPremiseServicesReachableIPv4": null, + "offPremiseServicesReachableIPv6": null, + "hardwareAddress": "YFX5V0js", + "IPv4Addresses": ["wKgBIw=="], + "IPv6Addresses": ["/oAAAAAAAABiVfn//ldI7A=="], + "type": 1 + } + ] + }, + "0/51/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.RebootCount", + "attribute_name": "RebootCount", + "value": 3 + }, + "0/51/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.UpTime", + "attribute_name": "UpTime", + "value": 213 + }, + "0/51/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.TotalOperationalHours", + "attribute_name": "TotalOperationalHours", + "value": 0 + }, + "0/51/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.BootReasons", + "attribute_name": "BootReasons", + "value": 1 + }, + "0/51/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveHardwareFaults", + "attribute_name": "ActiveHardwareFaults", + "value": [] + }, + "0/51/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveRadioFaults", + "attribute_name": "ActiveRadioFaults", + "value": [] + }, + "0/51/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveNetworkFaults", + "attribute_name": "ActiveNetworkFaults", + "value": [] + }, + "0/51/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.TestEventTriggersEnabled", + "attribute_name": "TestEventTriggersEnabled", + "value": false + }, + "0/51/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/51/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/51/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/51/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/51/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533 + ] + }, + "0/52/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.ThreadMetrics", + "attribute_name": "ThreadMetrics", + "value": [] + }, + "0/52/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapFree", + "attribute_name": "CurrentHeapFree", + "value": 166660 + }, + "0/52/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapUsed", + "attribute_name": "CurrentHeapUsed", + "value": 86332 + }, + "0/52/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapHighWatermark", + "attribute_name": "CurrentHeapHighWatermark", + "value": 99208 + }, + "0/52/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/52/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/52/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/52/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/52/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/53/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Channel", + "attribute_name": "Channel", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RoutingRole", + "attribute_name": "RoutingRole", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.NetworkName", + "attribute_name": "NetworkName", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PanId", + "attribute_name": "PanId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ExtendedPanId", + "attribute_name": "ExtendedPanId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.MeshLocalPrefix", + "attribute_name": "MeshLocalPrefix", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.NeighborTableList", + "attribute_name": "NeighborTableList", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RouteTableList", + "attribute_name": "RouteTableList", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/9": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PartitionId", + "attribute_name": "PartitionId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/10": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Weighting", + "attribute_name": "Weighting", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/11": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.DataVersion", + "attribute_name": "DataVersion", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/12": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.StableDataVersion", + "attribute_name": "StableDataVersion", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/13": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 13, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.LeaderRouterId", + "attribute_name": "LeaderRouterId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/14": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 14, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.DetachedRoleCount", + "attribute_name": "DetachedRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/15": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ChildRoleCount", + "attribute_name": "ChildRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/16": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RouterRoleCount", + "attribute_name": "RouterRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/17": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.LeaderRoleCount", + "attribute_name": "LeaderRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/18": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AttachAttemptCount", + "attribute_name": "AttachAttemptCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/19": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PartitionIdChangeCount", + "attribute_name": "PartitionIdChangeCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/20": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 20, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.BetterPartitionAttachAttemptCount", + "attribute_name": "BetterPartitionAttachAttemptCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/21": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 21, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ParentChangeCount", + "attribute_name": "ParentChangeCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/22": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 22, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxTotalCount", + "attribute_name": "TxTotalCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/23": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 23, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxUnicastCount", + "attribute_name": "TxUnicastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/24": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 24, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBroadcastCount", + "attribute_name": "TxBroadcastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/25": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 25, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxAckRequestedCount", + "attribute_name": "TxAckRequestedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/26": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 26, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxAckedCount", + "attribute_name": "TxAckedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/27": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 27, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxNoAckRequestedCount", + "attribute_name": "TxNoAckRequestedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/28": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 28, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDataCount", + "attribute_name": "TxDataCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/29": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 29, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDataPollCount", + "attribute_name": "TxDataPollCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/30": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 30, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBeaconCount", + "attribute_name": "TxBeaconCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/31": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 31, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBeaconRequestCount", + "attribute_name": "TxBeaconRequestCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/32": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 32, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxOtherCount", + "attribute_name": "TxOtherCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/33": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 33, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxRetryCount", + "attribute_name": "TxRetryCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/34": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 34, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDirectMaxRetryExpiryCount", + "attribute_name": "TxDirectMaxRetryExpiryCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/35": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 35, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxIndirectMaxRetryExpiryCount", + "attribute_name": "TxIndirectMaxRetryExpiryCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/36": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 36, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrCcaCount", + "attribute_name": "TxErrCcaCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/37": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 37, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrAbortCount", + "attribute_name": "TxErrAbortCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/38": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 38, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrBusyChannelCount", + "attribute_name": "TxErrBusyChannelCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/39": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 39, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxTotalCount", + "attribute_name": "RxTotalCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/40": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 40, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxUnicastCount", + "attribute_name": "RxUnicastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/41": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 41, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBroadcastCount", + "attribute_name": "RxBroadcastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/42": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 42, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDataCount", + "attribute_name": "RxDataCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/43": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 43, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDataPollCount", + "attribute_name": "RxDataPollCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/44": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 44, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBeaconCount", + "attribute_name": "RxBeaconCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/45": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 45, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBeaconRequestCount", + "attribute_name": "RxBeaconRequestCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/46": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 46, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxOtherCount", + "attribute_name": "RxOtherCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/47": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 47, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxAddressFilteredCount", + "attribute_name": "RxAddressFilteredCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/48": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 48, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDestAddrFilteredCount", + "attribute_name": "RxDestAddrFilteredCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/49": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 49, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDuplicatedCount", + "attribute_name": "RxDuplicatedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/50": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 50, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrNoFrameCount", + "attribute_name": "RxErrNoFrameCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/51": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 51, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrUnknownNeighborCount", + "attribute_name": "RxErrUnknownNeighborCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/52": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 52, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrInvalidSrcAddrCount", + "attribute_name": "RxErrInvalidSrcAddrCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/53": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 53, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrSecCount", + "attribute_name": "RxErrSecCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/54": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 54, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrFcsCount", + "attribute_name": "RxErrFcsCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/55": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 55, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrOtherCount", + "attribute_name": "RxErrOtherCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/56": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 56, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ActiveTimestamp", + "attribute_name": "ActiveTimestamp", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/57": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 57, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PendingTimestamp", + "attribute_name": "PendingTimestamp", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/58": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 58, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Delay", + "attribute_name": "Delay", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/59": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 59, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.SecurityPolicy", + "attribute_name": "SecurityPolicy", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/60": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 60, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ChannelPage0Mask", + "attribute_name": "ChannelPage0Mask", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/61": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 61, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.OperationalDatasetComponents", + "attribute_name": "OperationalDatasetComponents", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/62": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 62, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ActiveNetworkFaultsList", + "attribute_name": "ActiveNetworkFaultsList", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 15 + }, + "0/53/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/53/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/53/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/53/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, + 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 65528, 65529, + 65531, 65532, 65533 + ] + }, + "0/54/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.Bssid", + "attribute_name": "Bssid", + "value": "BKFR27h1" + }, + "0/54/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.SecurityType", + "attribute_name": "SecurityType", + "value": 4 + }, + "0/54/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.WiFiVersion", + "attribute_name": "WiFiVersion", + "value": 3 + }, + "0/54/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.ChannelNumber", + "attribute_name": "ChannelNumber", + "value": 3 + }, + "0/54/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.Rssi", + "attribute_name": "Rssi", + "value": -56 + }, + "0/54/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.BeaconLostCount", + "attribute_name": "BeaconLostCount", + "value": null + }, + "0/54/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.BeaconRxCount", + "attribute_name": "BeaconRxCount", + "value": null + }, + "0/54/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketMulticastRxCount", + "attribute_name": "PacketMulticastRxCount", + "value": null + }, + "0/54/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketMulticastTxCount", + "attribute_name": "PacketMulticastTxCount", + "value": null + }, + "0/54/9": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketUnicastRxCount", + "attribute_name": "PacketUnicastRxCount", + "value": null + }, + "0/54/10": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketUnicastTxCount", + "attribute_name": "PacketUnicastTxCount", + "value": null + }, + "0/54/11": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.CurrentMaxRate", + "attribute_name": "CurrentMaxRate", + "value": null + }, + "0/54/12": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": null + }, + "0/54/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "0/54/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/54/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/54/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/54/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 65528, 65529, 65531, + 65532, 65533 + ] + }, + "0/55/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PHYRate", + "attribute_name": "PHYRate", + "value": null + }, + "0/55/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.FullDuplex", + "attribute_name": "FullDuplex", + "value": null + }, + "0/55/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PacketRxCount", + "attribute_name": "PacketRxCount", + "value": 0 + }, + "0/55/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PacketTxCount", + "attribute_name": "PacketTxCount", + "value": 0 + }, + "0/55/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.TxErrCount", + "attribute_name": "TxErrCount", + "value": 0 + }, + "0/55/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.CollisionCount", + "attribute_name": "CollisionCount", + "value": 0 + }, + "0/55/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": 0 + }, + "0/55/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.CarrierDetect", + "attribute_name": "CarrierDetect", + "value": null + }, + "0/55/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.TimeSinceReset", + "attribute_name": "TimeSinceReset", + "value": 0 + }, + "0/55/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "0/55/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/55/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/55/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/55/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533 + ] + }, + "0/59/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/59/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/59/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/59/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/59/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [65528, 65529, 65531, 65532, 65533] + }, + "0/60/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.WindowStatus", + "attribute_name": "WindowStatus", + "value": 0 + }, + "0/60/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AdminFabricIndex", + "attribute_name": "AdminFabricIndex", + "value": null + }, + "0/60/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AdminVendorId", + "attribute_name": "AdminVendorId", + "value": null + }, + "0/60/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/60/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/60/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/60/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2] + }, + "0/60/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + }, + "0/62/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.NOCs", + "attribute_name": "NOCs", + "value": [ + { + "noc": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVASQRBRgkBwEkCAEwCUEEELwf3lni0ez0mRGa/z9gFtuTfn3Gpnsq/rBvQmpgjxqgC0RNcZmHfAm176H0j6ENQrnc1RhkKA5qiJtEgzQF4DcKNQEoARgkAgE2AwQCBAEYMAQURdGBtNYpheXbKDo2Od5OLDCytacwBRQc+rrVsNzRFL1V9i4OFnGKrwIajRgwC0AG9mdYqL5WJ0jKIBcEzeWQbo8xg6sFv0ANmq0KSpMbfqVvw8Y39XEOQ6B8v+JCXSGMpdPC0nbVQKuv/pKUvJoTGA==", + "icac": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEWYzjmQq/3zCbWfMKR0asASVnOBOkNAzdwdW1X6sC0zA5m3DhGRMEff09ZqHDZi/o6CW+I+rEGNEyW+00/M84azcKNQEpARgkAmAwBBQc+rrVsNzRFL1V9i4OFnGKrwIajTAFFI6CuLTopCFiBYeGuUcP8Ak5Jo3gGDALQDYMHSAwxZPP4TFqIGot2vm5+Wir58quxbojkWwyT9l8eat6f9sJmjTZ0VLggTwAWvY+IVm82YuMzTPxmkNWxVIY", + "fabricIndex": 1 + } + ] + }, + "0/62/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.Fabrics", + "attribute_name": "Fabrics", + "value": [ + { + "rootPublicKey": "BALNCzn2XOp1NrwszT+LOLYT+tM76+Pob8AIOFl9+0UWFsLp4ZHUainZZMJQIAHxv39srVUYW0+nacFcjHTzNHw=", + "vendorId": 65521, + "fabricId": 1, + "nodeId": 5, + "label": "", + "fabricIndex": 1 + } + ] + }, + "0/62/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.SupportedFabrics", + "attribute_name": "SupportedFabrics", + "value": 5 + }, + "0/62/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.CommissionedFabrics", + "attribute_name": "CommissionedFabrics", + "value": 1 + }, + "0/62/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.TrustedRootCertificates", + "attribute_name": "TrustedRootCertificates", + "value": [ + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEAs0LOfZc6nU2vCzNP4s4thP60zvr4+hvwAg4WX37RRYWwunhkdRqKdlkwlAgAfG/f2ytVRhbT6dpwVyMdPM0fDcKNQEpARgkAmAwBBSOgri06KQhYgWHhrlHD/AJOSaN4DAFFI6CuLTopCFiBYeGuUcP8Ak5Jo3gGDALQGxeigcXo8H7pmRHCOma3uT688xoreaDwV8JfFUMUnHUvqg+2GNzFtvfD6MkDaYVPghsXjITZLv5qsHhrUaIO7QY" + ] + }, + "0/62/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.CurrentFabricIndex", + "attribute_name": "CurrentFabricIndex", + "value": 1 + }, + "0/62/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/62/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/62/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 3, 5, 8] + }, + "0/62/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4, 6, 7, 9, 10, 11] + }, + "0/62/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533] + }, + "0/63/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.GroupKeyMap", + "attribute_name": "GroupKeyMap", + "value": [] + }, + "0/63/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.GroupTable", + "attribute_name": "GroupTable", + "value": [] + }, + "0/63/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.MaxGroupsPerFabric", + "attribute_name": "MaxGroupsPerFabric", + "value": 3 + }, + "0/63/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.MaxGroupKeysPerFabric", + "attribute_name": "MaxGroupKeysPerFabric", + "value": 3 + }, + "0/63/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/63/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/63/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [2, 5] + }, + "0/63/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 3, 4] + }, + "0/63/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/64/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.LabelList", + "attribute_name": "LabelList", + "value": [ + { + "label": "room", + "value": "bedroom 2" + }, + { + "label": "orientation", + "value": "North" + }, + { + "label": "floor", + "value": "2" + }, + { + "label": "direction", + "value": "up" + } + ] + }, + "0/64/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/64/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/64/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/64/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/64/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "0/65/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.LabelList", + "attribute_name": "LabelList", + "value": [] + }, + "0/65/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/65/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/65/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/65/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/65/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "1/3/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.IdentifyTime", + "attribute_name": "IdentifyTime", + "value": 0 + }, + "1/3/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.IdentifyType", + "attribute_name": "IdentifyType", + "value": 0 + }, + "1/3/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/3/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "1/3/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/3/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 64] + }, + "1/3/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 65528, 65529, 65531, 65532, 65533] + }, + "1/4/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.NameSupport", + "attribute_name": "NameSupport", + "value": 128 + }, + "1/4/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "1/4/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "1/4/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [0, 1, 2, 3] + }, + "1/4/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 3, 4, 5] + }, + "1/4/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "1/6/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.OnOff", + "attribute_name": "OnOff", + "value": false + }, + "1/6/16384": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16384, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.GlobalSceneControl", + "attribute_name": "GlobalSceneControl", + "value": true + }, + "1/6/16385": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16385, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.OnTime", + "attribute_name": "OnTime", + "value": 0 + }, + "1/6/16386": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16386, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.OffWaitTime", + "attribute_name": "OffWaitTime", + "value": 0 + }, + "1/6/16387": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16387, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.StartUpOnOff", + "attribute_name": "StartUpOnOff", + "value": null + }, + "1/6/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "1/6/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "1/6/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/6/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 64, 65, 66] + }, + "1/6/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 16384, 16385, 16386, 16387, 65528, 65529, 65531, 65532, 65533 + ] + }, + "1/8/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.CurrentLevel", + "attribute_name": "CurrentLevel", + "value": 254 + }, + "1/8/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.RemainingTime", + "attribute_name": "RemainingTime", + "value": 0 + }, + "1/8/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MinLevel", + "attribute_name": "MinLevel", + "value": 1 + }, + "1/8/3": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MaxLevel", + "attribute_name": "MaxLevel", + "value": 254 + }, + "1/8/4": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.CurrentFrequency", + "attribute_name": "CurrentFrequency", + "value": 0 + }, + "1/8/5": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MinFrequency", + "attribute_name": "MinFrequency", + "value": 0 + }, + "1/8/6": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MaxFrequency", + "attribute_name": "MaxFrequency", + "value": 0 + }, + "1/8/15": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.Options", + "attribute_name": "Options", + "value": 0 + }, + "1/8/16": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OnOffTransitionTime", + "attribute_name": "OnOffTransitionTime", + "value": 0 + }, + "1/8/17": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OnLevel", + "attribute_name": "OnLevel", + "value": null + }, + "1/8/18": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OnTransitionTime", + "attribute_name": "OnTransitionTime", + "value": 0 + }, + "1/8/19": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OffTransitionTime", + "attribute_name": "OffTransitionTime", + "value": 0 + }, + "1/8/20": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 20, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.DefaultMoveRate", + "attribute_name": "DefaultMoveRate", + "value": 50 + }, + "1/8/16384": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 16384, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.StartUpCurrentLevel", + "attribute_name": "StartUpCurrentLevel", + "value": null + }, + "1/8/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "1/8/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 5 + }, + "1/8/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/8/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 3, 4, 5, 6, 7] + }, + "1/8/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 15, 16, 17, 18, 19, 20, 16384, 65528, 65529, + 65531, 65532, 65533 + ] + }, + "1/29/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.DeviceTypeList", + "attribute_name": "DeviceTypeList", + "value": [ + { + "type": 257, + "revision": 1 + } + ] + }, + "1/29/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ServerList", + "attribute_name": "ServerList", + "value": [3, 4, 6, 8, 29, 768, 1030] + }, + "1/29/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClientList", + "attribute_name": "ClientList", + "value": [] + }, + "1/29/3": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.PartsList", + "attribute_name": "PartsList", + "value": [] + }, + "1/29/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/29/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "1/29/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/29/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "1/29/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "1/768/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentHue", + "attribute_name": "CurrentHue", + "value": 0 + }, + "1/768/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentSaturation", + "attribute_name": "CurrentSaturation", + "value": 0 + }, + "1/768/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.RemainingTime", + "attribute_name": "RemainingTime", + "value": 0 + }, + "1/768/3": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentX", + "attribute_name": "CurrentX", + "value": 24939 + }, + "1/768/4": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentY", + "attribute_name": "CurrentY", + "value": 24701 + }, + "1/768/7": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorTemperatureMireds", + "attribute_name": "ColorTemperatureMireds", + "value": 0 + }, + "1/768/8": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorMode", + "attribute_name": "ColorMode", + "value": 2 + }, + "1/768/15": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.Options", + "attribute_name": "Options", + "value": 0 + }, + "1/768/16": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.NumberOfPrimaries", + "attribute_name": "NumberOfPrimaries", + "value": 0 + }, + "1/768/16384": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16384, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.EnhancedCurrentHue", + "attribute_name": "EnhancedCurrentHue", + "value": 0 + }, + "1/768/16385": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16385, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.EnhancedColorMode", + "attribute_name": "EnhancedColorMode", + "value": 2 + }, + "1/768/16386": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16386, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopActive", + "attribute_name": "ColorLoopActive", + "value": 0 + }, + "1/768/16387": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16387, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopDirection", + "attribute_name": "ColorLoopDirection", + "value": 0 + }, + "1/768/16388": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16388, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopTime", + "attribute_name": "ColorLoopTime", + "value": 25 + }, + "1/768/16389": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16389, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopStartEnhancedHue", + "attribute_name": "ColorLoopStartEnhancedHue", + "value": 8960 + }, + "1/768/16390": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16390, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopStoredEnhancedHue", + "attribute_name": "ColorLoopStoredEnhancedHue", + "value": 0 + }, + "1/768/16394": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16394, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorCapabilities", + "attribute_name": "ColorCapabilities", + "value": 31 + }, + "1/768/16395": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16395, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorTempPhysicalMinMireds", + "attribute_name": "ColorTempPhysicalMinMireds", + "value": 0 + }, + "1/768/16396": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16396, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorTempPhysicalMaxMireds", + "attribute_name": "ColorTempPhysicalMaxMireds", + "value": 65279 + }, + "1/768/16397": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16397, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CoupleColorTempToLevelMinMireds", + "attribute_name": "CoupleColorTempToLevelMinMireds", + "value": 0 + }, + "1/768/16400": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16400, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.StartUpColorTemperatureMireds", + "attribute_name": "StartUpColorTemperatureMireds", + "value": 0 + }, + "1/768/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 31 + }, + "1/768/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 5 + }, + "1/768/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/768/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 64, 65, 66, 67, 68, 71, 75, 76 + ] + }, + "1/768/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 7, 8, 15, 16, 16384, 16385, 16386, 16387, 16388, + 16389, 16390, 16394, 16395, 16396, 16397, 16400, 65528, 65529, + 65531, 65532, 65533 + ] + }, + "1/1030/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.Occupancy", + "attribute_name": "Occupancy", + "value": 0 + }, + "1/1030/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.OccupancySensorType", + "attribute_name": "OccupancySensorType", + "value": 0 + }, + "1/1030/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.OccupancySensorTypeBitmap", + "attribute_name": "OccupancySensorTypeBitmap", + "value": 1 + }, + "1/1030/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/1030/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 3 + }, + "1/1030/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/1030/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "1/1030/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + } + }, + "endpoints": [0, 1], + "root_device_type_instance": { + "__type": "", + "repr": "" + }, + "aggregator_device_type_instance": null, + "device_type_instances": [ + { + "__type": "", + "repr": "" + } + ], + "node_devices": [ + { + "__type": "", + "repr": "" + } + ] + } + ], + "events": [] + } +} diff --git a/tests/components/matter/fixtures/nodes/contact-sensor.json b/tests/components/matter/fixtures/nodes/contact-sensor.json index 2aec6a32516..5e2ca4e937c 100644 --- a/tests/components/matter/fixtures/nodes/contact-sensor.json +++ b/tests/components/matter/fixtures/nodes/contact-sensor.json @@ -115,10 +115,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -126,10 +126,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -137,10 +137,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -148,10 +148,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock ContactSensor" }, @@ -159,10 +159,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -170,10 +170,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "Mock Contact sensor" }, @@ -181,10 +181,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -192,10 +192,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -203,10 +203,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -214,10 +214,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -225,10 +225,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -236,10 +236,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20221206" }, @@ -247,10 +247,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -258,10 +258,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -269,10 +269,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -280,10 +280,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -291,10 +291,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -302,10 +302,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -313,10 +313,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-contact-sensor" }, @@ -324,10 +324,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -338,10 +338,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -349,10 +349,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -360,10 +360,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -371,10 +371,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -382,10 +382,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/fixtures/nodes/device_diagnostics.json b/tests/components/matter/fixtures/nodes/device_diagnostics.json new file mode 100644 index 00000000000..377d72c7352 --- /dev/null +++ b/tests/components/matter/fixtures/nodes/device_diagnostics.json @@ -0,0 +1,4223 @@ +{ + "node_id": 5, + "date_commissioned": "2023-01-16T21:07:57.508440", + "last_interview": "2023-01-16T21:07:57.508448", + "interview_version": 1, + "attributes": { + "0/4/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.NameSupport", + "attribute_name": "NameSupport", + "value": 128 + }, + "0/4/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "0/4/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "0/4/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [0, 1, 2, 3] + }, + "0/4/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 3, 4, 5] + }, + "0/4/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "0/29/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.DeviceTypeList", + "attribute_name": "DeviceTypeList", + "value": [ + { + "type": 22, + "revision": 1 + } + ] + }, + "0/29/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ServerList", + "attribute_name": "ServerList", + "value": [ + 4, 29, 31, 40, 42, 43, 44, 48, 49, 50, 51, 52, 53, 54, 55, 59, 60, 62, + 63, 64, 65 + ] + }, + "0/29/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClientList", + "attribute_name": "ClientList", + "value": [41] + }, + "0/29/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.PartsList", + "attribute_name": "PartsList", + "value": [1] + }, + "0/29/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/29/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/29/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/29/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/29/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/31/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.Acl", + "attribute_name": "Acl", + "value": [ + { + "privilege": 5, + "authMode": 2, + "subjects": [112233], + "targets": null, + "fabricIndex": 1 + } + ] + }, + "0/31/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.Extension", + "attribute_name": "Extension", + "value": [] + }, + "0/31/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.SubjectsPerAccessControlEntry", + "attribute_name": "SubjectsPerAccessControlEntry", + "value": 4 + }, + "0/31/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.TargetsPerAccessControlEntry", + "attribute_name": "TargetsPerAccessControlEntry", + "value": 3 + }, + "0/31/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AccessControlEntriesPerFabric", + "attribute_name": "AccessControlEntriesPerFabric", + "value": 3 + }, + "0/31/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/31/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/31/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/31/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/31/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 31, + "cluster_type": "chip.clusters.Objects.AccessControl", + "cluster_name": "AccessControl", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.AccessControl.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533] + }, + "0/40/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", + "attribute_name": "DataModelRevision", + "value": 1 + }, + "0/40/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", + "attribute_name": "VendorName", + "value": "Nabu Casa" + }, + "0/40/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", + "attribute_name": "VendorID", + "value": 65521 + }, + "0/40/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", + "attribute_name": "ProductName", + "value": "M5STAMP Lighting App" + }, + "0/40/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", + "attribute_name": "ProductID", + "value": 32768 + }, + "0/40/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", + "attribute_name": "NodeLabel", + "value": "" + }, + "0/40/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", + "attribute_name": "Location", + "value": "XX" + }, + "0/40/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", + "attribute_name": "HardwareVersion", + "value": 0 + }, + "0/40/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", + "attribute_name": "HardwareVersionString", + "value": "v1.0" + }, + "0/40/9": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", + "attribute_name": "SoftwareVersion", + "value": 1 + }, + "0/40/10": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", + "attribute_name": "SoftwareVersionString", + "value": "v1.0" + }, + "0/40/11": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", + "attribute_name": "ManufacturingDate", + "value": "20200101" + }, + "0/40/12": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", + "attribute_name": "PartNumber", + "value": "" + }, + "0/40/13": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 13, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", + "attribute_name": "ProductURL", + "value": "" + }, + "0/40/14": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 14, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", + "attribute_name": "ProductLabel", + "value": "" + }, + "0/40/15": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", + "attribute_name": "SerialNumber", + "value": "TEST_SN" + }, + "0/40/16": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", + "attribute_name": "LocalConfigDisabled", + "value": false + }, + "0/40/17": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", + "attribute_name": "Reachable", + "value": true + }, + "0/40/18": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", + "attribute_name": "UniqueID", + "value": "869D5F986B588B29" + }, + "0/40/19": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", + "attribute_name": "CapabilityMinima", + "value": { + "caseSessionsPerFabric": 3, + "subscriptionsPerFabric": 3 + } + }, + "0/40/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/40/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/40/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/40/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/40/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 40, + "cluster_type": "chip.clusters.Objects.BasicInformation", + "cluster_name": "Basic", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 65528, 65529, 65531, 65532, 65533 + ] + }, + "0/42/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.DefaultOtaProviders", + "attribute_name": "DefaultOtaProviders", + "value": [] + }, + "0/42/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdatePossible", + "attribute_name": "UpdatePossible", + "value": true + }, + "0/42/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdateState", + "attribute_name": "UpdateState", + "value": 0 + }, + "0/42/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.UpdateStateProgress", + "attribute_name": "UpdateStateProgress", + "value": 0 + }, + "0/42/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/42/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/42/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/42/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/42/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 42, + "cluster_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor", + "cluster_name": "OtaSoftwareUpdateRequestor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OtaSoftwareUpdateRequestor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/43/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.ActiveLocale", + "attribute_name": "ActiveLocale", + "value": "en-US" + }, + "0/43/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.SupportedLocales", + "attribute_name": "SupportedLocales", + "value": [ + "en-US", + "de-DE", + "fr-FR", + "en-GB", + "es-ES", + "zh-CN", + "it-IT", + "ja-JP" + ] + }, + "0/43/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/43/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/43/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/43/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/43/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 43, + "cluster_type": "chip.clusters.Objects.LocalizationConfiguration", + "cluster_name": "LocalizationConfiguration", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.LocalizationConfiguration.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 65528, 65529, 65531, 65532, 65533] + }, + "0/44/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.HourFormat", + "attribute_name": "HourFormat", + "value": 0 + }, + "0/44/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.ActiveCalendarType", + "attribute_name": "ActiveCalendarType", + "value": 0 + }, + "0/44/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.SupportedCalendarTypes", + "attribute_name": "SupportedCalendarTypes", + "value": [0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 7] + }, + "0/44/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/44/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/44/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/44/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/44/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 44, + "cluster_type": "chip.clusters.Objects.TimeFormatLocalization", + "cluster_name": "TimeFormatLocalization", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.TimeFormatLocalization.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + }, + "0/48/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.Breadcrumb", + "attribute_name": "Breadcrumb", + "value": 0 + }, + "0/48/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.BasicCommissioningInfo", + "attribute_name": "BasicCommissioningInfo", + "value": { + "failSafeExpiryLengthSeconds": 60, + "maxCumulativeFailsafeSeconds": 900 + } + }, + "0/48/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.RegulatoryConfig", + "attribute_name": "RegulatoryConfig", + "value": 0 + }, + "0/48/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.LocationCapability", + "attribute_name": "LocationCapability", + "value": 0 + }, + "0/48/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.SupportsConcurrentConnection", + "attribute_name": "SupportsConcurrentConnection", + "value": true + }, + "0/48/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/48/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/48/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 3, 5] + }, + "0/48/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4] + }, + "0/48/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 48, + "cluster_type": "chip.clusters.Objects.GeneralCommissioning", + "cluster_name": "GeneralCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GeneralCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533] + }, + "0/49/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.MaxNetworks", + "attribute_name": "MaxNetworks", + "value": 1 + }, + "0/49/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.Networks", + "attribute_name": "Networks", + "value": [] + }, + "0/49/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ScanMaxTimeSeconds", + "attribute_name": "ScanMaxTimeSeconds", + "value": 10 + }, + "0/49/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ConnectMaxTimeSeconds", + "attribute_name": "ConnectMaxTimeSeconds", + "value": 30 + }, + "0/49/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.InterfaceEnabled", + "attribute_name": "InterfaceEnabled", + "value": true + }, + "0/49/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastNetworkingStatus", + "attribute_name": "LastNetworkingStatus", + "value": 0 + }, + "0/49/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastNetworkID", + "attribute_name": "LastNetworkID", + "value": "bVdMQU4yLjQ=" + }, + "0/49/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.LastConnectErrorValue", + "attribute_name": "LastConnectErrorValue", + "value": null + }, + "0/49/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "0/49/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/49/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 5, 7] + }, + "0/49/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4, 6, 8] + }, + "0/49/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 49, + "cluster_type": "chip.clusters.Objects.NetworkCommissioning", + "cluster_name": "NetworkCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.NetworkCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533] + }, + "0/50/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/50/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/50/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1] + }, + "0/50/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/50/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 50, + "cluster_type": "chip.clusters.Objects.DiagnosticLogs", + "cluster_name": "DiagnosticLogs", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.DiagnosticLogs.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [65528, 65529, 65531, 65532, 65533] + }, + "0/51/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.NetworkInterfaces", + "attribute_name": "NetworkInterfaces", + "value": [ + { + "name": "WIFI_STA_DEF", + "isOperational": true, + "offPremiseServicesReachableIPv4": null, + "offPremiseServicesReachableIPv6": null, + "hardwareAddress": "YFX5V0js", + "IPv4Addresses": ["wKgBIw=="], + "IPv6Addresses": ["/oAAAAAAAABiVfn//ldI7A=="], + "type": 1 + } + ] + }, + "0/51/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.RebootCount", + "attribute_name": "RebootCount", + "value": 3 + }, + "0/51/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.UpTime", + "attribute_name": "UpTime", + "value": 213 + }, + "0/51/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.TotalOperationalHours", + "attribute_name": "TotalOperationalHours", + "value": 0 + }, + "0/51/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.BootReasons", + "attribute_name": "BootReasons", + "value": 1 + }, + "0/51/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveHardwareFaults", + "attribute_name": "ActiveHardwareFaults", + "value": [] + }, + "0/51/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveRadioFaults", + "attribute_name": "ActiveRadioFaults", + "value": [] + }, + "0/51/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ActiveNetworkFaults", + "attribute_name": "ActiveNetworkFaults", + "value": [] + }, + "0/51/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.TestEventTriggersEnabled", + "attribute_name": "TestEventTriggersEnabled", + "value": false + }, + "0/51/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/51/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/51/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/51/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/51/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 51, + "cluster_type": "chip.clusters.Objects.GeneralDiagnostics", + "cluster_name": "GeneralDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GeneralDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533] + }, + "0/52/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.ThreadMetrics", + "attribute_name": "ThreadMetrics", + "value": [] + }, + "0/52/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapFree", + "attribute_name": "CurrentHeapFree", + "value": 166660 + }, + "0/52/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapUsed", + "attribute_name": "CurrentHeapUsed", + "value": 86332 + }, + "0/52/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.CurrentHeapHighWatermark", + "attribute_name": "CurrentHeapHighWatermark", + "value": 99208 + }, + "0/52/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/52/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/52/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/52/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/52/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 52, + "cluster_type": "chip.clusters.Objects.SoftwareDiagnostics", + "cluster_name": "SoftwareDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.SoftwareDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/53/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Channel", + "attribute_name": "Channel", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RoutingRole", + "attribute_name": "RoutingRole", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.NetworkName", + "attribute_name": "NetworkName", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PanId", + "attribute_name": "PanId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ExtendedPanId", + "attribute_name": "ExtendedPanId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.MeshLocalPrefix", + "attribute_name": "MeshLocalPrefix", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.NeighborTableList", + "attribute_name": "NeighborTableList", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RouteTableList", + "attribute_name": "RouteTableList", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/9": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PartitionId", + "attribute_name": "PartitionId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/10": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Weighting", + "attribute_name": "Weighting", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/11": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.DataVersion", + "attribute_name": "DataVersion", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/12": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.StableDataVersion", + "attribute_name": "StableDataVersion", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/13": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 13, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.LeaderRouterId", + "attribute_name": "LeaderRouterId", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/14": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 14, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.DetachedRoleCount", + "attribute_name": "DetachedRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/15": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ChildRoleCount", + "attribute_name": "ChildRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/16": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RouterRoleCount", + "attribute_name": "RouterRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/17": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.LeaderRoleCount", + "attribute_name": "LeaderRoleCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/18": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AttachAttemptCount", + "attribute_name": "AttachAttemptCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/19": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PartitionIdChangeCount", + "attribute_name": "PartitionIdChangeCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/20": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 20, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.BetterPartitionAttachAttemptCount", + "attribute_name": "BetterPartitionAttachAttemptCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/21": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 21, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ParentChangeCount", + "attribute_name": "ParentChangeCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/22": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 22, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxTotalCount", + "attribute_name": "TxTotalCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/23": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 23, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxUnicastCount", + "attribute_name": "TxUnicastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/24": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 24, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBroadcastCount", + "attribute_name": "TxBroadcastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/25": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 25, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxAckRequestedCount", + "attribute_name": "TxAckRequestedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/26": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 26, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxAckedCount", + "attribute_name": "TxAckedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/27": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 27, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxNoAckRequestedCount", + "attribute_name": "TxNoAckRequestedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/28": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 28, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDataCount", + "attribute_name": "TxDataCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/29": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 29, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDataPollCount", + "attribute_name": "TxDataPollCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/30": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 30, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBeaconCount", + "attribute_name": "TxBeaconCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/31": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 31, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxBeaconRequestCount", + "attribute_name": "TxBeaconRequestCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/32": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 32, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxOtherCount", + "attribute_name": "TxOtherCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/33": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 33, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxRetryCount", + "attribute_name": "TxRetryCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/34": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 34, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxDirectMaxRetryExpiryCount", + "attribute_name": "TxDirectMaxRetryExpiryCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/35": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 35, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxIndirectMaxRetryExpiryCount", + "attribute_name": "TxIndirectMaxRetryExpiryCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/36": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 36, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrCcaCount", + "attribute_name": "TxErrCcaCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/37": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 37, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrAbortCount", + "attribute_name": "TxErrAbortCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/38": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 38, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.TxErrBusyChannelCount", + "attribute_name": "TxErrBusyChannelCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/39": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 39, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxTotalCount", + "attribute_name": "RxTotalCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/40": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 40, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxUnicastCount", + "attribute_name": "RxUnicastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/41": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 41, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBroadcastCount", + "attribute_name": "RxBroadcastCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/42": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 42, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDataCount", + "attribute_name": "RxDataCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/43": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 43, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDataPollCount", + "attribute_name": "RxDataPollCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/44": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 44, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBeaconCount", + "attribute_name": "RxBeaconCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/45": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 45, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxBeaconRequestCount", + "attribute_name": "RxBeaconRequestCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/46": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 46, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxOtherCount", + "attribute_name": "RxOtherCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/47": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 47, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxAddressFilteredCount", + "attribute_name": "RxAddressFilteredCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/48": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 48, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDestAddrFilteredCount", + "attribute_name": "RxDestAddrFilteredCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/49": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 49, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxDuplicatedCount", + "attribute_name": "RxDuplicatedCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/50": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 50, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrNoFrameCount", + "attribute_name": "RxErrNoFrameCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/51": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 51, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrUnknownNeighborCount", + "attribute_name": "RxErrUnknownNeighborCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/52": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 52, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrInvalidSrcAddrCount", + "attribute_name": "RxErrInvalidSrcAddrCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/53": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 53, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrSecCount", + "attribute_name": "RxErrSecCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/54": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 54, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrFcsCount", + "attribute_name": "RxErrFcsCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/55": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 55, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.RxErrOtherCount", + "attribute_name": "RxErrOtherCount", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/56": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 56, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ActiveTimestamp", + "attribute_name": "ActiveTimestamp", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/57": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 57, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.PendingTimestamp", + "attribute_name": "PendingTimestamp", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/58": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 58, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.Delay", + "attribute_name": "Delay", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/59": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 59, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.SecurityPolicy", + "attribute_name": "SecurityPolicy", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/60": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 60, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ChannelPage0Mask", + "attribute_name": "ChannelPage0Mask", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/61": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 61, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.OperationalDatasetComponents", + "attribute_name": "OperationalDatasetComponents", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/62": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 62, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ActiveNetworkFaultsList", + "attribute_name": "ActiveNetworkFaultsList", + "value": { + "TLVValue": null, + "Reason": null + } + }, + "0/53/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 15 + }, + "0/53/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/53/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/53/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/53/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 53, + "cluster_type": "chip.clusters.Objects.ThreadNetworkDiagnostics", + "cluster_name": "ThreadNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.ThreadNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, + 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, + 56, 57, 58, 59, 60, 61, 62, 65528, 65529, 65531, 65532, 65533 + ] + }, + "0/54/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.Bssid", + "attribute_name": "Bssid", + "value": "BKFR27h1" + }, + "0/54/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.SecurityType", + "attribute_name": "SecurityType", + "value": 4 + }, + "0/54/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.WiFiVersion", + "attribute_name": "WiFiVersion", + "value": 3 + }, + "0/54/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.ChannelNumber", + "attribute_name": "ChannelNumber", + "value": 3 + }, + "0/54/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.Rssi", + "attribute_name": "Rssi", + "value": -56 + }, + "0/54/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.BeaconLostCount", + "attribute_name": "BeaconLostCount", + "value": null + }, + "0/54/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.BeaconRxCount", + "attribute_name": "BeaconRxCount", + "value": null + }, + "0/54/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketMulticastRxCount", + "attribute_name": "PacketMulticastRxCount", + "value": null + }, + "0/54/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketMulticastTxCount", + "attribute_name": "PacketMulticastTxCount", + "value": null + }, + "0/54/9": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 9, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketUnicastRxCount", + "attribute_name": "PacketUnicastRxCount", + "value": null + }, + "0/54/10": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 10, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.PacketUnicastTxCount", + "attribute_name": "PacketUnicastTxCount", + "value": null + }, + "0/54/11": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 11, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.CurrentMaxRate", + "attribute_name": "CurrentMaxRate", + "value": null + }, + "0/54/12": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 12, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": null + }, + "0/54/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "0/54/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/54/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/54/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/54/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 54, + "cluster_type": "chip.clusters.Objects.WiFiNetworkDiagnostics", + "cluster_name": "WiFiNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.WiFiNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 65528, 65529, 65531, 65532, + 65533 + ] + }, + "0/55/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PHYRate", + "attribute_name": "PHYRate", + "value": null + }, + "0/55/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.FullDuplex", + "attribute_name": "FullDuplex", + "value": null + }, + "0/55/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PacketRxCount", + "attribute_name": "PacketRxCount", + "value": 0 + }, + "0/55/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.PacketTxCount", + "attribute_name": "PacketTxCount", + "value": 0 + }, + "0/55/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.TxErrCount", + "attribute_name": "TxErrCount", + "value": 0 + }, + "0/55/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.CollisionCount", + "attribute_name": "CollisionCount", + "value": 0 + }, + "0/55/6": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.OverrunCount", + "attribute_name": "OverrunCount", + "value": 0 + }, + "0/55/7": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.CarrierDetect", + "attribute_name": "CarrierDetect", + "value": null + }, + "0/55/8": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.TimeSinceReset", + "attribute_name": "TimeSinceReset", + "value": 0 + }, + "0/55/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "0/55/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/55/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/55/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0] + }, + "0/55/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 55, + "cluster_type": "chip.clusters.Objects.EthernetNetworkDiagnostics", + "cluster_name": "EthernetNetworkDiagnostics", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.EthernetNetworkDiagnostics.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 5, 6, 7, 8, 65528, 65529, 65531, 65532, 65533] + }, + "0/59/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/59/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/59/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/59/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/59/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 59, + "cluster_type": "chip.clusters.Objects.Switch", + "cluster_name": "Switch", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Switch.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [65528, 65529, 65531, 65532, 65533] + }, + "0/60/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.WindowStatus", + "attribute_name": "WindowStatus", + "value": 0 + }, + "0/60/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AdminFabricIndex", + "attribute_name": "AdminFabricIndex", + "value": null + }, + "0/60/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AdminVendorId", + "attribute_name": "AdminVendorId", + "value": null + }, + "0/60/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/60/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/60/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/60/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2] + }, + "0/60/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 60, + "cluster_type": "chip.clusters.Objects.AdministratorCommissioning", + "cluster_name": "AdministratorCommissioning", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.AdministratorCommissioning.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + }, + "0/62/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.NOCs", + "attribute_name": "NOCs", + "value": [ + { + "noc": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVASQRBRgkBwEkCAEwCUEEELwf3lni0ez0mRGa/z9gFtuTfn3Gpnsq/rBvQmpgjxqgC0RNcZmHfAm176H0j6ENQrnc1RhkKA5qiJtEgzQF4DcKNQEoARgkAgE2AwQCBAEYMAQURdGBtNYpheXbKDo2Od5OLDCytacwBRQc+rrVsNzRFL1V9i4OFnGKrwIajRgwC0AG9mdYqL5WJ0jKIBcEzeWQbo8xg6sFv0ANmq0KSpMbfqVvw8Y39XEOQ6B8v+JCXSGMpdPC0nbVQKuv/pKUvJoTGA==", + "icac": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEWYzjmQq/3zCbWfMKR0asASVnOBOkNAzdwdW1X6sC0zA5m3DhGRMEff09ZqHDZi/o6CW+I+rEGNEyW+00/M84azcKNQEpARgkAmAwBBQc+rrVsNzRFL1V9i4OFnGKrwIajTAFFI6CuLTopCFiBYeGuUcP8Ak5Jo3gGDALQDYMHSAwxZPP4TFqIGot2vm5+Wir58quxbojkWwyT9l8eat6f9sJmjTZ0VLggTwAWvY+IVm82YuMzTPxmkNWxVIY", + "fabricIndex": 1 + } + ] + }, + "0/62/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.Fabrics", + "attribute_name": "Fabrics", + "value": [ + { + "rootPublicKey": "BALNCzn2XOp1NrwszT+LOLYT+tM76+Pob8AIOFl9+0UWFsLp4ZHUainZZMJQIAHxv39srVUYW0+nacFcjHTzNHw=", + "vendorId": 65521, + "fabricId": 1, + "nodeId": 5, + "label": "", + "fabricIndex": 1 + } + ] + }, + "0/62/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.SupportedFabrics", + "attribute_name": "SupportedFabrics", + "value": 5 + }, + "0/62/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.CommissionedFabrics", + "attribute_name": "CommissionedFabrics", + "value": 1 + }, + "0/62/4": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.TrustedRootCertificates", + "attribute_name": "TrustedRootCertificates", + "value": [ + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEEAs0LOfZc6nU2vCzNP4s4thP60zvr4+hvwAg4WX37RRYWwunhkdRqKdlkwlAgAfG/f2ytVRhbT6dpwVyMdPM0fDcKNQEpARgkAmAwBBSOgri06KQhYgWHhrlHD/AJOSaN4DAFFI6CuLTopCFiBYeGuUcP8Ak5Jo3gGDALQGxeigcXo8H7pmRHCOma3uT688xoreaDwV8JfFUMUnHUvqg+2GNzFtvfD6MkDaYVPghsXjITZLv5qsHhrUaIO7QY" + ] + }, + "0/62/5": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.CurrentFabricIndex", + "attribute_name": "CurrentFabricIndex", + "value": 1 + }, + "0/62/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/62/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/62/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [1, 3, 5, 8] + }, + "0/62/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 2, 4, 6, 7, 9, 10, 11] + }, + "0/62/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 62, + "cluster_type": "chip.clusters.Objects.OperationalCredentials", + "cluster_name": "OperationalCredentials", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OperationalCredentials.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533] + }, + "0/63/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.GroupKeyMap", + "attribute_name": "GroupKeyMap", + "value": [] + }, + "0/63/1": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.GroupTable", + "attribute_name": "GroupTable", + "value": [] + }, + "0/63/2": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.MaxGroupsPerFabric", + "attribute_name": "MaxGroupsPerFabric", + "value": 3 + }, + "0/63/3": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.MaxGroupKeysPerFabric", + "attribute_name": "MaxGroupKeysPerFabric", + "value": 3 + }, + "0/63/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/63/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/63/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [2, 5] + }, + "0/63/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 3, 4] + }, + "0/63/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 63, + "cluster_type": "chip.clusters.Objects.GroupKeyManagement", + "cluster_name": "GroupKeyManagement", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.GroupKeyManagement.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "0/64/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.LabelList", + "attribute_name": "LabelList", + "value": [ + { + "label": "room", + "value": "bedroom 2" + }, + { + "label": "orientation", + "value": "North" + }, + { + "label": "floor", + "value": "2" + }, + { + "label": "direction", + "value": "up" + } + ] + }, + "0/64/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/64/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/64/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/64/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/64/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 64, + "cluster_type": "chip.clusters.Objects.FixedLabel", + "cluster_name": "FixedLabel", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.FixedLabel.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "0/65/0": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.LabelList", + "attribute_name": "LabelList", + "value": [] + }, + "0/65/65532": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "0/65/65533": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "0/65/65528": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "0/65/65529": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "0/65/65531": { + "node_id": 5, + "endpoint": 0, + "cluster_id": 65, + "cluster_type": "chip.clusters.Objects.UserLabel", + "cluster_name": "UserLabel", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.UserLabel.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "1/3/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.IdentifyTime", + "attribute_name": "IdentifyTime", + "value": 0 + }, + "1/3/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.IdentifyType", + "attribute_name": "IdentifyType", + "value": 0 + }, + "1/3/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/3/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "1/3/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/3/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 64] + }, + "1/3/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 3, + "cluster_type": "chip.clusters.Objects.Identify", + "cluster_name": "Identify", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Identify.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 65528, 65529, 65531, 65532, 65533] + }, + "1/4/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.NameSupport", + "attribute_name": "NameSupport", + "value": 128 + }, + "1/4/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "1/4/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "1/4/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [0, 1, 2, 3] + }, + "1/4/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 3, 4, 5] + }, + "1/4/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 4, + "cluster_type": "chip.clusters.Objects.Groups", + "cluster_name": "Groups", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Groups.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 65528, 65529, 65531, 65532, 65533] + }, + "1/6/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.OnOff", + "attribute_name": "OnOff", + "value": false + }, + "1/6/16384": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16384, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.GlobalSceneControl", + "attribute_name": "GlobalSceneControl", + "value": true + }, + "1/6/16385": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16385, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.OnTime", + "attribute_name": "OnTime", + "value": 0 + }, + "1/6/16386": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16386, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.OffWaitTime", + "attribute_name": "OffWaitTime", + "value": 0 + }, + "1/6/16387": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 16387, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.StartUpOnOff", + "attribute_name": "StartUpOnOff", + "value": null + }, + "1/6/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 1 + }, + "1/6/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 4 + }, + "1/6/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/6/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 64, 65, 66] + }, + "1/6/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 6, + "cluster_type": "chip.clusters.Objects.OnOff", + "cluster_name": "OnOff", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OnOff.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 16384, 16385, 16386, 16387, 65528, 65529, 65531, 65532, 65533 + ] + }, + "1/8/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.CurrentLevel", + "attribute_name": "CurrentLevel", + "value": 254 + }, + "1/8/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.RemainingTime", + "attribute_name": "RemainingTime", + "value": 0 + }, + "1/8/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MinLevel", + "attribute_name": "MinLevel", + "value": 1 + }, + "1/8/3": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MaxLevel", + "attribute_name": "MaxLevel", + "value": 254 + }, + "1/8/4": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.CurrentFrequency", + "attribute_name": "CurrentFrequency", + "value": 0 + }, + "1/8/5": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 5, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MinFrequency", + "attribute_name": "MinFrequency", + "value": 0 + }, + "1/8/6": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 6, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.MaxFrequency", + "attribute_name": "MaxFrequency", + "value": 0 + }, + "1/8/15": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.Options", + "attribute_name": "Options", + "value": 0 + }, + "1/8/16": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OnOffTransitionTime", + "attribute_name": "OnOffTransitionTime", + "value": 0 + }, + "1/8/17": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 17, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OnLevel", + "attribute_name": "OnLevel", + "value": null + }, + "1/8/18": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 18, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OnTransitionTime", + "attribute_name": "OnTransitionTime", + "value": 0 + }, + "1/8/19": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 19, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.OffTransitionTime", + "attribute_name": "OffTransitionTime", + "value": 0 + }, + "1/8/20": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 20, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.DefaultMoveRate", + "attribute_name": "DefaultMoveRate", + "value": 50 + }, + "1/8/16384": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 16384, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.StartUpCurrentLevel", + "attribute_name": "StartUpCurrentLevel", + "value": null + }, + "1/8/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 3 + }, + "1/8/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 5 + }, + "1/8/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/8/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [0, 1, 2, 3, 4, 5, 6, 7] + }, + "1/8/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 8, + "cluster_type": "chip.clusters.Objects.LevelControl", + "cluster_name": "LevelControl", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.LevelControl.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 15, 16, 17, 18, 19, 20, 16384, 65528, 65529, 65531, + 65532, 65533 + ] + }, + "1/29/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.DeviceTypeList", + "attribute_name": "DeviceTypeList", + "value": [ + { + "type": 257, + "revision": 1 + } + ] + }, + "1/29/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ServerList", + "attribute_name": "ServerList", + "value": [3, 4, 6, 8, 29, 768, 1030] + }, + "1/29/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClientList", + "attribute_name": "ClientList", + "value": [] + }, + "1/29/3": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.PartsList", + "attribute_name": "PartsList", + "value": [] + }, + "1/29/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/29/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 1 + }, + "1/29/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/29/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "1/29/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 29, + "cluster_type": "chip.clusters.Objects.Descriptor", + "cluster_name": "Descriptor", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.Descriptor.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533] + }, + "1/768/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentHue", + "attribute_name": "CurrentHue", + "value": 0 + }, + "1/768/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentSaturation", + "attribute_name": "CurrentSaturation", + "value": 0 + }, + "1/768/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.RemainingTime", + "attribute_name": "RemainingTime", + "value": 0 + }, + "1/768/3": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 3, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentX", + "attribute_name": "CurrentX", + "value": 24939 + }, + "1/768/4": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 4, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CurrentY", + "attribute_name": "CurrentY", + "value": 24701 + }, + "1/768/7": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 7, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorTemperatureMireds", + "attribute_name": "ColorTemperatureMireds", + "value": 0 + }, + "1/768/8": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 8, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorMode", + "attribute_name": "ColorMode", + "value": 2 + }, + "1/768/15": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 15, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.Options", + "attribute_name": "Options", + "value": 0 + }, + "1/768/16": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.NumberOfPrimaries", + "attribute_name": "NumberOfPrimaries", + "value": 0 + }, + "1/768/16384": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16384, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.EnhancedCurrentHue", + "attribute_name": "EnhancedCurrentHue", + "value": 0 + }, + "1/768/16385": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16385, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.EnhancedColorMode", + "attribute_name": "EnhancedColorMode", + "value": 2 + }, + "1/768/16386": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16386, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopActive", + "attribute_name": "ColorLoopActive", + "value": 0 + }, + "1/768/16387": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16387, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopDirection", + "attribute_name": "ColorLoopDirection", + "value": 0 + }, + "1/768/16388": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16388, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopTime", + "attribute_name": "ColorLoopTime", + "value": 25 + }, + "1/768/16389": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16389, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopStartEnhancedHue", + "attribute_name": "ColorLoopStartEnhancedHue", + "value": 8960 + }, + "1/768/16390": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16390, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorLoopStoredEnhancedHue", + "attribute_name": "ColorLoopStoredEnhancedHue", + "value": 0 + }, + "1/768/16394": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16394, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorCapabilities", + "attribute_name": "ColorCapabilities", + "value": 31 + }, + "1/768/16395": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16395, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorTempPhysicalMinMireds", + "attribute_name": "ColorTempPhysicalMinMireds", + "value": 0 + }, + "1/768/16396": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16396, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ColorTempPhysicalMaxMireds", + "attribute_name": "ColorTempPhysicalMaxMireds", + "value": 65279 + }, + "1/768/16397": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16397, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.CoupleColorTempToLevelMinMireds", + "attribute_name": "CoupleColorTempToLevelMinMireds", + "value": 0 + }, + "1/768/16400": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 16400, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.StartUpColorTemperatureMireds", + "attribute_name": "StartUpColorTemperatureMireds", + "value": 0 + }, + "1/768/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 31 + }, + "1/768/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 5 + }, + "1/768/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/768/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 64, 65, 66, 67, 68, 71, 75, 76 + ] + }, + "1/768/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 768, + "cluster_type": "chip.clusters.Objects.ColorControl", + "cluster_name": "ColorControl", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.ColorControl.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [ + 0, 1, 2, 3, 4, 7, 8, 15, 16, 16384, 16385, 16386, 16387, 16388, 16389, + 16390, 16394, 16395, 16396, 16397, 16400, 65528, 65529, 65531, 65532, + 65533 + ] + }, + "1/1030/0": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 0, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.Occupancy", + "attribute_name": "Occupancy", + "value": 0 + }, + "1/1030/1": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 1, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.OccupancySensorType", + "attribute_name": "OccupancySensorType", + "value": 0 + }, + "1/1030/2": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 2, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.OccupancySensorTypeBitmap", + "attribute_name": "OccupancySensorTypeBitmap", + "value": 1 + }, + "1/1030/65532": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65532, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.FeatureMap", + "attribute_name": "FeatureMap", + "value": 0 + }, + "1/1030/65533": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65533, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.ClusterRevision", + "attribute_name": "ClusterRevision", + "value": 3 + }, + "1/1030/65528": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65528, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.GeneratedCommandList", + "attribute_name": "GeneratedCommandList", + "value": [] + }, + "1/1030/65529": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65529, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.AcceptedCommandList", + "attribute_name": "AcceptedCommandList", + "value": [] + }, + "1/1030/65531": { + "node_id": 5, + "endpoint": 1, + "cluster_id": 1030, + "cluster_type": "chip.clusters.Objects.OccupancySensing", + "cluster_name": "OccupancySensing", + "attribute_id": 65531, + "attribute_type": "chip.clusters.Objects.OccupancySensing.Attributes.AttributeList", + "attribute_name": "AttributeList", + "value": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + } + }, + "endpoints": [0, 1], + "root_device_type_instance": { + "__type": "", + "repr": "" + }, + "aggregator_device_type_instance": null, + "device_type_instances": [ + { + "__type": "", + "repr": "" + } + ], + "node_devices": [ + { + "__type": "", + "repr": "" + } + ] +} diff --git a/tests/components/matter/fixtures/nodes/dimmable-light.json b/tests/components/matter/fixtures/nodes/dimmable-light.json index 03067468f24..7449c600b3c 100644 --- a/tests/components/matter/fixtures/nodes/dimmable-light.json +++ b/tests/components/matter/fixtures/nodes/dimmable-light.json @@ -299,10 +299,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -310,10 +310,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -321,10 +321,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -332,10 +332,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock Dimmable Light" }, @@ -343,10 +343,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -354,10 +354,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "Mock Dimmable Light" }, @@ -365,10 +365,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -376,10 +376,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -387,10 +387,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -398,10 +398,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -409,10 +409,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -420,10 +420,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20200101" }, @@ -431,10 +431,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -442,10 +442,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -453,10 +453,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -464,10 +464,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -475,10 +475,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -486,10 +486,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -497,10 +497,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-dimmable-light" }, @@ -508,10 +508,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -522,10 +522,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -533,10 +533,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -544,10 +544,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -555,10 +555,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -566,10 +566,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/fixtures/nodes/flow-sensor.json b/tests/components/matter/fixtures/nodes/flow-sensor.json index 92f4c5b73b2..fbc39412245 100644 --- a/tests/components/matter/fixtures/nodes/flow-sensor.json +++ b/tests/components/matter/fixtures/nodes/flow-sensor.json @@ -115,10 +115,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -126,10 +126,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -137,10 +137,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -148,10 +148,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock FlowSensor" }, @@ -159,10 +159,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -170,10 +170,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "Mock Flow Sensor" }, @@ -181,10 +181,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -192,10 +192,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -203,10 +203,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -214,10 +214,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -225,10 +225,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -236,10 +236,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20221206" }, @@ -247,10 +247,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -258,10 +258,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -269,10 +269,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -280,10 +280,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -291,10 +291,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -302,10 +302,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -313,10 +313,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-flow-sensor" }, @@ -324,10 +324,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -338,10 +338,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -349,10 +349,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -360,10 +360,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -371,10 +371,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -382,10 +382,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/fixtures/nodes/humidity-sensor.json b/tests/components/matter/fixtures/nodes/humidity-sensor.json index 34b95263de5..0782d7cacfa 100644 --- a/tests/components/matter/fixtures/nodes/humidity-sensor.json +++ b/tests/components/matter/fixtures/nodes/humidity-sensor.json @@ -115,10 +115,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -126,10 +126,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -137,10 +137,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -148,10 +148,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock HumiditySensor" }, @@ -159,10 +159,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -170,10 +170,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "Mock Humidity Sensor" }, @@ -181,10 +181,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -192,10 +192,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -203,10 +203,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -214,10 +214,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -225,10 +225,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -236,10 +236,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20221206" }, @@ -247,10 +247,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -258,10 +258,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -269,10 +269,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -280,10 +280,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -291,10 +291,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -302,10 +302,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -313,10 +313,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-humidity-sensor" }, @@ -324,10 +324,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -338,10 +338,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -349,10 +349,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -360,10 +360,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -371,10 +371,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -382,10 +382,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/fixtures/nodes/light-sensor.json b/tests/components/matter/fixtures/nodes/light-sensor.json index 152292d4589..07dc8be2a8e 100644 --- a/tests/components/matter/fixtures/nodes/light-sensor.json +++ b/tests/components/matter/fixtures/nodes/light-sensor.json @@ -115,10 +115,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -126,10 +126,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -137,10 +137,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -148,10 +148,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock LightSensor" }, @@ -159,10 +159,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -170,10 +170,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "Mock Light Sensor" }, @@ -181,10 +181,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -192,10 +192,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -203,10 +203,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -214,10 +214,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -225,10 +225,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -236,10 +236,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20221206" }, @@ -247,10 +247,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -258,10 +258,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -269,10 +269,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -280,10 +280,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -291,10 +291,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -302,10 +302,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -313,10 +313,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-light-sensor" }, @@ -324,10 +324,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -338,10 +338,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -349,10 +349,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -360,10 +360,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -371,10 +371,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -382,10 +382,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/fixtures/nodes/occupancy-sensor.json b/tests/components/matter/fixtures/nodes/occupancy-sensor.json index 3e16b92f261..4d8a05a9c1b 100644 --- a/tests/components/matter/fixtures/nodes/occupancy-sensor.json +++ b/tests/components/matter/fixtures/nodes/occupancy-sensor.json @@ -115,10 +115,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -126,10 +126,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -137,10 +137,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -148,10 +148,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock OccupancySensor" }, @@ -159,10 +159,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -170,10 +170,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "Mock Occupancy Sensor" }, @@ -181,10 +181,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -192,10 +192,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -203,10 +203,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -214,10 +214,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -225,10 +225,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -236,10 +236,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20221206" }, @@ -247,10 +247,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -258,10 +258,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -269,10 +269,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -280,10 +280,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -291,10 +291,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -302,10 +302,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -313,10 +313,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-temperature-sensor" }, @@ -324,10 +324,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -338,10 +338,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -349,10 +349,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -360,10 +360,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -371,10 +371,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -382,10 +382,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json b/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json index e26450a9a28..60037588f67 100644 --- a/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json +++ b/tests/components/matter/fixtures/nodes/on-off-plugin-unit.json @@ -115,10 +115,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -126,10 +126,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -137,10 +137,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -148,10 +148,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock OnOffPluginUnit (powerplug/switch)" }, @@ -159,10 +159,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -170,10 +170,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "" }, @@ -181,10 +181,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -192,10 +192,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -203,10 +203,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -214,10 +214,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -225,10 +225,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -236,10 +236,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20221206" }, @@ -247,10 +247,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -258,10 +258,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -269,10 +269,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -280,10 +280,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -291,10 +291,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -302,10 +302,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -313,10 +313,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-onoff-plugin-unit" }, @@ -324,10 +324,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -338,10 +338,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -349,10 +349,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -360,10 +360,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -371,10 +371,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -382,10 +382,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/fixtures/nodes/onoff-light.json b/tests/components/matter/fixtures/nodes/onoff-light.json index 340d7cb71c9..86f294e0255 100644 --- a/tests/components/matter/fixtures/nodes/onoff-light.json +++ b/tests/components/matter/fixtures/nodes/onoff-light.json @@ -299,10 +299,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -310,10 +310,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -321,10 +321,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -332,10 +332,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock Light" }, @@ -343,10 +343,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -354,10 +354,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "Mock OnOff Light" }, @@ -365,10 +365,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -376,10 +376,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -387,10 +387,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -398,10 +398,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -409,10 +409,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -420,10 +420,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20200101" }, @@ -431,10 +431,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -442,10 +442,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -453,10 +453,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -464,10 +464,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "12345678" }, @@ -475,10 +475,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -486,10 +486,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -497,10 +497,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-onoff-light" }, @@ -508,10 +508,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -522,10 +522,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -533,10 +533,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -544,10 +544,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -555,10 +555,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -566,10 +566,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/fixtures/nodes/pressure-sensor.json b/tests/components/matter/fixtures/nodes/pressure-sensor.json index e338933fbc8..b2dd9964de6 100644 --- a/tests/components/matter/fixtures/nodes/pressure-sensor.json +++ b/tests/components/matter/fixtures/nodes/pressure-sensor.json @@ -115,10 +115,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -126,10 +126,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -137,10 +137,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -148,10 +148,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock PressureSensor" }, @@ -159,10 +159,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -170,10 +170,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "Mock Pressure Sensor" }, @@ -181,10 +181,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -192,10 +192,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -203,10 +203,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -214,10 +214,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -225,10 +225,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -236,10 +236,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20221206" }, @@ -247,10 +247,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -258,10 +258,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -269,10 +269,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -280,10 +280,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -291,10 +291,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -302,10 +302,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -313,10 +313,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-pressure-sensor" }, @@ -324,10 +324,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -338,10 +338,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -349,10 +349,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -360,10 +360,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -371,10 +371,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -382,10 +382,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/fixtures/nodes/temperature-sensor.json b/tests/components/matter/fixtures/nodes/temperature-sensor.json index 5e451f31fd2..cd288ea14f9 100644 --- a/tests/components/matter/fixtures/nodes/temperature-sensor.json +++ b/tests/components/matter/fixtures/nodes/temperature-sensor.json @@ -115,10 +115,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 0, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.DataModelRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.DataModelRevision", "attribute_name": "DataModelRevision", "value": 1 }, @@ -126,10 +126,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 1, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorName", "attribute_name": "VendorName", "value": "Nabu Casa" }, @@ -137,10 +137,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 2, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.VendorID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.VendorID", "attribute_name": "VendorID", "value": 65521 }, @@ -148,10 +148,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 3, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductName", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductName", "attribute_name": "ProductName", "value": "Mock PressureSensor" }, @@ -159,10 +159,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 4, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductID", "attribute_name": "ProductID", "value": 32768 }, @@ -170,10 +170,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 5, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.NodeLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.NodeLabel", "attribute_name": "NodeLabel", "value": "Mock Temperature Sensor" }, @@ -181,10 +181,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 6, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Location", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Location", "attribute_name": "Location", "value": "XX" }, @@ -192,10 +192,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 7, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersion", "attribute_name": "HardwareVersion", "value": 0 }, @@ -203,10 +203,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 8, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.HardwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.HardwareVersionString", "attribute_name": "HardwareVersionString", "value": "v1.0" }, @@ -214,10 +214,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 9, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersion", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersion", "attribute_name": "SoftwareVersion", "value": 1 }, @@ -225,10 +225,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 10, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SoftwareVersionString", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SoftwareVersionString", "attribute_name": "SoftwareVersionString", "value": "v1.0" }, @@ -236,10 +236,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 11, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ManufacturingDate", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ManufacturingDate", "attribute_name": "ManufacturingDate", "value": "20221206" }, @@ -247,10 +247,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 12, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.PartNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.PartNumber", "attribute_name": "PartNumber", "value": "" }, @@ -258,10 +258,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 13, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductURL", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductURL", "attribute_name": "ProductURL", "value": "" }, @@ -269,10 +269,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 14, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ProductLabel", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ProductLabel", "attribute_name": "ProductLabel", "value": "" }, @@ -280,10 +280,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 15, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.SerialNumber", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.SerialNumber", "attribute_name": "SerialNumber", "value": "TEST_SN" }, @@ -291,10 +291,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 16, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.LocalConfigDisabled", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.LocalConfigDisabled", "attribute_name": "LocalConfigDisabled", "value": false }, @@ -302,10 +302,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 17, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.Reachable", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.Reachable", "attribute_name": "Reachable", "value": true }, @@ -313,10 +313,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 18, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.UniqueID", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.UniqueID", "attribute_name": "UniqueID", "value": "mock-temperature-sensor" }, @@ -324,10 +324,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 19, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.CapabilityMinima", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.CapabilityMinima", "attribute_name": "CapabilityMinima", "value": { "caseSessionsPerFabric": 3, @@ -338,10 +338,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65532, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.FeatureMap", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.FeatureMap", "attribute_name": "FeatureMap", "value": 0 }, @@ -349,10 +349,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65533, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.ClusterRevision", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.ClusterRevision", "attribute_name": "ClusterRevision", "value": 1 }, @@ -360,10 +360,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65528, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.GeneratedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.GeneratedCommandList", "attribute_name": "GeneratedCommandList", "value": [] }, @@ -371,10 +371,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65529, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AcceptedCommandList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AcceptedCommandList", "attribute_name": "AcceptedCommandList", "value": [] }, @@ -382,10 +382,10 @@ "node_id": 1, "endpoint": 0, "cluster_id": 40, - "cluster_type": "chip.clusters.Objects.Basic", + "cluster_type": "chip.clusters.Objects.BasicInformation", "cluster_name": "Basic", "attribute_id": 65531, - "attribute_type": "chip.clusters.Objects.Basic.Attributes.AttributeList", + "attribute_type": "chip.clusters.Objects.BasicInformation.Attributes.AttributeList", "attribute_name": "AttributeList", "value": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, diff --git a/tests/components/matter/test_diagnostics.py b/tests/components/matter/test_diagnostics.py new file mode 100644 index 00000000000..537a569e823 --- /dev/null +++ b/tests/components/matter/test_diagnostics.py @@ -0,0 +1,112 @@ +"""Test the Matter diagnostics platform.""" +from __future__ import annotations + +from collections.abc import Awaitable, Callable +import json +from typing import Any +from unittest.mock import MagicMock + +from aiohttp import ClientSession +from matter_server.common.helpers.util import dataclass_from_dict +from matter_server.common.models.server_information import ServerDiagnostics +import pytest + +from homeassistant.components.matter.const import DOMAIN +from homeassistant.components.matter.diagnostics import redact_matter_attributes +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .common import setup_integration_with_node_fixture + +from tests.common import MockConfigEntry, load_fixture +from tests.components.diagnostics import ( + get_diagnostics_for_config_entry, + get_diagnostics_for_device, +) + + +@pytest.fixture(name="config_entry_diagnostics") +def config_entry_diagnostics_fixture() -> dict[str, Any]: + """Fixture for config entry diagnostics.""" + return json.loads(load_fixture("config_entry_diagnostics.json", DOMAIN)) + + +@pytest.fixture(name="config_entry_diagnostics_redacted") +def config_entry_diagnostics_redacted_fixture() -> dict[str, Any]: + """Fixture for redacted config entry diagnostics.""" + return json.loads(load_fixture("config_entry_diagnostics_redacted.json", DOMAIN)) + + +@pytest.fixture(name="device_diagnostics") +def device_diagnostics_fixture() -> dict[str, Any]: + """Fixture for device diagnostics.""" + return json.loads(load_fixture("nodes/device_diagnostics.json", DOMAIN)) + + +async def test_matter_attribute_redact(device_diagnostics: dict[str, Any]) -> None: + """Test the matter attribute redact helper.""" + assert device_diagnostics["attributes"]["0/40/6"]["value"] == "XX" + + redacted_device_diagnostics = redact_matter_attributes(device_diagnostics) + + # Check that the correct attribute value is redacted. + assert ( + redacted_device_diagnostics["attributes"]["0/40/6"]["value"] == "**REDACTED**" + ) + + # Check that the other attribute values are not redacted. + redacted_device_diagnostics["attributes"]["0/40/6"]["value"] = "XX" + assert redacted_device_diagnostics == device_diagnostics + + +async def test_config_entry_diagnostics( + hass: HomeAssistant, + hass_client: Callable[..., Awaitable[ClientSession]], + matter_client: MagicMock, + integration: MockConfigEntry, + config_entry_diagnostics: dict[str, Any], + config_entry_diagnostics_redacted: dict[str, Any], +) -> None: + """Test the config entry level diagnostics.""" + matter_client.get_diagnostics.return_value = dataclass_from_dict( + ServerDiagnostics, config_entry_diagnostics + ) + + diagnostics = await get_diagnostics_for_config_entry(hass, hass_client, integration) + + assert diagnostics == config_entry_diagnostics_redacted + + +async def test_device_diagnostics( + hass: HomeAssistant, + hass_client: Callable[..., Awaitable[ClientSession]], + matter_client: MagicMock, + config_entry_diagnostics: dict[str, Any], + device_diagnostics: dict[str, Any], +) -> None: + """Test the device diagnostics.""" + await setup_integration_with_node_fixture(hass, "device_diagnostics", matter_client) + system_info_dict = config_entry_diagnostics["info"] + device_diagnostics_redacted = { + "server_info": system_info_dict, + "node": redact_matter_attributes(device_diagnostics), + } + server_diagnostics_response = { + "info": system_info_dict, + "nodes": [device_diagnostics], + "events": [], + } + server_diagnostics = dataclass_from_dict( + ServerDiagnostics, server_diagnostics_response + ) + matter_client.get_diagnostics.return_value = server_diagnostics + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + dev_reg = dr.async_get(hass) + device = dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id)[0] + assert device + + diagnostics = await get_diagnostics_for_device( + hass, hass_client, config_entry, device + ) + + assert diagnostics == device_diagnostics_redacted diff --git a/tests/components/matter/test_helpers.py b/tests/components/matter/test_helpers.py new file mode 100644 index 00000000000..3da0a26b7ee --- /dev/null +++ b/tests/components/matter/test_helpers.py @@ -0,0 +1,22 @@ +"""Test the Matter helpers.""" +from __future__ import annotations + +from unittest.mock import MagicMock + +from homeassistant.components.matter.helpers import get_device_id +from homeassistant.core import HomeAssistant + +from .common import setup_integration_with_node_fixture + + +async def test_get_device_id( + hass: HomeAssistant, + matter_client: MagicMock, +) -> None: + """Test get_device_id.""" + node = await setup_integration_with_node_fixture( + hass, "device_diagnostics", matter_client + ) + device_id = get_device_id(matter_client.server_info, node.node_devices[0]) + + assert device_id == "00000000000004D2-0000000000000005-MatterNodeDevice" diff --git a/tests/components/mazda/test_sensor.py b/tests/components/mazda/test_sensor.py index 24ade7e0485..b200147f0ca 100644 --- a/tests/components/mazda/test_sensor.py +++ b/tests/components/mazda/test_sensor.py @@ -10,9 +10,8 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - LENGTH_KILOMETERS, - LENGTH_MILES, PERCENTAGE, + UnitOfLength, UnitOfPressure, ) from homeassistant.helpers import entity_registry as er @@ -49,7 +48,8 @@ async def test_sensors(hass): state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Fuel distance remaining" ) assert state.attributes.get(ATTR_ICON) == "mdi:gas-station" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DISTANCE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.KILOMETERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.state == "381" entry = entity_registry.async_get("sensor.my_mazda3_fuel_distance_remaining") @@ -61,7 +61,8 @@ async def test_sensors(hass): assert state assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Odometer" assert state.attributes.get(ATTR_ICON) == "mdi:speedometer" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DISTANCE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.KILOMETERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.TOTAL_INCREASING assert state.state == "2795" entry = entity_registry.async_get("sensor.my_mazda3_odometer") @@ -130,22 +131,26 @@ async def test_sensors(hass): assert entry.unique_id == "JM000000000000000_rear_right_tire_pressure" -async def test_sensors_imperial_units(hass): - """Test that the sensors work properly with imperial units.""" +async def test_sensors_us_customary_units(hass): + """Test that the sensors work properly with US customary units.""" hass.config.units = US_CUSTOMARY_SYSTEM await init_integration(hass) + # In the US, miles are used for vehicle odometers. + # These tests verify that the unit conversion logic for the distance + # sensor device class automatically converts the unit to miles. + # Fuel Distance Remaining state = hass.states.get("sensor.my_mazda3_fuel_distance_remaining") assert state - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.MILES assert state.state == "237" # Odometer state = hass.states.get("sensor.my_mazda3_odometer") assert state - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_MILES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.MILES assert state.state == "1737" @@ -181,7 +186,8 @@ async def test_electric_vehicle_sensors(hass): assert state assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Remaining range" assert state.attributes.get(ATTR_ICON) == "mdi:ev-station" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_KILOMETERS + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DISTANCE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfLength.KILOMETERS assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT assert state.state == "218" entry = entity_registry.async_get("sensor.my_mazda3_remaining_range") diff --git a/tests/components/melissa/test_climate.py b/tests/components/melissa/test_climate.py index 9eb2a9fda78..11bb663b372 100644 --- a/tests/components/melissa/test_climate.py +++ b/tests/components/melissa/test_climate.py @@ -11,7 +11,7 @@ from homeassistant.components.climate import ( ) from homeassistant.components.melissa import DATA_MELISSA, climate as melissa from homeassistant.components.melissa.climate import MelissaClimate -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS +from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from tests.common import load_fixture @@ -194,7 +194,7 @@ async def test_temperature_unit(hass): api = melissa_mock() device = (await api.async_fetch_devices())[_SERIAL] thermostat = MelissaClimate(api, _SERIAL, device) - assert thermostat.temperature_unit == TEMP_CELSIUS + assert thermostat.temperature_unit == UnitOfTemperature.CELSIUS async def test_min_temp(hass): diff --git a/tests/components/melnor/conftest.py b/tests/components/melnor/conftest.py index c06933b7404..2f3c765cfa8 100644 --- a/tests/components/melnor/conftest.py +++ b/tests/components/melnor/conftest.py @@ -1,11 +1,11 @@ """Tests for the melnor integration.""" - from __future__ import annotations from unittest.mock import AsyncMock, patch from bleak.backends.device import BLEDevice from melnor_bluetooth.device import Device +import pytest from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak from homeassistant.components.melnor.const import DOMAIN @@ -52,6 +52,11 @@ FAKE_SERVICE_INFO_2 = BluetoothServiceInfoBleak( ) +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" + + class MockedValve: """Mocked class for a Valve.""" diff --git a/tests/components/melnor/test_sensor.py b/tests/components/melnor/test_sensor.py index 778acbc96d9..ebe7d0c171f 100644 --- a/tests/components/melnor/test_sensor.py +++ b/tests/components/melnor/test_sensor.py @@ -47,7 +47,7 @@ async def test_minutes_remaining_sensor(hass): end_time = now + dt_util.dt.timedelta(minutes=10) # we control this mock - # pylint: disable=protected-access + device.zone1._end_time = (end_time).timestamp() with freeze_time(now), patch_async_ble_device_from_address(), patch_melnor_device( diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index ea67ab6f427..771d97a7327 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -9,7 +9,7 @@ import requests import homeassistant.components.mfi.sensor as mfi import homeassistant.components.sensor as sensor_component from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import UnitOfTemperature from homeassistant.setup import async_setup_component PLATFORM = mfi @@ -134,7 +134,7 @@ async def test_name(port, sensor): async def test_uom_temp(port, sensor): """Test the UOM temperature.""" port.tag = "temperature" - assert sensor.unit_of_measurement == TEMP_CELSIUS + assert sensor.unit_of_measurement == UnitOfTemperature.CELSIUS assert sensor.device_class is SensorDeviceClass.TEMPERATURE @@ -148,7 +148,7 @@ async def test_uom_power(port, sensor): async def test_uom_digital(port, sensor): """Test the UOM digital input.""" port.model = "Input Digital" - assert sensor.unit_of_measurement == "State" + assert sensor.unit_of_measurement is None assert sensor.device_class is None @@ -162,7 +162,7 @@ async def test_uom_unknown(port, sensor): async def test_uom_uninitialized(port, sensor): """Test that the UOM defaults if not initialized.""" type(port).tag = mock.PropertyMock(side_effect=ValueError) - assert sensor.unit_of_measurement == "State" + assert sensor.unit_of_measurement is None assert sensor.device_class is None diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py index c61585898de..4636f861673 100644 --- a/tests/components/mfi/test_switch.py +++ b/tests/components/mfi/test_switch.py @@ -74,13 +74,13 @@ async def test_update(port, switch): async def test_update_with_target_state(port, switch): """Test update with target state.""" - # pylint: disable=protected-access + switch._target_state = True port.data = {} port.data["output"] = "stale" switch.update() assert port.data["output"] == 1.0 - # pylint: disable=protected-access + assert switch._target_state is None port.data["output"] = "untouched" switch.update() @@ -92,7 +92,7 @@ async def test_turn_on(port, switch): switch.turn_on() assert port.control.call_count == 1 assert port.control.call_args == mock.call(True) - # pylint: disable=protected-access + assert switch._target_state @@ -101,5 +101,5 @@ async def test_turn_off(port, switch): switch.turn_off() assert port.control.call_count == 1 assert port.control.call_args == mock.call(False) - # pylint: disable=protected-access + assert not switch._target_state diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index 9ba043427b5..20969cfbc8f 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -13,8 +13,7 @@ from homeassistant.const import ( SERVICE_RELOAD, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.entity_registry as er @@ -345,17 +344,19 @@ async def test_different_unit_of_measurement(hass: HomeAssistant) -> None: entity_ids = config["sensor"]["entity_ids"] hass.states.async_set( - entity_ids[0], VALUES[0], {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + entity_ids[0], VALUES[0], {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) await hass.async_block_till_done() state = hass.states.get("sensor.test") assert str(float(VALUES[0])) == state.state - assert state.attributes.get("unit_of_measurement") == TEMP_CELSIUS + assert state.attributes.get("unit_of_measurement") == UnitOfTemperature.CELSIUS hass.states.async_set( - entity_ids[1], VALUES[1], {ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT} + entity_ids[1], + VALUES[1], + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT}, ) await hass.async_block_till_done() diff --git a/tests/components/mjpeg/test_config_flow.py b/tests/components/mjpeg/test_config_flow.py index 3d66af21f0a..c95f4f1c40f 100644 --- a/tests/components/mjpeg/test_config_flow.py +++ b/tests/components/mjpeg/test_config_flow.py @@ -37,7 +37,6 @@ async def test_full_user_flow( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -83,7 +82,6 @@ async def test_full_flow_with_authentication_error( assert result.get("type") == FlowResultType.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 @@ -101,7 +99,6 @@ async def test_full_flow_with_authentication_error( assert result2.get("type") == FlowResultType.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 @@ -145,7 +142,6 @@ async def test_connection_error( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result # Test connectione error on MJPEG url mock_mjpeg_requests.get( @@ -163,7 +159,6 @@ async def test_connection_error( assert result2.get("type") == FlowResultType.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 @@ -187,7 +182,6 @@ async def test_connection_error( assert result3.get("type") == FlowResultType.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 @@ -233,7 +227,6 @@ async def test_already_configured( 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"], @@ -257,7 +250,6 @@ async def test_options_flow( assert result.get("type") == FlowResultType.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") @@ -287,7 +279,6 @@ async def test_options_flow( assert result2.get("type") == FlowResultType.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 @@ -306,7 +297,6 @@ async def test_options_flow( assert result3.get("type") == FlowResultType.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 @@ -325,7 +315,6 @@ async def test_options_flow( assert result4.get("type") == FlowResultType.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 @@ -345,7 +334,6 @@ async def test_options_flow( assert result5.get("type") == FlowResultType.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 diff --git a/tests/components/mobile_app/conftest.py b/tests/components/mobile_app/conftest.py index 0455d4a8ea4..875fccea294 100644 --- a/tests/components/mobile_app/conftest.py +++ b/tests/components/mobile_app/conftest.py @@ -1,7 +1,7 @@ """Tests for mobile_app component.""" from http import HTTPStatus -# pylint: disable=redefined-outer-name,unused-import +# pylint: disable=unused-import import pytest from homeassistant.components.mobile_app.const import DOMAIN diff --git a/tests/components/mobile_app/test_sensor.py b/tests/components/mobile_app/test_sensor.py index 13e11db3eff..0e492b4dde6 100644 --- a/tests/components/mobile_app/test_sensor.py +++ b/tests/components/mobile_app/test_sensor.py @@ -9,8 +9,7 @@ from homeassistant.const import ( PERCENTAGE, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM @@ -19,8 +18,8 @@ from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM @pytest.mark.parametrize( "unit_system, state_unit, state1, state2", ( - (METRIC_SYSTEM, TEMP_CELSIUS, "100", "123"), - (US_CUSTOMARY_SYSTEM, TEMP_FAHRENHEIT, "212", "253"), + (METRIC_SYSTEM, UnitOfTemperature.CELSIUS, "100", "123"), + (US_CUSTOMARY_SYSTEM, UnitOfTemperature.FAHRENHEIT, "212", "253"), ), ) async def test_sensor( @@ -46,7 +45,7 @@ async def test_sensor( "entity_category": "diagnostic", "unique_id": "battery_temp", "state_class": "total", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, }, }, ) @@ -123,10 +122,22 @@ async def test_sensor( @pytest.mark.parametrize( "unique_id, unit_system, state_unit, state1, state2", ( - ("battery_temperature", METRIC_SYSTEM, TEMP_CELSIUS, "100", "123"), - ("battery_temperature", US_CUSTOMARY_SYSTEM, TEMP_FAHRENHEIT, "212", "253"), + ("battery_temperature", METRIC_SYSTEM, UnitOfTemperature.CELSIUS, "100", "123"), + ( + "battery_temperature", + US_CUSTOMARY_SYSTEM, + UnitOfTemperature.FAHRENHEIT, + "212", + "253", + ), # The unique_id doesn't match that of the mobile app's battery temperature sensor - ("battery_temp", US_CUSTOMARY_SYSTEM, TEMP_FAHRENHEIT, "212", "123"), + ( + "battery_temp", + US_CUSTOMARY_SYSTEM, + UnitOfTemperature.FAHRENHEIT, + "212", + "123", + ), ), ) async def test_sensor_migration( @@ -159,7 +170,7 @@ async def test_sensor_migration( "entity_category": "diagnostic", "unique_id": unique_id, "state_class": "total", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, }, }, ) diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 0794aab0fda..a6ab5797a11 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -22,6 +22,10 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from .const import CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE from tests.common import async_capture_events, async_mock_service +from tests.components.conversation.conftest import mock_agent + +# To avoid autoflake8 removing the import +mock_agent = mock_agent def encrypt_payload(secret_key, payload, encode_json=True): @@ -974,3 +978,122 @@ async def test_reregister_sensor(hass, create_registrations, webhook_client): assert reg_resp.status == HTTPStatus.CREATED entry = ent_reg.async_get("sensor.test_1_battery_state") assert entry.disabled_by is None + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "name": "New Name 2", + "state": 100, + "type": "sensor", + "unique_id": "abcd", + "state_class": None, + "device_class": None, + "entity_category": None, + "icon": None, + "unit_of_measurement": None, + }, + }, + ) + + assert reg_resp.status == HTTPStatus.CREATED + entry = ent_reg.async_get("sensor.test_1_battery_state") + assert entry.original_name == "Test 1 New Name 2" + assert entry.device_class is None + assert entry.unit_of_measurement is None + assert entry.entity_category is None + assert entry.original_icon is None + + +async def test_webhook_handle_conversation_process( + hass, create_registrations, webhook_client, mock_agent +): + """Test that we can converse.""" + webhook_client.server.app.router._frozen = False + + resp = await webhook_client.post( + "/api/webhook/{}".format(create_registrations[1]["webhook_id"]), + json={ + "type": "conversation_process", + "data": { + "text": "Turn the kitchen light off", + }, + }, + ) + + assert resp.status == HTTPStatus.OK + json = await resp.json() + assert json == { + "response": { + "response_type": "action_done", + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Test response", + } + }, + "language": hass.config.language, + "data": { + "targets": [], + "success": [], + "failed": [], + }, + }, + "conversation_id": None, + } + + +async def test_sending_sensor_state(hass, create_registrations, webhook_client, caplog): + """Test that we can register and send sensor state as number and None.""" + webhook_id = create_registrations[1]["webhook_id"] + webhook_url = f"/api/webhook/{webhook_id}" + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "register_sensor", + "data": { + "name": "Battery State", + "state": 100, + "type": "sensor", + "unique_id": "abcd", + }, + }, + ) + + assert reg_resp.status == HTTPStatus.CREATED + + ent_reg = er.async_get(hass) + entry = ent_reg.async_get("sensor.test_1_battery_state") + assert entry.original_name == "Test 1 Battery State" + assert entry.device_class is None + assert entry.unit_of_measurement is None + assert entry.entity_category is None + assert entry.original_icon == "mdi:cellphone" + assert entry.disabled_by is None + + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_1_battery_state") + assert state is not None + assert state.state == "100" + + reg_resp = await webhook_client.post( + webhook_url, + json={ + "type": "update_sensor_states", + "data": { + "state": 50.0000, + "type": "sensor", + "unique_id": "abcd", + }, + }, + ) + + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_1_battery_state") + assert state is not None + assert state.state == "50.0" diff --git a/tests/components/modbus/test_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py index 611f558cf1f..652ae7e74af 100644 --- a/tests/components/modbus/test_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -19,17 +19,20 @@ from homeassistant.const import ( CONF_NAME, CONF_SCAN_INTERVAL, CONF_SLAVE, + CONF_UNIQUE_ID, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import State +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") +SLAVE_UNIQUE_ID = "ground_floor_sensor" @pytest.mark.parametrize( @@ -341,31 +344,31 @@ async def test_config_slave_binary_sensor(hass, mock_modbus): "config_addon,register_words,expected, slaves", [ ( - {CONF_SLAVE_COUNT: 1}, + {CONF_SLAVE_COUNT: 1, CONF_UNIQUE_ID: SLAVE_UNIQUE_ID}, [False] * 8, STATE_OFF, [STATE_OFF], ), ( - {CONF_SLAVE_COUNT: 1}, + {CONF_SLAVE_COUNT: 1, CONF_UNIQUE_ID: SLAVE_UNIQUE_ID}, [True] + [False] * 7, STATE_ON, [STATE_OFF], ), ( - {CONF_SLAVE_COUNT: 1}, + {CONF_SLAVE_COUNT: 1, CONF_UNIQUE_ID: SLAVE_UNIQUE_ID}, [False, True] + [False] * 6, STATE_OFF, [STATE_ON], ), ( - {CONF_SLAVE_COUNT: 7}, + {CONF_SLAVE_COUNT: 7, CONF_UNIQUE_ID: SLAVE_UNIQUE_ID}, [True, False] * 4, STATE_ON, [STATE_OFF, STATE_ON] * 3 + [STATE_OFF], ), ( - {CONF_SLAVE_COUNT: 31}, + {CONF_SLAVE_COUNT: 31, CONF_UNIQUE_ID: SLAVE_UNIQUE_ID}, [True, False] * 16, STATE_ON, [STATE_OFF, STATE_ON] * 15 + [STATE_OFF], @@ -375,10 +378,14 @@ async def test_config_slave_binary_sensor(hass, mock_modbus): 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 + entity_registry = er.async_get(hass) - for i in range(len(slaves)): + for i, slave in enumerate(slaves): entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}_{i+1}".replace(" ", "_") - assert hass.states.get(entity_id).state == slaves[i] + assert hass.states.get(entity_id).state == slave + unique_id = f"{SLAVE_UNIQUE_ID}_{i+1}" + entry = entity_registry.async_get(entity_id) + assert entry.unique_id == unique_id async def test_no_discovery_info_binary_sensor(hass, caplog): diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 3b704eff161..2a212535916 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -862,3 +862,20 @@ async def test_integration_reload( async_fire_time_changed(hass) await hass.async_block_till_done() assert "Modbus reloading" in caplog.text + + +@pytest.mark.parametrize("do_config", [{}]) +async def test_integration_reload_failed(hass, caplog, mock_modbus) -> None: + """Run test for integration connect failure on reload.""" + caplog.set_level(logging.INFO) + caplog.clear() + + yaml_path = get_fixture_path("configuration.yaml", "modbus") + with mock.patch.object( + hass_config, "YAML_CONFIG_FILE", yaml_path + ), mock.patch.object(mock_modbus, "connect", side_effect=ModbusException("error")): + await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) + await hass.async_block_till_done() + + assert "Modbus reloading" in caplog.text + assert "connect failed, retry in pymodbus" in caplog.text diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index bb9e4285c42..4a6495d5b46 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -33,15 +33,18 @@ from homeassistant.const import ( CONF_SENSORS, CONF_SLAVE, CONF_STRUCTURE, + CONF_UNIQUE_ID, STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import State +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_NAME, ReadResult, do_next_cycle ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_") +SLAVE_UNIQUE_ID = "ground_floor_sensor" @pytest.mark.parametrize( @@ -573,6 +576,7 @@ async def test_all_sensor(hass, mock_do_cycle, expected): ( { CONF_SLAVE_COUNT: 0, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, }, [0x0102, 0x0304], False, @@ -581,6 +585,7 @@ async def test_all_sensor(hass, mock_do_cycle, expected): ( { CONF_SLAVE_COUNT: 1, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, }, [0x0102, 0x0304, 0x0403, 0x0201], False, @@ -589,6 +594,7 @@ async def test_all_sensor(hass, mock_do_cycle, expected): ( { CONF_SLAVE_COUNT: 3, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, }, [ 0x0102, @@ -611,6 +617,7 @@ async def test_all_sensor(hass, mock_do_cycle, expected): ( { CONF_SLAVE_COUNT: 1, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, }, [0x0102, 0x0304, 0x0403, 0x0201], True, @@ -619,6 +626,7 @@ async def test_all_sensor(hass, mock_do_cycle, expected): ( { CONF_SLAVE_COUNT: 1, + CONF_UNIQUE_ID: SLAVE_UNIQUE_ID, }, [], False, @@ -629,10 +637,14 @@ async def test_all_sensor(hass, mock_do_cycle, expected): async def test_slave_sensor(hass, mock_do_cycle, expected): """Run test for sensor.""" assert hass.states.get(ENTITY_ID).state == expected[0] + entity_registry = er.async_get(hass) for i in range(1, len(expected)): entity_id = f"{SENSOR_DOMAIN}.{TEST_ENTITY_NAME}_{i}".replace(" ", "_") assert hass.states.get(entity_id).state == expected[i] + unique_id = f"{SLAVE_UNIQUE_ID}_{i}" + entry = entity_registry.async_get(entity_id) + assert entry.unique_id == unique_id @pytest.mark.parametrize( diff --git a/tests/components/modern_forms/test_config_flow.py b/tests/components/modern_forms/test_config_flow.py index 05cebb2fef5..540a8fef93d 100644 --- a/tests/components/modern_forms/test_config_flow.py +++ b/tests/components/modern_forms/test_config_flow.py @@ -34,7 +34,6 @@ async def test_full_user_flow_implementation( assert result.get("step_id") == "user" assert result.get("type") == FlowResultType.FORM - assert "flow_id" in result with patch( "homeassistant.components.modern_forms.async_setup_entry", @@ -82,7 +81,6 @@ async def test_full_zeroconf_flow_implementation( assert result.get("description_placeholders") == {CONF_NAME: "example"} assert result.get("step_id") == "zeroconf_confirm" assert result.get("type") == FlowResultType.FORM - assert "flow_id" in result flow = flows[0] assert "context" in flow diff --git a/tests/components/mold_indicator/test_sensor.py b/tests/components/mold_indicator/test_sensor.py index 04305b724d8..7b6ac955b4a 100644 --- a/tests/components/mold_indicator/test_sensor.py +++ b/tests/components/mold_indicator/test_sensor.py @@ -10,7 +10,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.setup import async_setup_component @@ -19,10 +19,10 @@ from homeassistant.setup import async_setup_component def init_sensors_fixture(hass): """Set up things to be run when tests are started.""" hass.states.async_set( - "test.indoortemp", "20", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.indoortemp", "20", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) hass.states.async_set( - "test.outdoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.outdoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) hass.states.async_set( "test.indoorhumidity", "50", {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE} @@ -53,10 +53,10 @@ async def test_setup(hass): async def test_invalidcalib(hass): """Test invalid sensor values.""" hass.states.async_set( - "test.indoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.indoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) hass.states.async_set( - "test.outdoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.outdoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) hass.states.async_set( "test.indoorhumidity", "0", {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE} @@ -88,10 +88,10 @@ async def test_invalidcalib(hass): async def test_invalidhum(hass): """Test invalid sensor values.""" hass.states.async_set( - "test.indoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.indoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) hass.states.async_set( - "test.outdoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.outdoortemp", "10", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) hass.states.async_set( "test.indoorhumidity", "-1", {ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE} @@ -131,7 +131,9 @@ async def test_invalidhum(hass): assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None hass.states.async_set( - "test.indoorhumidity", "10", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.indoorhumidity", + "10", + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() moldind = hass.states.get("sensor.mold_indicator") @@ -199,7 +201,9 @@ async def test_unknown_sensor(hass): await hass.async_start() hass.states.async_set( - "test.indoortemp", STATE_UNKNOWN, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.indoortemp", + STATE_UNKNOWN, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() moldind = hass.states.get("sensor.mold_indicator") @@ -209,10 +213,12 @@ async def test_unknown_sensor(hass): assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None hass.states.async_set( - "test.indoortemp", "30", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.indoortemp", "30", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) hass.states.async_set( - "test.outdoortemp", STATE_UNKNOWN, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.outdoortemp", + STATE_UNKNOWN, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() moldind = hass.states.get("sensor.mold_indicator") @@ -222,7 +228,7 @@ async def test_unknown_sensor(hass): assert moldind.attributes.get(ATTR_CRITICAL_TEMP) is None hass.states.async_set( - "test.outdoortemp", "25", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.outdoortemp", "25", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) hass.states.async_set( "test.indoorhumidity", @@ -273,13 +279,13 @@ async def test_sensor_changed(hass): await hass.async_start() hass.states.async_set( - "test.indoortemp", "30", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.indoortemp", "30", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) await hass.async_block_till_done() assert hass.states.get("sensor.mold_indicator").state == "90" hass.states.async_set( - "test.outdoortemp", "25", {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.outdoortemp", "25", {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS} ) await hass.async_block_till_done() assert hass.states.get("sensor.mold_indicator").state == "57" diff --git a/tests/components/moon/test_config_flow.py b/tests/components/moon/test_config_flow.py index 9dfc186d492..2ef01b4f890 100644 --- a/tests/components/moon/test_config_flow.py +++ b/tests/components/moon/test_config_flow.py @@ -23,7 +23,6 @@ async def test_full_user_flow( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/mopeka/__init__.py b/tests/components/mopeka/__init__.py new file mode 100644 index 00000000000..389400cc511 --- /dev/null +++ b/tests/components/mopeka/__init__.py @@ -0,0 +1,35 @@ +"""Tests for the Mopeka integration.""" + + +from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo + +NOT_MOPEKA_SERVICE_INFO = BluetoothServiceInfo( + name="Not it", + address="aa:bb:cc:dd:ee:ff", + rssi=-63, + manufacturer_data={3234: b"\x00\x01"}, + service_data={}, + service_uuids=[], + source="local", +) + +PRO_SERVICE_INFO = BluetoothServiceInfo( + name="", + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + manufacturer_data={89: b"\x08rF\x00@\xe0\xf5\t\xf0\xd8"}, + service_data={}, + service_uuids=["0000fee5-0000-1000-8000-00805f9b34fb"], + source="local", +) + + +PRO_GOOD_SIGNAL_SERVICE_INFO = BluetoothServiceInfo( + name="", + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + manufacturer_data={89: b"\x08pC\xb6\xc3\xe0\xf5\t\xfa\xe3"}, + service_data={}, + service_uuids=["0000fee5-0000-1000-8000-00805f9b34fb"], + source="local", +) diff --git a/tests/components/mopeka/conftest.py b/tests/components/mopeka/conftest.py new file mode 100644 index 00000000000..1d6d0fc7eb7 --- /dev/null +++ b/tests/components/mopeka/conftest.py @@ -0,0 +1,8 @@ +"""Mopeka session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/mopeka/test_config_flow.py b/tests/components/mopeka/test_config_flow.py new file mode 100644 index 00000000000..fef8483d100 --- /dev/null +++ b/tests/components/mopeka/test_config_flow.py @@ -0,0 +1,192 @@ +"""Test the Mopeka config flow.""" + +from unittest.mock import patch + +from homeassistant import config_entries +from homeassistant.components.mopeka.const import DOMAIN +from homeassistant.data_entry_flow import FlowResultType + +from . import NOT_MOPEKA_SERVICE_INFO, PRO_SERVICE_INFO + +from tests.common import MockConfigEntry + + +async def test_async_step_bluetooth_valid_device(hass): + """Test discovery via bluetooth with a valid device.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=PRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch("homeassistant.components.mopeka.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Pro Plus EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_bluetooth_not_mopeka(hass): + """Test discovery via bluetooth not mopeka.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=NOT_MOPEKA_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "not_supported" + + +async def test_async_step_user_no_devices_found(hass): + """Test setup from service info cache with no devices found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_user_with_found_devices(hass): + """Test setup from service info cache with devices found.""" + with patch( + "homeassistant.components.mopeka.config_flow.async_discovered_service_info", + return_value=[PRO_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + with patch("homeassistant.components.mopeka.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Pro Plus EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + +async def test_async_step_user_device_added_between_steps(hass): + """Test the device gets added via another flow between steps.""" + with patch( + "homeassistant.components.mopeka.config_flow.async_discovered_service_info", + return_value=[PRO_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch("homeassistant.components.mopeka.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_async_step_user_with_found_devices_already_setup(hass): + """Test setup from service info cache with devices found.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.mopeka.config_flow.async_discovered_service_info", + return_value=[PRO_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "no_devices_found" + + +async def test_async_step_bluetooth_devices_already_setup(hass): + """Test we can't start a flow if there is already a config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=PRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_async_step_bluetooth_already_in_progress(hass): + """Test we can't start a flow for the same device twice.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=PRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=PRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_in_progress" + + +async def test_async_step_user_takes_precedence_over_discovery(hass): + """Test manual setup takes precedence over discovery.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=PRO_SERVICE_INFO, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + + with patch( + "homeassistant.components.mopeka.config_flow.async_discovered_service_info", + return_value=[PRO_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] == FlowResultType.FORM + + with patch("homeassistant.components.mopeka.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Pro Plus EEFF" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + # Verify the original one was aborted + assert not hass.config_entries.flow.async_progress(DOMAIN) diff --git a/tests/components/mopeka/test_sensor.py b/tests/components/mopeka/test_sensor.py new file mode 100644 index 00000000000..27704ec38ed --- /dev/null +++ b/tests/components/mopeka/test_sensor.py @@ -0,0 +1,85 @@ +"""Test the Mopeka sensors.""" + + +from homeassistant.components.mopeka.const import DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ( + ATTR_FRIENDLY_NAME, + ATTR_UNIT_OF_MEASUREMENT, + STATE_UNKNOWN, + UnitOfLength, + UnitOfTemperature, +) + +from . import PRO_GOOD_SIGNAL_SERVICE_INFO, PRO_SERVICE_INFO + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +async def test_sensors_bad_signal(hass): + """Test setting up creates the sensors when there is bad signal.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 0 + inject_bluetooth_service_info(hass, PRO_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 6 + + temp_sensor = hass.states.get("sensor.pro_plus_eeff_temperature") + temp_sensor_attrs = temp_sensor.attributes + assert temp_sensor.state == "30" + assert temp_sensor_attrs[ATTR_FRIENDLY_NAME] == "Pro Plus EEFF Temperature" + assert temp_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + assert temp_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + tank_sensor = hass.states.get("sensor.pro_plus_eeff_tank_level") + tank_sensor_attrs = tank_sensor.attributes + assert tank_sensor.state == STATE_UNKNOWN + assert tank_sensor_attrs[ATTR_FRIENDLY_NAME] == "Pro Plus EEFF Tank Level" + assert tank_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == UnitOfLength.MILLIMETERS + assert tank_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_sensors_good_signal(hass): + """Test setting up creates the sensors when there is good signal.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="aa:bb:cc:dd:ee:ff", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all("sensor")) == 0 + inject_bluetooth_service_info(hass, PRO_GOOD_SIGNAL_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all("sensor")) == 6 + + temp_sensor = hass.states.get("sensor.pro_plus_eeff_temperature") + temp_sensor_attrs = temp_sensor.attributes + assert temp_sensor.state == "27" + assert temp_sensor_attrs[ATTR_FRIENDLY_NAME] == "Pro Plus EEFF Temperature" + assert temp_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + assert temp_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + tank_sensor = hass.states.get("sensor.pro_plus_eeff_tank_level") + tank_sensor_attrs = tank_sensor.attributes + assert tank_sensor.state == "341" + assert tank_sensor_attrs[ATTR_FRIENDLY_NAME] == "Pro Plus EEFF Tank Level" + assert tank_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == UnitOfLength.MILLIMETERS + assert tank_sensor_attrs[ATTR_STATE_CLASS] == "measurement" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index d70405cd297..8cc64b8ff00 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -164,6 +164,7 @@ async def test_setup_camera_new_data_camera_removed(hass: HomeAssistant) -> None client.async_get_cameras = AsyncMock(return_value={KEY_CAMERAS: []}) async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() + await hass.async_block_till_done() assert not hass.states.get(TEST_CAMERA_ENTITY_ID) assert not device_registry.async_get_device({TEST_CAMERA_DEVICE_IDENTIFIER}) assert not device_registry.async_get_device({(DOMAIN, old_device_id)}) diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index edb987e5664..a7bf537add6 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -86,12 +86,10 @@ async def test_hassio_success(hass: HomeAssistant) -> None: assert result.get("type") == data_entry_flow.FlowResultType.FORM assert result.get("step_id") == "hassio_confirm" assert result.get("description_placeholders") == {"addon": "motionEye"} - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result2.get("type") == data_entry_flow.FlowResultType.FORM assert result2.get("step_id") == "user" - assert "flow_id" in result2 mock_client = create_mock_motioneye_client() @@ -423,7 +421,6 @@ async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result2.get("type") == data_entry_flow.FlowResultType.FORM - assert "flow_id" in result2 mock_client = create_mock_motioneye_client() diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 91607de9343..ca8e5c441f9 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -455,7 +455,7 @@ async def test_setting_sensor_value_via_mqtt_message_and_template_and_raw_state_ async def test_setting_sensor_value_via_mqtt_message_empty_template( - hass, mqtt_mock_entry_with_yaml_config, caplog + hass, mqtt_mock_entry_with_yaml_config ): """Test the setting of the value via MQTT.""" assert await async_setup_component( @@ -482,7 +482,6 @@ async def test_setting_sensor_value_via_mqtt_message_empty_template( async_fire_mqtt_message(hass, "test-topic", "DEF") state = hass.states.get("binary_sensor.test") assert state.state == STATE_UNKNOWN - assert "Empty template output" in caplog.text async_fire_mqtt_message(hass, "test-topic", "ABC") state = hass.states.get("binary_sensor.test") @@ -1060,13 +1059,6 @@ async def test_cleanup_triggers_and_restoring_state( await help_test_reload_with_config( hass, caplog, tmp_path, {mqtt.DOMAIN: {domain: [config1, config2]}} ) - assert "Clean up expire after trigger for binary_sensor.test1" in caplog.text - assert "Clean up expire after trigger for binary_sensor.test2" not in caplog.text - assert ( - "State recovered after reload for binary_sensor.test1, remaining time before expiring" - in caplog.text - ) - assert "State recovered after reload for binary_sensor.test2" not in caplog.text state = hass.states.get("binary_sensor.test1") assert state.state == state1 @@ -1084,7 +1076,7 @@ async def test_cleanup_triggers_and_restoring_state( async def test_skip_restoring_state_with_over_due_expire_trigger( - hass, mqtt_mock_entry_with_yaml_config, caplog, freezer + hass, mqtt_mock_entry_with_yaml_config, freezer ): """Test restoring a state with over due expire timer.""" @@ -1107,7 +1099,8 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() - assert "Skip state recovery after reload for binary_sensor.test3" in caplog.text + state = hass.states.get("binary_sensor.test3") + assert state.state == STATE_UNAVAILABLE async def test_setup_manual_entity_from_yaml(hass): diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index dd86c41dcc7..cdf85ae5f76 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -9,13 +9,17 @@ import voluptuous as vol from homeassistant.components import climate, mqtt from homeassistant.components.climate import ( ATTR_AUX_HEAT, + ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, + ATTR_HUMIDITY, ATTR_HVAC_ACTION, ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + DEFAULT_MAX_HUMIDITY, DEFAULT_MAX_TEMP, + DEFAULT_MIN_HUMIDITY, DEFAULT_MIN_TEMP, PRESET_ECO, ClimateEntityFeature, @@ -67,6 +71,7 @@ DEFAULT_CONFIG = { climate.DOMAIN: { "name": "test", "mode_command_topic": "mode-topic", + "target_humidity_command_topic": "humidity-topic", "temperature_command_topic": "temperature-topic", "temperature_low_command_topic": "temperature-low-topic", "temperature_high_command_topic": "temperature-high-topic", @@ -108,6 +113,8 @@ async def test_setup_params(hass, mqtt_mock_entry_with_yaml_config): assert state.state == "off" assert state.attributes.get("min_temp") == DEFAULT_MIN_TEMP assert state.attributes.get("max_temp") == DEFAULT_MAX_TEMP + assert state.attributes.get("min_humidity") == DEFAULT_MIN_HUMIDITY + assert state.attributes.get("max_humidity") == DEFAULT_MAX_HUMIDITY async def test_preset_none_in_preset_modes(hass, caplog): @@ -156,6 +163,7 @@ async def test_supported_features(hass, mqtt_mock_entry_with_yaml_config): | ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.AUX_HEAT | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + | ClimateEntityFeature.TARGET_HUMIDITY ) assert state.attributes.get("supported_features") == support @@ -240,6 +248,34 @@ async def test_set_operation_pessimistic(hass, mqtt_mock_entry_with_yaml_config) assert state.state == "cool" +async def test_set_operation_optimistic(hass, mqtt_mock_entry_with_yaml_config): + """Test setting operation mode in optimistic mode.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["mode_state_topic"] = "mode-state" + config["climate"]["optimistic"] = True + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "off" + + await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "cool" + + async_fire_mqtt_message(hass, "mode-state", "heat") + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "heat" + + async_fire_mqtt_message(hass, "mode-state", "bogus mode") + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "heat" + + +# CONF_POWER_COMMAND_TOPIC, CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE are deprecated, +# support for CONF_POWER_STATE_TOPIC and CONF_POWER_STATE_TEMPLATE was already removed or never added +# support was deprecated with release 2023.2 and will be removed with release 2023.8 async def test_set_operation_with_power_command(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new operation mode with power command enabled.""" config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) @@ -308,6 +344,31 @@ async def test_set_fan_mode_pessimistic(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("fan_mode") == "high" +async def test_set_fan_mode_optimistic(hass, mqtt_mock_entry_with_yaml_config): + """Test setting of new fan mode in optimistic mode.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["fan_mode_state_topic"] = "fan-state" + config["climate"]["optimistic"] = True + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("fan_mode") == "low" + + await common.async_set_fan_mode(hass, "high", ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("fan_mode") == "high" + + async_fire_mqtt_message(hass, "fan-state", "low") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("fan_mode") == "low" + + async_fire_mqtt_message(hass, "fan-state", "bogus mode") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("fan_mode") == "low" + + async def test_set_fan_mode(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new fan mode.""" assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) @@ -363,6 +424,31 @@ async def test_set_swing_pessimistic(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("swing_mode") == "on" +async def test_set_swing_optimistic(hass, mqtt_mock_entry_with_yaml_config): + """Test setting swing mode in optimistic mode.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["swing_mode_state_topic"] = "swing-state" + config["climate"]["optimistic"] = True + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("swing_mode") == "off" + + await common.async_set_swing_mode(hass, "on", ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("swing_mode") == "on" + + async_fire_mqtt_message(hass, "swing-state", "off") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("swing_mode") == "off" + + async_fire_mqtt_message(hass, "swing-state", "bogus state") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("swing_mode") == "off" + + async def test_set_swing(hass, mqtt_mock_entry_with_yaml_config): """Test setting of new swing mode.""" assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) @@ -414,6 +500,21 @@ async def test_set_target_temperature(hass, mqtt_mock_entry_with_yaml_config): mqtt_mock.async_publish.reset_mock() +async def test_set_target_humidity(hass, mqtt_mock_entry_with_yaml_config): + """Test setting the target humidity.""" + assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") is None + await common.async_set_humidity(hass, humidity=82, entity_id=ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") == 82 + mqtt_mock.async_publish.assert_called_once_with("humidity-topic", "82", 0, False) + mqtt_mock.async_publish.reset_mock() + + async def test_set_target_temperature_pessimistic( hass, mqtt_mock_entry_with_yaml_config ): @@ -440,6 +541,33 @@ async def test_set_target_temperature_pessimistic( assert state.attributes.get("temperature") == 1701 +async def test_set_target_temperature_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): + """Test setting the target temperature optimistic.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["temperature_state_topic"] = "temperature-state" + config["climate"]["optimistic"] = True + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("temperature") == 21 + await common.async_set_hvac_mode(hass, "heat", ENTITY_CLIMATE) + await common.async_set_temperature(hass, temperature=17, entity_id=ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("temperature") == 17 + + async_fire_mqtt_message(hass, "temperature-state", "18") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("temperature") == 18 + + async_fire_mqtt_message(hass, "temperature-state", "not a number") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("temperature") == 18 + + async def test_set_target_temperature_low_high(hass, mqtt_mock_entry_with_yaml_config): """Test setting the low/high target temperature.""" assert await async_setup_component(hass, mqtt.DOMAIN, DEFAULT_CONFIG) @@ -496,6 +624,94 @@ async def test_set_target_temperature_low_highpessimistic( assert state.attributes.get("target_temp_high") == 1703 +async def test_set_target_temperature_low_high_optimistic( + hass, mqtt_mock_entry_with_yaml_config +): + """Test setting the low/high target temperature optimistic.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["optimistic"] = True + config["climate"]["temperature_low_state_topic"] = "temperature-low-state" + config["climate"]["temperature_high_state_topic"] = "temperature-high-state" + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_low") == 21 + assert state.attributes.get("target_temp_high") == 21 + await common.async_set_temperature( + hass, target_temp_low=20, target_temp_high=23, entity_id=ENTITY_CLIMATE + ) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_low") == 20 + assert state.attributes.get("target_temp_high") == 23 + + async_fire_mqtt_message(hass, "temperature-low-state", "15") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_low") == 15 + assert state.attributes.get("target_temp_high") == 23 + + async_fire_mqtt_message(hass, "temperature-high-state", "25") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_low") == 15 + assert state.attributes.get("target_temp_high") == 25 + + async_fire_mqtt_message(hass, "temperature-low-state", "not a number") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_low") == 15 + + async_fire_mqtt_message(hass, "temperature-high-state", "not a number") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_high") == 25 + + +async def test_set_target_humidity_optimistic(hass, mqtt_mock_entry_with_yaml_config): + """Test setting the target humidity optimistic.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["target_humidity_state_topic"] = "humidity-state" + config["climate"]["optimistic"] = True + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") is None + await common.async_set_humidity(hass, humidity=52, entity_id=ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") == 52 + + async_fire_mqtt_message(hass, "humidity-state", "53") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") == 53 + + async_fire_mqtt_message(hass, "humidity-state", "not a number") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") == 53 + + +async def test_set_target_humidity_pessimistic(hass, mqtt_mock_entry_with_yaml_config): + """Test setting the target humidity.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["target_humidity_state_topic"] = "humidity-state" + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") is None + await common.async_set_humidity(hass, humidity=50, entity_id=ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") is None + + async_fire_mqtt_message(hass, "humidity-state", "80") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") == 80 + + async_fire_mqtt_message(hass, "humidity-state", "not a number") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") == 80 + + async def test_receive_mqtt_temperature(hass, mqtt_mock_entry_with_yaml_config): """Test getting the current temperature via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) @@ -509,6 +725,36 @@ async def test_receive_mqtt_temperature(hass, mqtt_mock_entry_with_yaml_config): assert state.attributes.get("current_temperature") == 47 +async def test_receive_mqtt_humidity(hass, mqtt_mock_entry_with_yaml_config): + """Test getting the current humidity via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["current_humidity_topic"] = "current_humidity" + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + async_fire_mqtt_message(hass, "current_humidity", "35") + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("current_humidity") == 35 + + +async def test_handle_target_humidity_received(hass, mqtt_mock_entry_with_yaml_config): + """Test setting the target humidity via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["target_humidity_state_topic"] = "humidity-state" + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") is None + + async_fire_mqtt_message(hass, "humidity-state", "65") + + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") == 65 + + async def test_handle_action_received(hass, mqtt_mock_entry_with_yaml_config): """Test getting the action received via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) @@ -580,6 +826,56 @@ async def test_set_preset_mode_optimistic( assert "'invalid' is not a valid preset mode" in caplog.text +async def test_set_preset_mode_explicit_optimistic( + hass, mqtt_mock_entry_with_yaml_config, caplog +): + """Test setting of the preset mode.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["optimistic"] = True + config["climate"]["preset_mode_state_topic"] = "preset-mode-state" + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() + + 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_entry_with_yaml_config, caplog ): @@ -736,7 +1032,8 @@ async def test_get_target_temperature_low_high_with_templates( async_fire_mqtt_message(hass, "temperature-state", '"-INVALID-"') state = hass.states.get(ENTITY_CLIMATE) # make sure, the invalid value gets logged... - assert "Could not parse temperature from" in caplog.text + assert "Could not parse temperature_low_state_template from" in caplog.text + assert "Could not parse temperature_high_state_template from" in caplog.text # ... but the actual value stays unchanged. assert state.attributes.get("target_temp_low") == 1031 assert state.attributes.get("target_temp_high") == 1032 @@ -758,8 +1055,10 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog 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"]["target_humidity_state_topic"] = "humidity-state" config["climate"]["aux_state_topic"] = "aux-state" config["climate"]["current_temperature_topic"] = "current-temperature" + config["climate"]["current_humidity_topic"] = "current-humidity" config["climate"]["preset_mode_state_topic"] = "current-preset-mode" assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() @@ -793,10 +1092,26 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog async_fire_mqtt_message(hass, "temperature-state", '"-INVALID-"') state = hass.states.get(ENTITY_CLIMATE) # make sure, the invalid value gets logged... - assert "Could not parse temperature from -INVALID-" in caplog.text + assert "Could not parse temperature_state_template from -INVALID-" in caplog.text # ... but the actual value stays unchanged. assert state.attributes.get("temperature") == 1031 + # Humidity - with valid value + assert state.attributes.get("humidity") is None + async_fire_mqtt_message(hass, "humidity-state", '"82"') + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") == 82 + + # Humidity - with invalid value + async_fire_mqtt_message(hass, "humidity-state", '"-INVALID-"') + state = hass.states.get(ENTITY_CLIMATE) + # make sure, the invalid value gets logged... + assert ( + "Could not parse target_humidity_state_template from -INVALID-" in caplog.text + ) + # ... but the actual value stays unchanged. + assert state.attributes.get("humidity") == 82 + # Preset Mode assert state.attributes.get("preset_mode") == "none" async_fire_mqtt_message(hass, "current-preset-mode", '{"attribute": "eco"}') @@ -807,7 +1122,6 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog hass, "current-preset-mode", '{"other_attribute": "some_value"}' ) state = hass.states.get(ENTITY_CLIMATE) - assert "Ignoring empty preset_mode from 'current-preset-mode'" assert state.attributes.get("preset_mode") == "eco" # Aux mode @@ -826,6 +1140,11 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("current_temperature") == 74656 + # Current humidity + async_fire_mqtt_message(hass, "current-humidity", '"35"') + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("current_humidity") == 35 + # Action async_fire_mqtt_message(hass, "action", '"cooling"') state = hass.states.get(ENTITY_CLIMATE) @@ -835,10 +1154,6 @@ async def test_get_with_templates(hass, mqtt_mock_entry_with_yaml_config, caplog async_fire_mqtt_message(hass, "action", "null") state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get("hvac_action") == "cooling" - assert ( - "Invalid ['cooling', 'drying', 'fan', 'heating', 'idle', 'off'] action: None, ignoring" - in caplog.text - ) async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog): @@ -852,6 +1167,7 @@ async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog) config["climate"]["temperature_command_template"] = "temp: {{ value }}" config["climate"]["temperature_high_command_template"] = "temp_hi: {{ value }}" config["climate"]["temperature_low_command_template"] = "temp_lo: {{ value }}" + config["climate"]["target_humidity_command_template"] = "humidity: {{ value }}" assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) await hass.async_block_till_done() @@ -918,6 +1234,15 @@ async def test_set_and_templates(hass, mqtt_mock_entry_with_yaml_config, caplog) assert state.attributes.get("target_temp_low") == 20 assert state.attributes.get("target_temp_high") == 23 + # Humidity + await common.async_set_humidity(hass, humidity=82, entity_id=ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "humidity-topic", "humidity: 82", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("humidity") == 82 + async def test_min_temp_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom min temp.""" @@ -951,6 +1276,38 @@ async def test_max_temp_custom(hass, mqtt_mock_entry_with_yaml_config): assert max_temp == 60 +async def test_min_humidity_custom(hass, mqtt_mock_entry_with_yaml_config): + """Test a custom min humidity.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["min_humidity"] = 42 + + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get(ENTITY_CLIMATE) + min_humidity = state.attributes.get("min_humidity") + + assert isinstance(min_humidity, float) + assert state.attributes.get("min_humidity") == 42 + + +async def test_max_humidity_custom(hass, mqtt_mock_entry_with_yaml_config): + """Test a custom max humidity.""" + config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) + config["climate"]["max_humidity"] = 58 + + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get(ENTITY_CLIMATE) + max_humidity = state.attributes.get("max_humidity") + + assert isinstance(max_humidity, float) + assert max_humidity == 58 + + async def test_temp_step_custom(hass, mqtt_mock_entry_with_yaml_config): """Test a custom temp step.""" config = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN]) @@ -1056,14 +1413,14 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): climate.DOMAIN: [ { "name": "Test 1", - "power_state_topic": "test-topic", - "power_command_topic": "test_topic", + "mode_state_topic": "test_topic1/state", + "mode_command_topic": "test_topic1/command", "unique_id": "TOTALLY_UNIQUE", }, { "name": "Test 2", - "power_state_topic": "test-topic", - "power_command_topic": "test_topic", + "mode_state_topic": "test_topic2/state", + "mode_command_topic": "test_topic2/command", "unique_id": "TOTALLY_UNIQUE", }, ] @@ -1081,6 +1438,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): ("action_topic", "cooling", ATTR_HVAC_ACTION, "cooling"), ("aux_state_topic", "ON", ATTR_AUX_HEAT, "on"), ("current_temperature_topic", "22.1", ATTR_CURRENT_TEMPERATURE, 22.1), + ("current_humidity_topic", "60.4", ATTR_CURRENT_HUMIDITY, 60.4), ("fan_mode_state_topic", "low", ATTR_FAN_MODE, "low"), ("mode_state_topic", "cool", None, None), ("mode_state_topic", "fan_only", None, None), @@ -1088,6 +1446,7 @@ async def test_unique_id(hass, mqtt_mock_entry_with_yaml_config): ("temperature_low_state_topic", "19.1", ATTR_TARGET_TEMP_LOW, 19.1), ("temperature_high_state_topic", "22.9", ATTR_TARGET_TEMP_HIGH, 22.9), ("temperature_state_topic", "19.9", ATTR_TEMPERATURE, 19.9), + ("target_humidity_state_topic", "82.6", ATTR_HUMIDITY, 82.6), ], ) async def test_encoding_subscribable_topics( @@ -1357,6 +1716,13 @@ async def test_precision_whole(hass, mqtt_mock_entry_with_yaml_config): 29.8, "temperature_high_command_template", ), + ( + climate.SERVICE_SET_HUMIDITY, + "target_humidity_command_topic", + {"humidity": "82"}, + 82, + "target_humidity_command_template", + ), ], ) async def test_publishing_with_custom_encoding( @@ -1390,6 +1756,62 @@ async def test_publishing_with_custom_encoding( ) +@pytest.mark.parametrize( + "config,valid", + [ + ( + { + "name": "test_valid_humidity_min_max", + "min_humidity": 20, + "max_humidity": 80, + }, + True, + ), + ( + { + "name": "test_invalid_humidity_min_max_1", + "min_humidity": 0, + "max_humidity": 101, + }, + False, + ), + ( + { + "name": "test_invalid_humidity_min_max_2", + "max_humidity": 20, + "min_humidity": 40, + }, + False, + ), + ( + { + "name": "test_valid_humidity_state", + "target_humidity_state_topic": "humidity-state", + "target_humidity_command_topic": "humidity-command", + }, + True, + ), + ( + { + "name": "test_invalid_humidity_state", + "target_humidity_state_topic": "humidity-state", + }, + False, + ), + ], +) +async def test_humidity_configuration_validity(hass, config, valid): + """Test the validity of humidity configurations.""" + assert ( + await async_setup_component( + hass, + mqtt.DOMAIN, + {mqtt.DOMAIN: {climate.DOMAIN: config}}, + ) + is valid + ) + + async def test_reloadable(hass, mqtt_mock_entry_with_yaml_config, caplog, tmp_path): """Test reloading the MQTT platform.""" domain = climate.DOMAIN diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 9a5a01ec3fd..f0a22987aa3 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -1673,8 +1673,6 @@ async def help_test_reload_with_config(hass, caplog, tmp_path, config): ) await hass.async_block_till_done() - assert "" in caplog.text - async def help_test_entry_reload_with_new_config(hass, tmp_path, new_config): """Test reloading with supplied config.""" diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 818cdcf33a6..5cccb341218 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -454,93 +454,90 @@ async def test_hassio_cannot_connect( assert len(mock_finish_setup.mock_calls) == 0 -@patch( - "homeassistant.config.async_hass_config_yaml", - AsyncMock(return_value={}), -) async def test_option_flow( hass, mqtt_mock_entry_no_yaml_config, mock_try_connection, - caplog, ): """Test config flow options.""" - mqtt_mock = await mqtt_mock_entry_no_yaml_config() - mock_try_connection.return_value = True - config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] - config_entry.data = { - mqtt.CONF_BROKER: "test-broker", - mqtt.CONF_PORT: 1234, - } + with patch( + "homeassistant.config.async_hass_config_yaml", AsyncMock(return_value={}) + ) as yaml_mock: + mqtt_mock = await mqtt_mock_entry_no_yaml_config() + mock_try_connection.return_value = True + config_entry = hass.config_entries.async_entries(mqtt.DOMAIN)[0] + config_entry.data = { + mqtt.CONF_BROKER: "test-broker", + mqtt.CONF_PORT: 1234, + } - mqtt_mock.async_connect.reset_mock() + mqtt_mock.async_connect.reset_mock() - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "broker" + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "broker" - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + mqtt.CONF_BROKER: "another-broker", + mqtt.CONF_PORT: 2345, + mqtt.CONF_USERNAME: "user", + mqtt.CONF_PASSWORD: "pass", + }, + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "options" + + await hass.async_block_till_done() + assert mqtt_mock.async_connect.call_count == 0 + + yaml_mock.reset_mock() + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + mqtt.CONF_DISCOVERY: True, + "discovery_prefix": "homeassistant", + "birth_enable": True, + "birth_topic": "ha_state/online", + "birth_payload": "online", + "birth_qos": 1, + "birth_retain": True, + "will_enable": True, + "will_topic": "ha_state/offline", + "will_payload": "offline", + "will_qos": 2, + "will_retain": True, + }, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == {} + assert config_entry.data == { mqtt.CONF_BROKER: "another-broker", mqtt.CONF_PORT: 2345, mqtt.CONF_USERNAME: "user", mqtt.CONF_PASSWORD: "pass", - }, - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "options" - - await hass.async_block_till_done() - assert mqtt_mock.async_connect.call_count == 0 - - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ mqtt.CONF_DISCOVERY: True, - "discovery_prefix": "homeassistant", - "birth_enable": True, - "birth_topic": "ha_state/online", - "birth_payload": "online", - "birth_qos": 1, - "birth_retain": True, - "will_enable": True, - "will_topic": "ha_state/offline", - "will_payload": "offline", - "will_qos": 2, - "will_retain": True, - }, - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["data"] == {} - assert config_entry.data == { - mqtt.CONF_BROKER: "another-broker", - mqtt.CONF_PORT: 2345, - mqtt.CONF_USERNAME: "user", - mqtt.CONF_PASSWORD: "pass", - mqtt.CONF_DISCOVERY: True, - mqtt.CONF_DISCOVERY_PREFIX: "homeassistant", - mqtt.CONF_BIRTH_MESSAGE: { - mqtt.ATTR_TOPIC: "ha_state/online", - mqtt.ATTR_PAYLOAD: "online", - mqtt.ATTR_QOS: 1, - mqtt.ATTR_RETAIN: True, - }, - mqtt.CONF_WILL_MESSAGE: { - mqtt.ATTR_TOPIC: "ha_state/offline", - mqtt.ATTR_PAYLOAD: "offline", - mqtt.ATTR_QOS: 2, - mqtt.ATTR_RETAIN: True, - }, - } + mqtt.CONF_DISCOVERY_PREFIX: "homeassistant", + mqtt.CONF_BIRTH_MESSAGE: { + mqtt.ATTR_TOPIC: "ha_state/online", + mqtt.ATTR_PAYLOAD: "online", + mqtt.ATTR_QOS: 1, + mqtt.ATTR_RETAIN: True, + }, + mqtt.CONF_WILL_MESSAGE: { + mqtt.ATTR_TOPIC: "ha_state/offline", + mqtt.ATTR_PAYLOAD: "offline", + mqtt.ATTR_QOS: 2, + mqtt.ATTR_RETAIN: True, + }, + } - await hass.async_block_till_done() - assert config_entry.title == "another-broker" + await hass.async_block_till_done() + assert config_entry.title == "another-broker" # assert that the entry was reloaded with the new config - assert ( - "" - in caplog.text - ) + assert yaml_mock.await_count @pytest.mark.parametrize( diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 89a56903c3b..251c0af24a6 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -6,6 +6,7 @@ import re from unittest.mock import AsyncMock, call, patch import pytest +from voluptuous import MultipleInvalid from homeassistant import config_entries from homeassistant.components import mqtt @@ -1494,7 +1495,7 @@ async def test_clean_up_registry_monitoring( async def test_unique_id_collission_has_priority( hass, mqtt_mock_entry_no_yaml_config, entity_reg ): - """Test tehe unique_id collision detection has priority over registry disabled items.""" + """Test the unique_id collision detection has priority over registry disabled items.""" await mqtt_mock_entry_no_yaml_config() config = { "name": "sbfspot_12345", @@ -1534,3 +1535,57 @@ async def test_unique_id_collission_has_priority( assert entity_reg.async_get("sensor.sbfspot_12345_1") is not None # Verify the second entity is not created because it is not unique assert entity_reg.async_get("sensor.sbfspot_12345_2") is None + + +@pytest.mark.xfail(raises=MultipleInvalid) +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) +async def test_update_with_bad_config_not_breaks_discovery( + hass: ha.HomeAssistant, mqtt_mock_entry_no_yaml_config, entity_reg +) -> None: + """Test a bad update does not break discovery.""" + await mqtt_mock_entry_no_yaml_config() + # discover a sensor + config1 = { + "name": "sbfspot_12345", + "state_topic": "homeassistant_test/sensor/sbfspot_0/state", + } + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/config", + json.dumps(config1), + ) + await hass.async_block_till_done() + assert hass.states.get("sensor.sbfspot_12345") is not None + # update with a breaking config + config2 = { + "name": "sbfspot_12345", + "availability": 1, + "state_topic": "homeassistant_test/sensor/sbfspot_0/state", + } + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/config", + json.dumps(config2), + ) + await hass.async_block_till_done() + # update the state topic + config3 = { + "name": "sbfspot_12345", + "state_topic": "homeassistant_test/sensor/sbfspot_0/new_state_topic", + } + async_fire_mqtt_message( + hass, + "homeassistant/sensor/sbfspot_0/config", + json.dumps(config3), + ) + await hass.async_block_till_done() + + # Send an update for the state + async_fire_mqtt_message( + hass, + "homeassistant_test/sensor/sbfspot_0/new_state_topic", + "new_value", + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.sbfspot_12345").state == "new_value" diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index f4e89aa1ceb..d0ffc87141c 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -415,7 +415,7 @@ async def test_controlling_state_via_topic_and_json_message( assert state.attributes.get(fan.ATTR_PERCENTAGE) is None async_fire_mqtt_message(hass, "percentage-state-topic", '{"otherval": 100}') - assert "Ignoring empty speed from" in caplog.text + assert state.attributes.get(fan.ATTR_PERCENTAGE) is None caplog.clear() async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "low"}') @@ -439,8 +439,7 @@ async def test_controlling_state_via_topic_and_json_message( assert state.attributes.get("preset_mode") is None async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"otherval": 100}') - assert "Ignoring empty preset_mode from" in caplog.text - caplog.clear() + assert state.attributes.get("preset_mode") is None async def test_controlling_state_via_topic_and_json_message_shared_topic( @@ -528,9 +527,6 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( state = hass.states.get("fan.test") assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100 assert state.attributes.get("preset_mode") == "auto" - assert "Ignoring empty preset_mode from" in caplog.text - assert "Ignoring empty state from" in caplog.text - assert "Ignoring empty oscillation from" in caplog.text caplog.clear() diff --git a/tests/components/mqtt/test_humidifier.py b/tests/components/mqtt/test_humidifier.py index 1b8eac397cf..c5ffdc0df8f 100644 --- a/tests/components/mqtt/test_humidifier.py +++ b/tests/components/mqtt/test_humidifier.py @@ -301,7 +301,7 @@ async def test_controlling_state_via_topic_and_json_message( assert state.attributes.get(humidifier.ATTR_HUMIDITY) is None async_fire_mqtt_message(hass, "humidity-state-topic", '{"otherval": 100}') - assert "Ignoring empty target humidity from" in caplog.text + assert state.attributes.get(humidifier.ATTR_HUMIDITY) is None caplog.clear() async_fire_mqtt_message(hass, "mode-state-topic", '{"val": "low"}') @@ -325,7 +325,7 @@ async def test_controlling_state_via_topic_and_json_message( assert state.attributes.get(humidifier.ATTR_MODE) is None async_fire_mqtt_message(hass, "mode-state-topic", '{"otherval": 100}') - assert "Ignoring empty mode from" in caplog.text + assert state.attributes.get(humidifier.ATTR_MODE) is None caplog.clear() async_fire_mqtt_message(hass, "state-topic", '{"val": null}') @@ -407,8 +407,6 @@ async def test_controlling_state_via_topic_and_json_message_shared_topic( state = hass.states.get("humidifier.test") assert state.attributes.get(humidifier.ATTR_HUMIDITY) == 100 assert state.attributes.get(humidifier.ATTR_MODE) == "auto" - assert "Ignoring empty mode from" in caplog.text - assert "Ignoring empty state from" in caplog.text caplog.clear() diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 38849167959..219e11cfdfe 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -21,8 +21,8 @@ from homeassistant.const import ( ATTR_ASSUMED_STATE, EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, - TEMP_CELSIUS, Platform, + UnitOfTemperature, ) import homeassistant.core as ha from homeassistant.core import CoreState, HomeAssistant, callback @@ -816,7 +816,7 @@ async def test_all_subscriptions_run_when_decode_fails( await mqtt.async_subscribe(hass, "test-topic", record_calls, encoding="ascii") await mqtt.async_subscribe(hass, "test-topic", record_calls) - async_fire_mqtt_message(hass, "test-topic", TEMP_CELSIUS) + async_fire_mqtt_message(hass, "test-topic", UnitOfTemperature.CELSIUS) await hass.async_block_till_done() assert len(calls) == 1 @@ -1383,12 +1383,8 @@ async def test_handle_mqtt_on_callback( 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 + # Since the mid event was already set, we should not see any timeout warning in the log 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 @@ -1425,18 +1421,26 @@ async def test_subscribe_error( async def test_handle_message_callback( - hass, caplog, mqtt_mock_entry_no_yaml_config, mqtt_client_mock + hass, mqtt_mock_entry_no_yaml_config, mqtt_client_mock ): """Test for handling an incoming message callback.""" + callbacks = [] + + def _callback(args): + callbacks.append(args) + await mqtt_mock_entry_no_yaml_config() msg = ReceiveMessage("some-topic", b"test-payload", 1, False) mqtt_client_mock.on_connect(mqtt_client_mock, None, None, 0) - await mqtt.async_subscribe(hass, "some-topic", lambda *args: 0) + await mqtt.async_subscribe(hass, "some-topic", _callback) 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 (qos=1): b'test-payload'" in caplog.text + assert len(callbacks) == 1 + assert callbacks[0].topic == "some-topic" + assert callbacks[0].qos == 1 + assert callbacks[0].payload == "test-payload" async def test_setup_override_configuration(hass, caplog, tmp_path): diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 2404d8a0f1f..0577bdf4ea4 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -493,32 +493,26 @@ async def test_invalid_state_via_topic(hass, mqtt_mock_entry_with_yaml_config, c assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message(hass, "test_light_rgb/status", "") - assert "Ignoring empty state message" in caplog.text light_state = hass.states.get("light.test") assert state.state == STATE_ON async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "") - assert "Ignoring empty brightness message" in caplog.text light_state = hass.states.get("light.test") assert light_state.attributes["brightness"] == 255 async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "") - assert "Ignoring empty color mode message" in caplog.text light_state = hass.states.get("light.test") - assert light_state.attributes["effect"] == "none" + assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "") - assert "Ignoring empty effect message" in caplog.text light_state = hass.states.get("light.test") assert light_state.attributes["effect"] == "none" async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "") - assert "Ignoring empty rgb message" in caplog.text light_state = hass.states.get("light.test") assert light_state.attributes.get("rgb_color") == (255, 255, 255) async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "") - assert "Ignoring empty hs message" in caplog.text light_state = hass.states.get("light.test") assert light_state.attributes.get("hs_color") == (0, 0) @@ -528,21 +522,18 @@ async def test_invalid_state_via_topic(hass, mqtt_mock_entry_with_yaml_config, c assert light_state.attributes.get("hs_color") == (0, 0) async_fire_mqtt_message(hass, "test_light_rgb/xy/status", "") - assert "Ignoring empty xy-color message" in caplog.text light_state = hass.states.get("light.test") assert light_state.attributes.get("xy_color") == (0.323, 0.329) async_fire_mqtt_message(hass, "test_light_rgb/rgbw/status", "255,255,255,1") async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgbw") async_fire_mqtt_message(hass, "test_light_rgb/rgbw/status", "") - assert "Ignoring empty rgbw message" in caplog.text light_state = hass.states.get("light.test") assert light_state.attributes.get("rgbw_color") == (255, 255, 255, 1) async_fire_mqtt_message(hass, "test_light_rgb/rgbww/status", "255,255,255,1,2") async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgbww") async_fire_mqtt_message(hass, "test_light_rgb/rgbww/status", "") - assert "Ignoring empty rgbww message" in caplog.text light_state = hass.states.get("light.test") assert light_state.attributes.get("rgbww_color") == (255, 255, 255, 1, 2) @@ -559,7 +550,6 @@ async def test_invalid_state_via_topic(hass, mqtt_mock_entry_with_yaml_config, c assert state.attributes.get("xy_color") == (0.326, 0.333) async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "") - assert "Ignoring empty color temp message" in caplog.text light_state = hass.states.get("light.test") assert light_state.attributes["color_temp"] == 153 @@ -2871,18 +2861,18 @@ async def test_max_mireds(hass, mqtt_mock_entry_with_yaml_config): "hs_command_topic", {"rgb_color": [255, 128, 0]}, "30.118,100.0", - None, - None, - None, + "hs_command_template", + "hue", + b"3", ), ( light.SERVICE_TURN_ON, "xy_command_topic", {"hs_color": [30.118, 100.0]}, "0.611,0.375", - None, - None, - None, + "xy_command_template", + "x * 10", + b"6", ), ( light.SERVICE_TURN_OFF, @@ -3123,6 +3113,78 @@ async def test_sending_mqtt_effect_command_with_template( assert state.attributes.get("effect") == "colorloop" +async def test_sending_mqtt_hs_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): + """Test the sending of HS Color command with template.""" + config = { + light.DOMAIN: { + "name": "test", + "command_topic": "test_light_hs/set", + "hs_command_topic": "test_light_hs/hs_color/set", + "hs_command_template": '{"hue": {{ hue | int }}, "sat": {{ sat | int}}}', + "qos": 0, + } + } + + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get("light.test") + assert state.state == STATE_UNKNOWN + + await common.async_turn_on(hass, "light.test", hs_color=(30, 100)) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_hs/set", "ON", 0, False), + call("test_light_hs/hs_color/set", '{"hue": 30, "sat": 100}', 0, False), + ], + any_order=True, + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes["hs_color"] == (30, 100) + + +async def test_sending_mqtt_xy_command_with_template( + hass, mqtt_mock_entry_with_yaml_config +): + """Test the sending of XY Color command with template.""" + config = { + light.DOMAIN: { + "name": "test", + "command_topic": "test_light_xy/set", + "xy_command_topic": "test_light_xy/xy_color/set", + "xy_command_template": '{"Color": "{{ (x * 65536) | round | int }},{{ (y * 65536) | round | int }}"}', + "qos": 0, + } + } + + assert await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: config}) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get("light.test") + assert state.state == STATE_UNKNOWN + + await common.async_turn_on(hass, "light.test", xy_color=(0.151, 0.343)) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_xy/set", "ON", 0, False), + call("test_light_xy/xy_color/set", '{"Color": "9896,22479"}', 0, False), + ], + any_order=True, + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes["xy_color"] == (0.151, 0.343) + + async def test_setup_manual_entity_from_yaml(hass): """Test setup manual configured MQTT entity.""" platform = light.DOMAIN diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index ef1690221aa..b7d4122319c 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -8,17 +8,22 @@ from homeassistant.components.lock import ( SERVICE_LOCK, SERVICE_OPEN, SERVICE_UNLOCK, + STATE_JAMMED, STATE_LOCKED, + STATE_LOCKING, STATE_UNLOCKED, + STATE_UNLOCKING, LockEntityFeature, ) from homeassistant.components.mqtt.lock import MQTT_LOCK_ATTRIBUTES_BLOCKED from homeassistant.const import ( ATTR_ASSUMED_STATE, + ATTR_CODE, ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, Platform, ) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from .test_common import ( @@ -65,7 +70,18 @@ def lock_platform_only(): yield -async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_config): +@pytest.mark.parametrize( + "payload,lock_state", + [ + ("LOCKED", STATE_LOCKED), + ("LOCKING", STATE_LOCKING), + ("UNLOCKED", STATE_UNLOCKED), + ("UNLOCKING", STATE_UNLOCKING), + ], +) +async def test_controlling_state_via_topic( + hass, mqtt_mock_entry_with_yaml_config, payload, lock_state +): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -79,7 +95,9 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi "payload_lock": "LOCK", "payload_unlock": "UNLOCK", "state_locked": "LOCKED", + "state_locking": "LOCKING", "state_unlocked": "UNLOCKED", + "state_unlocking": "UNLOCKING", } } }, @@ -92,19 +110,23 @@ async def test_controlling_state_via_topic(hass, mqtt_mock_entry_with_yaml_confi assert not state.attributes.get(ATTR_ASSUMED_STATE) assert not state.attributes.get(ATTR_SUPPORTED_FEATURES) - async_fire_mqtt_message(hass, "state-topic", "LOCKED") + async_fire_mqtt_message(hass, "state-topic", payload) state = hass.states.get("lock.test") - assert state.state is STATE_LOCKED - - async_fire_mqtt_message(hass, "state-topic", "UNLOCKED") - - state = hass.states.get("lock.test") - assert state.state is STATE_UNLOCKED + assert state.state is lock_state +@pytest.mark.parametrize( + "payload,lock_state", + [ + ("closed", STATE_LOCKED), + ("closing", STATE_LOCKING), + ("open", STATE_UNLOCKED), + ("opening", STATE_UNLOCKING), + ], +) async def test_controlling_non_default_state_via_topic( - hass, mqtt_mock_entry_with_yaml_config + hass, mqtt_mock_entry_with_yaml_config, payload, lock_state ): """Test the controlling state via topic.""" assert await async_setup_component( @@ -119,7 +141,9 @@ async def test_controlling_non_default_state_via_topic( "payload_lock": "LOCK", "payload_unlock": "UNLOCK", "state_locked": "closed", + "state_locking": "closing", "state_unlocked": "open", + "state_unlocking": "opening", } } }, @@ -131,19 +155,23 @@ async def test_controlling_non_default_state_via_topic( assert state.state is STATE_UNLOCKED assert not state.attributes.get(ATTR_ASSUMED_STATE) - async_fire_mqtt_message(hass, "state-topic", "closed") + async_fire_mqtt_message(hass, "state-topic", payload) state = hass.states.get("lock.test") - assert state.state is STATE_LOCKED - - async_fire_mqtt_message(hass, "state-topic", "open") - - state = hass.states.get("lock.test") - assert state.state is STATE_UNLOCKED + assert state.state is lock_state +@pytest.mark.parametrize( + "payload,lock_state", + [ + ('{"val":"LOCKED"}', STATE_LOCKED), + ('{"val":"LOCKING"}', STATE_LOCKING), + ('{"val":"UNLOCKED"}', STATE_UNLOCKED), + ('{"val":"UNLOCKING"}', STATE_UNLOCKING), + ], +) async def test_controlling_state_via_topic_and_json_message( - hass, mqtt_mock_entry_with_yaml_config + hass, mqtt_mock_entry_with_yaml_config, payload, lock_state ): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( @@ -158,7 +186,9 @@ async def test_controlling_state_via_topic_and_json_message( "payload_lock": "LOCK", "payload_unlock": "UNLOCK", "state_locked": "LOCKED", + "state_locking": "LOCKING", "state_unlocked": "UNLOCKED", + "state_unlocking": "UNLOCKING", "value_template": "{{ value_json.val }}", } } @@ -170,19 +200,23 @@ async def test_controlling_state_via_topic_and_json_message( state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED - async_fire_mqtt_message(hass, "state-topic", '{"val":"LOCKED"}') + async_fire_mqtt_message(hass, "state-topic", payload) state = hass.states.get("lock.test") - assert state.state is STATE_LOCKED - - async_fire_mqtt_message(hass, "state-topic", '{"val":"UNLOCKED"}') - - state = hass.states.get("lock.test") - assert state.state is STATE_UNLOCKED + assert state.state is lock_state +@pytest.mark.parametrize( + "payload,lock_state", + [ + ('{"val":"closed"}', STATE_LOCKED), + ('{"val":"closing"}', STATE_LOCKING), + ('{"val":"open"}', STATE_UNLOCKED), + ('{"val":"opening"}', STATE_UNLOCKING), + ], +) async def test_controlling_non_default_state_via_topic_and_json_message( - hass, mqtt_mock_entry_with_yaml_config + hass, mqtt_mock_entry_with_yaml_config, payload, lock_state ): """Test the controlling state via topic and JSON message.""" assert await async_setup_component( @@ -197,7 +231,9 @@ async def test_controlling_non_default_state_via_topic_and_json_message( "payload_lock": "LOCK", "payload_unlock": "UNLOCK", "state_locked": "closed", + "state_locking": "closing", "state_unlocked": "open", + "state_unlocking": "opening", "value_template": "{{ value_json.val }}", } } @@ -209,15 +245,10 @@ async def test_controlling_non_default_state_via_topic_and_json_message( state = hass.states.get("lock.test") assert state.state is STATE_UNLOCKED - async_fire_mqtt_message(hass, "state-topic", '{"val":"closed"}') + async_fire_mqtt_message(hass, "state-topic", payload) state = hass.states.get("lock.test") - assert state.state is STATE_LOCKED - - async_fire_mqtt_message(hass, "state-topic", '{"val":"open"}') - - state = hass.states.get("lock.test") - assert state.state is STATE_UNLOCKED + assert state.state is lock_state async def test_sending_mqtt_commands_and_optimistic( @@ -268,6 +299,67 @@ async def test_sending_mqtt_commands_and_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) +async def test_sending_mqtt_commands_with_template( + hass, mqtt_mock_entry_with_yaml_config +): + """Test sending commands with template.""" + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "code_format": "^\\d{4}$", + "command_topic": "command-topic", + "command_template": '{ "{{ value }}": "{{ code }}" }', + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "payload_open": "OPEN", + "state_locked": "LOCKED", + "state_unlocked": "UNLOCKED", + } + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await hass.services.async_call( + lock.DOMAIN, + SERVICE_LOCK, + {ATTR_ENTITY_ID: "lock.test", ATTR_CODE: "1234"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", '{ "LOCK": "1234" }', 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("lock.test") + assert state.state is STATE_LOCKED + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await hass.services.async_call( + lock.DOMAIN, + SERVICE_UNLOCK, + {ATTR_ENTITY_ID: "lock.test", ATTR_CODE: "1234"}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "command-topic", '{ "UNLOCK": "1234" }', 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + assert state.attributes.get(ATTR_ASSUMED_STATE) + + async def test_sending_mqtt_commands_and_explicit_optimistic( hass, mqtt_mock_entry_with_yaml_config ): @@ -440,6 +532,112 @@ async def test_sending_mqtt_commands_support_open_and_explicit_optimistic( assert state.attributes.get(ATTR_ASSUMED_STATE) +async def test_sending_mqtt_commands_pessimistic( + hass: HomeAssistant, mqtt_mock_entry_with_yaml_config +) -> None: + """Test function of the lock with state topics.""" + assert await async_setup_component( + hass, + mqtt.DOMAIN, + { + mqtt.DOMAIN: { + lock.DOMAIN: { + "name": "test", + "command_topic": "command-topic", + "state_topic": "state-topic", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "payload_open": "OPEN", + "state_locked": "LOCKED", + "state_locking": "LOCKING", + "state_unlocked": "UNLOCKED", + "state_unlocking": "UNLOCKING", + "state_jammed": "JAMMED", + } + } + }, + ) + await hass.async_block_till_done() + mqtt_mock = await mqtt_mock_entry_with_yaml_config() + + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == LockEntityFeature.OPEN + + # send lock command to lock + await hass.services.async_call( + lock.DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + ) + + mqtt_mock.async_publish.assert_called_once_with("command-topic", "LOCK", 0, False) + mqtt_mock.async_publish.reset_mock() + + # receive state from lock + async_fire_mqtt_message(hass, "state-topic", "LOCKED") + await hass.async_block_till_done() + + state = hass.states.get("lock.test") + assert state.state is STATE_LOCKED + + await hass.services.async_call( + lock.DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + ) + + mqtt_mock.async_publish.assert_called_once_with("command-topic", "UNLOCK", 0, False) + mqtt_mock.async_publish.reset_mock() + + # receive state from lock + async_fire_mqtt_message(hass, "state-topic", "UNLOCKED") + await hass.async_block_till_done() + + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + + await hass.services.async_call( + lock.DOMAIN, SERVICE_OPEN, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + ) + + mqtt_mock.async_publish.assert_called_once_with("command-topic", "OPEN", 0, False) + mqtt_mock.async_publish.reset_mock() + + # receive state from lock + async_fire_mqtt_message(hass, "state-topic", "UNLOCKED") + await hass.async_block_till_done() + + state = hass.states.get("lock.test") + assert state.state is STATE_UNLOCKED + + # send lock command to lock + await hass.services.async_call( + lock.DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: "lock.test"}, blocking=True + ) + + # Go to locking state + mqtt_mock.async_publish.assert_called_once_with("command-topic", "LOCK", 0, False) + mqtt_mock.async_publish.reset_mock() + + # receive locking state from lock + async_fire_mqtt_message(hass, "state-topic", "LOCKING") + await hass.async_block_till_done() + + state = hass.states.get("lock.test") + assert state.state is STATE_LOCKING + + # receive jammed state from lock + async_fire_mqtt_message(hass, "state-topic", "JAMMED") + await hass.async_block_till_done() + + state = hass.states.get("lock.test") + assert state.state is STATE_JAMMED + + # receive solved state from lock + async_fire_mqtt_message(hass, "state-topic", "LOCKED") + await hass.async_block_till_done() + + state = hass.states.get("lock.test") + assert state.state is STATE_LOCKED + + async def test_availability_when_connection_lost( hass, mqtt_mock_entry_with_yaml_config ): diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index e547e8207a7..4eee11a58b4 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -25,8 +25,8 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_MODE, ATTR_UNIT_OF_MEASUREMENT, - TEMP_FAHRENHEIT, Platform, + UnitOfTemperature, ) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -88,7 +88,7 @@ async def test_run_number_setup(hass, mqtt_mock_entry_with_yaml_config): "command_topic": topic, "name": "Test Number", "device_class": "temperature", - "unit_of_measurement": TEMP_FAHRENHEIT, + "unit_of_measurement": UnitOfTemperature.FAHRENHEIT, "payload_reset": "reset!", } } @@ -190,7 +190,7 @@ async def test_restore_native_value(hass, mqtt_mock_entry_with_yaml_config): number.DOMAIN: { "command_topic": topic, "device_class": "temperature", - "unit_of_measurement": TEMP_FAHRENHEIT, + "unit_of_measurement": UnitOfTemperature.FAHRENHEIT, "name": "Test Number", } } diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 3b586380fa0..1262643253b 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -12,9 +12,8 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, Platform, + UnitOfTemperature, ) import homeassistant.core as ha from homeassistant.helpers import device_registry as dr @@ -293,7 +292,6 @@ async def test_setting_sensor_value_via_mqtt_json_message( sensor.DOMAIN: { "name": "test", "state_topic": "test-topic", - "unit_of_measurement": "fav unit", "value_template": "{{ value_json.val }}", } } @@ -326,7 +324,6 @@ async def test_setting_sensor_value_via_mqtt_json_message_and_default_current_st sensor.DOMAIN: { "name": "test", "state_topic": "test-topic", - "unit_of_measurement": "fav unit", "value_template": "{{ value_json.val | is_defined }}-{{ value_json.par }}", } } @@ -410,7 +407,7 @@ async def test_setting_sensor_bad_last_reset_via_mqtt_message( async def test_setting_sensor_empty_last_reset_via_mqtt_message( - hass, caplog, mqtt_mock_entry_with_yaml_config + hass, mqtt_mock_entry_with_yaml_config ): """Test the setting of the last_reset property via MQTT.""" assert await async_setup_component( @@ -434,7 +431,6 @@ async def test_setting_sensor_empty_last_reset_via_mqtt_message( async_fire_mqtt_message(hass, "last-reset-topic", "") state = hass.states.get("sensor.test") assert state.attributes.get("last_reset") is None - assert "Ignoring empty last_reset message" in caplog.text async def test_setting_sensor_last_reset_via_mqtt_json_message( @@ -1128,14 +1124,14 @@ async def test_cleanup_triggers_and_restoring_state( config1["expire_after"] = 30 config1["state_topic"] = "test-topic1" config1["device_class"] = "temperature" - config1["unit_of_measurement"] = TEMP_FAHRENHEIT + config1["unit_of_measurement"] = UnitOfTemperature.FAHRENHEIT.value config2 = copy.deepcopy(DEFAULT_CONFIG[mqtt.DOMAIN][domain]) config2["name"] = "test2" config2["expire_after"] = 5 config2["state_topic"] = "test-topic2" config2["device_class"] = "temperature" - config2["unit_of_measurement"] = TEMP_CELSIUS + config2["unit_of_measurement"] = UnitOfTemperature.CELSIUS.value freezer.move_to("2022-02-02 12:01:00+01:00") @@ -1161,14 +1157,6 @@ async def test_cleanup_triggers_and_restoring_state( ) await hass.async_block_till_done() - assert "Clean up expire after trigger for sensor.test1" in caplog.text - assert "Clean up expire after trigger for sensor.test2" not in caplog.text - assert ( - "State recovered after reload for sensor.test1, remaining time before expiring" - in caplog.text - ) - assert "State recovered after reload for sensor.test2" not in caplog.text - state = hass.states.get("sensor.test1") assert state.state == "38" # 100 °F -> 38 °C @@ -1185,7 +1173,7 @@ async def test_cleanup_triggers_and_restoring_state( async def test_skip_restoring_state_with_over_due_expire_trigger( - hass, mqtt_mock_entry_with_yaml_config, caplog, freezer + hass, mqtt_mock_entry_with_yaml_config, freezer ): """Test restoring a state with over due expire timer.""" @@ -1209,7 +1197,8 @@ async def test_skip_restoring_state_with_over_due_expire_trigger( ) await hass.async_block_till_done() await mqtt_mock_entry_with_yaml_config() - assert "Skip state recovery after reload for sensor.test3" in caplog.text + state = hass.states.get("sensor.test3") + assert state.state == STATE_UNAVAILABLE @pytest.mark.parametrize( diff --git a/tests/components/mqtt/test_siren.py b/tests/components/mqtt/test_siren.py index 361a043ed4b..02af2d7ac1c 100644 --- a/tests/components/mqtt/test_siren.py +++ b/tests/components/mqtt/test_siren.py @@ -276,10 +276,6 @@ async def test_controlling_state_and_attributes_with_json_message_without_templa 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( diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index ed33ed0dcdf..9276f9658b6 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -4,10 +4,12 @@ import json from unittest.mock import ANY, patch import pytest +from voluptuous import MultipleInvalid from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.mqtt.const import DOMAIN as MQTT_DOMAIN from homeassistant.const import Platform +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -805,6 +807,50 @@ async def test_cleanup_device_with_entity2( assert device_entry is None +@pytest.mark.xfail(raises=MultipleInvalid) +async def test_update_with_bad_config_not_breaks_discovery( + hass: HomeAssistant, + mqtt_mock_entry_no_yaml_config, + tag_mock, +) -> None: + """Test a bad update does not break discovery.""" + await mqtt_mock_entry_no_yaml_config() + config1 = { + "topic": "test-topic", + "device": {"identifiers": ["helloworld"]}, + } + config2 = { + "topic": "test-topic", + "device": {"bad_key": "some bad value"}, + } + + config3 = { + "topic": "test-topic-update", + "device": {"identifiers": ["helloworld"]}, + } + + data1 = json.dumps(config1) + data2 = json.dumps(config2) + data3 = json.dumps(config3) + + async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", data1) + await hass.async_block_till_done() + + # Update with bad identifier + async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", data2) + await hass.async_block_till_done() + + # Topic update + async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", data3) + await hass.async_block_till_done() + + # Fake tag scan. + async_fire_mqtt_message(hass, "test-topic-update", "12345") + + await hass.async_block_till_done() + tag_mock.assert_called_once_with(ANY, "12345", ANY) + + async def test_unload_entry(hass, device_reg, mqtt_mock, tag_mock, tmp_path) -> None: """Test unloading the MQTT entry.""" diff --git a/tests/components/mysensors/conftest.py b/tests/components/mysensors/conftest.py index 8068f6894bf..e7c0a3c5a7b 100644 --- a/tests/components/mysensors/conftest.py +++ b/tests/components/mysensors/conftest.py @@ -12,7 +12,6 @@ from mysensors.persistence import MySensorsJSONDecoder from mysensors.sensor import Sensor import pytest -from homeassistant.components.device_tracker.legacy import Device from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.components.mysensors.config_flow import DEFAULT_BAUD_RATE from homeassistant.components.mysensors.const import ( @@ -29,13 +28,6 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture -@pytest.fixture(autouse=True) -def device_tracker_storage(mock_device_tracker_conf: list[Device]) -> list[Device]: - """Mock out device tracker known devices storage.""" - devices = mock_device_tracker_conf - return devices - - @pytest.fixture(name="mqtt") def mock_mqtt_fixture(hass: HomeAssistant) -> None: """Mock the MQTT integration.""" diff --git a/tests/components/mysensors/fixtures/ir_transceiver_state.json b/tests/components/mysensors/fixtures/ir_transceiver_state.json index 34e16e96787..4785c13d113 100644 --- a/tests/components/mysensors/fixtures/ir_transceiver_state.json +++ b/tests/components/mysensors/fixtures/ir_transceiver_state.json @@ -9,7 +9,7 @@ "values": { "2": "0", "32": "test_code", - "33": "test_code" + "50": "test_code" } } }, diff --git a/tests/components/mysensors/test_binary_sensor.py b/tests/components/mysensors/test_binary_sensor.py index 7dfb188e842..886c13e6ff5 100644 --- a/tests/components/mysensors/test_binary_sensor.py +++ b/tests/components/mysensors/test_binary_sensor.py @@ -2,7 +2,6 @@ from __future__ import annotations from collections.abc import Callable -from unittest.mock import MagicMock from mysensors.sensor import Sensor @@ -15,7 +14,6 @@ async def test_door_sensor( hass: HomeAssistant, door_sensor: Sensor, receive_message: Callable[[str], None], - transport_write: MagicMock, ) -> None: """Test a door sensor.""" entity_id = "binary_sensor.door_sensor_1_1" diff --git a/tests/components/mysensors/test_remote.py b/tests/components/mysensors/test_remote.py new file mode 100644 index 00000000000..adc8590914c --- /dev/null +++ b/tests/components/mysensors/test_remote.py @@ -0,0 +1,165 @@ +"""Provide tests for mysensors remote platform.""" +from __future__ import annotations + +from collections.abc import Callable +from unittest.mock import MagicMock, call + +from mysensors.const_14 import SetReq +from mysensors.sensor import Sensor +import pytest + +from homeassistant.components.remote import ( + ATTR_COMMAND, + DOMAIN as REMOTE_DOMAIN, + SERVICE_LEARN_COMMAND, + SERVICE_SEND_COMMAND, +) +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.core import HomeAssistant + + +async def test_ir_transceiver( + hass: HomeAssistant, + ir_transceiver: Sensor, + receive_message: Callable[[str], None], + transport_write: MagicMock, +) -> None: + """Test an ir transceiver.""" + entity_id = "remote.ir_transceiver_1_1" + + state = hass.states.get(entity_id) + + assert state + assert state.state == "off" + + # Test turn on + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert transport_write.call_count == 2 + assert transport_write.call_args_list[0] == call("1;1;1;1;32;test_code\n") + assert transport_write.call_args_list[1] == call("1;1;1;1;2;1\n") + + receive_message("1;1;1;0;32;test_code\n") + receive_message("1;1;1;0;2;1\n") + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == "on" + + transport_write.reset_mock() + + # Test send command + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_ENTITY_ID: entity_id, ATTR_COMMAND: "new_code"}, + blocking=True, + ) + + assert transport_write.call_count == 1 + assert transport_write.call_args == call("1;1;1;1;32;new_code\n") + + receive_message("1;1;1;0;32;new_code\n") + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == "on" + + transport_write.reset_mock() + + # Test learn command + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_LEARN_COMMAND, + {ATTR_ENTITY_ID: entity_id, ATTR_COMMAND: "learn_code"}, + blocking=True, + ) + + assert transport_write.call_count == 1 + assert transport_write.call_args == call("1;1;1;1;50;learn_code\n") + + receive_message("1;1;1;0;50;learn_code\n") + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == "on" + + transport_write.reset_mock() + + # Test learn command with missing command parameter + with pytest.raises(ValueError): + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_LEARN_COMMAND, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert transport_write.call_count == 0 + + transport_write.reset_mock() + + # Test turn off + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert transport_write.call_count == 1 + assert transport_write.call_args == call("1;1;1;1;2;0\n") + + receive_message("1;1;1;0;2;0\n") + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == "off" + + transport_write.reset_mock() + + # Test turn on with new default code + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + + assert transport_write.call_count == 2 + assert transport_write.call_args_list[0] == call("1;1;1;1;32;new_code\n") + assert transport_write.call_args_list[1] == call("1;1;1;1;2;1\n") + + receive_message("1;1;1;0;32;new_code\n") + receive_message("1;1;1;0;2;1\n") + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == "on" + + # Test unknown state + ir_transceiver.children[1].values.pop(SetReq.V_LIGHT) + + # Trigger state update + receive_message("1;1;1;0;32;new_code\n") + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == "unknown" diff --git a/tests/components/mysensors/test_sensor.py b/tests/components/mysensors/test_sensor.py index 7b5854b9efe..610cda40536 100644 --- a/tests/components/mysensors/test_sensor.py +++ b/tests/components/mysensors/test_sensor.py @@ -15,10 +15,9 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfEnergy, + UnitOfPower, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.util.unit_system import ( @@ -56,6 +55,28 @@ async def test_gps_sensor( assert state.state == f"{new_coords},{altitude}" +async def test_ir_transceiver( + hass: HomeAssistant, + ir_transceiver: Sensor, + receive_message: Callable[[str], None], +) -> None: + """Test an ir transceiver.""" + entity_id = "sensor.ir_transceiver_1_1" + + state = hass.states.get(entity_id) + + assert state + assert state.state == "test_code" + + receive_message("1;1;1;0;50;new_code\n") + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == "new_code" + + async def test_power_sensor( hass: HomeAssistant, power_sensor: Sensor, @@ -69,7 +90,7 @@ async def test_power_sensor( assert state assert state.state == "1200" assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.POWER - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfPower.WATT assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT @@ -86,7 +107,7 @@ async def test_energy_sensor( assert state assert state.state == "18000" assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.ENERGY - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == ENERGY_KILO_WATT_HOUR + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.TOTAL_INCREASING @@ -125,7 +146,10 @@ async def test_distance_sensor( @pytest.mark.parametrize( "unit_system, unit", - [(METRIC_SYSTEM, TEMP_CELSIUS), (US_CUSTOMARY_SYSTEM, TEMP_FAHRENHEIT)], + [ + (METRIC_SYSTEM, UnitOfTemperature.CELSIUS), + (US_CUSTOMARY_SYSTEM, UnitOfTemperature.FAHRENHEIT), + ], ) async def test_temperature_sensor( hass: HomeAssistant, diff --git a/tests/components/mysensors/test_text.py b/tests/components/mysensors/test_text.py new file mode 100644 index 00000000000..3b4b5c767d3 --- /dev/null +++ b/tests/components/mysensors/test_text.py @@ -0,0 +1,65 @@ +"""Provide tests for mysensors text platform.""" +from __future__ import annotations + +from collections.abc import Callable +from unittest.mock import MagicMock, call + +from mysensors.sensor import Sensor +import pytest + +from homeassistant.components.text import ( + ATTR_VALUE, + DOMAIN as TEXT_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import HomeAssistant + + +async def test_text_node( + hass: HomeAssistant, + text_node: Sensor, + receive_message: Callable[[str], None], + transport_write: MagicMock, +) -> None: + """Test a text node.""" + entity_id = "text.text_node_1_1" + + state = hass.states.get(entity_id) + + assert state + assert state.state == "test" + + await hass.services.async_call( + TEXT_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: entity_id, ATTR_VALUE: "Hello World"}, + blocking=True, + ) + + assert transport_write.call_count == 1 + assert transport_write.call_args == call("1;1;1;1;47;Hello World\n") + + receive_message("1;1;1;0;47;Hello World\n") + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + + assert state + assert state.state == "Hello World" + + transport_write.reset_mock() + + value = "12345678123456781234567812" + + with pytest.raises(ValueError) as err: + await hass.services.async_call( + TEXT_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: entity_id, ATTR_VALUE: value}, + blocking=True, + ) + + assert str(err.value) == ( + f"Value {value} for Text Node 1 1 is too long (maximum length 25)" + ) diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py index 271f8a7cb85..81f114c5a8f 100644 --- a/tests/components/nam/test_sensor.py +++ b/tests/components/nam/test_sensor.py @@ -20,10 +20,10 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_MILLION, PERCENTAGE, - PRESSURE_HPA, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, STATE_UNAVAILABLE, - TEMP_CELSIUS, + UnitOfPressure, + UnitOfTemperature, ) from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -75,7 +75,7 @@ async def test_sensor(hass): assert state.state == "7.6" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_bme280_temperature") assert entry @@ -86,7 +86,7 @@ async def test_sensor(hass): assert state.state == "1011" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.HPA entry = registry.async_get("sensor.nettigo_air_monitor_bme280_pressure") assert entry @@ -97,7 +97,7 @@ async def test_sensor(hass): assert state.state == "7.6" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_bmp180_temperature") assert entry @@ -108,7 +108,7 @@ async def test_sensor(hass): assert state.state == "1032" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.HPA entry = registry.async_get("sensor.nettigo_air_monitor_bmp180_pressure") assert entry @@ -119,7 +119,7 @@ async def test_sensor(hass): assert state.state == "5.6" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_bmp280_temperature") assert entry @@ -130,7 +130,7 @@ async def test_sensor(hass): assert state.state == "1022" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.PRESSURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPressure.HPA entry = registry.async_get("sensor.nettigo_air_monitor_bmp280_pressure") assert entry @@ -152,7 +152,7 @@ async def test_sensor(hass): assert state.state == "6.3" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_sht3x_temperature") assert entry @@ -174,7 +174,7 @@ async def test_sensor(hass): assert state.state == "6.3" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_dht22_temperature") assert entry @@ -196,7 +196,7 @@ async def test_sensor(hass): assert state.state == "8.0" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS entry = registry.async_get("sensor.nettigo_air_monitor_heca_temperature") assert entry @@ -231,14 +231,14 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_pmsx003_caqi_level") assert state - assert state.state == "very low" + assert state.state == "very_low" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM assert state.attributes.get(ATTR_OPTIONS) == [ - "very low", + "very_low", "low", "medium", "high", - "very high", + "very_high", ] assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" @@ -331,14 +331,14 @@ async def test_sensor(hass): state = hass.states.get("sensor.nettigo_air_monitor_sds011_caqi_level") assert state - assert state.state == "very low" + assert state.state == "very_low" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM assert state.attributes.get(ATTR_OPTIONS) == [ - "very low", + "very_low", "low", "medium", "high", - "very high", + "very_high", ] assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" @@ -377,11 +377,11 @@ async def test_sensor(hass): assert state.state == "medium" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENUM assert state.attributes.get(ATTR_OPTIONS) == [ - "very low", + "very_low", "low", "medium", "high", - "very high", + "very_high", ] assert state.attributes.get(ATTR_ICON) == "mdi:air-filter" @@ -494,7 +494,7 @@ async def test_incompleta_data_after_device_restart(hass): assert state assert state.state == "8.0" assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS future = utcnow() + timedelta(minutes=6) update_response = Mock(json=AsyncMock(return_value=INCOMPLETE_NAM_DATA)) diff --git a/tests/components/nest/test_sensor_sdm.py b/tests/components/nest/test_sensor_sdm.py index c3698cf4123..b1dddcd9494 100644 --- a/tests/components/nest/test_sensor_sdm.py +++ b/tests/components/nest/test_sensor_sdm.py @@ -21,7 +21,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNAVAILABLE, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -60,7 +60,10 @@ async def test_thermostat_device( temperature = hass.states.get("sensor.my_sensor_temperature") assert temperature is not None assert temperature.state == "25.1" - assert temperature.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert ( + temperature.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfTemperature.CELSIUS + ) assert ( temperature.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE ) diff --git a/tests/components/nexia/test_sensor.py b/tests/components/nexia/test_sensor.py index ad15fc308f4..a5b39af872f 100644 --- a/tests/components/nexia/test_sensor.py +++ b/tests/components/nexia/test_sensor.py @@ -1,6 +1,6 @@ """The sensor tests for the nexia platform.""" -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import PERCENTAGE, UnitOfTemperature from .util import async_init_integration @@ -17,7 +17,7 @@ async def test_create_sensors(hass): "attribution": "Data provided by Trane Technologies", "device_class": "temperature", "friendly_name": "Nick Office Temperature", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears @@ -84,7 +84,7 @@ async def test_create_sensors(hass): "attribution": "Data provided by Trane Technologies", "device_class": "temperature", "friendly_name": "Master Suite Outdoor Temperature", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, } # Only test for a subset of attributes in case # HA changes the implementation and a new one appears diff --git a/tests/components/nibe_heatpump/__init__.py b/tests/components/nibe_heatpump/__init__.py index 2f440d208e7..5446e289656 100644 --- a/tests/components/nibe_heatpump/__init__.py +++ b/tests/components/nibe_heatpump/__init__.py @@ -1 +1,19 @@ """Tests for the Nibe Heat Pump integration.""" + +from typing import Any + +from homeassistant.components.nibe_heatpump import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def async_add_entry(hass: HomeAssistant, data: dict[str, Any]) -> None: + """Add entry and get the coordinator.""" + entry = MockConfigEntry(domain=DOMAIN, title="Dummy", data=data) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == ConfigEntryState.LOADED diff --git a/tests/components/nibe_heatpump/conftest.py b/tests/components/nibe_heatpump/conftest.py new file mode 100644 index 00000000000..43647a73f48 --- /dev/null +++ b/tests/components/nibe_heatpump/conftest.py @@ -0,0 +1,57 @@ +"""Test configuration for Nibe Heat Pump.""" +from collections.abc import AsyncIterator, Iterable +from contextlib import ExitStack +from typing import Any +from unittest.mock import AsyncMock, Mock, patch + +from nibe.coil import Coil +from nibe.connection import Connection +from nibe.exceptions import CoilReadException +import pytest + + +@pytest.fixture(autouse=True, name="mock_connection_constructor") +async def fixture_mock_connection_constructor(): + """Make sure we have a dummy connection.""" + mock_constructor = Mock() + with ExitStack() as stack: + places = [ + "homeassistant.components.nibe_heatpump.config_flow.NibeGW", + "homeassistant.components.nibe_heatpump.config_flow.Modbus", + "homeassistant.components.nibe_heatpump.NibeGW", + "homeassistant.components.nibe_heatpump.Modbus", + ] + for place in places: + stack.enter_context(patch(place, new=mock_constructor)) + yield mock_constructor + + +@pytest.fixture(name="mock_connection") +def fixture_mock_connection(mock_connection_constructor: Mock): + """Make sure we have a dummy connection.""" + mock_connection = AsyncMock(spec=Connection) + mock_connection_constructor.return_value = mock_connection + return mock_connection + + +@pytest.fixture(name="coils") +async def fixture_coils(mock_connection): + """Return a dict with coil data.""" + coils: dict[int, Any] = {} + + async def read_coil(coil: Coil, timeout: float = 0) -> Coil: + nonlocal coils + if (data := coils.get(coil.address, None)) is None: + raise CoilReadException() + coil.value = data + return coil + + async def read_coils( + coils: Iterable[Coil], timeout: float = 0 + ) -> AsyncIterator[Coil]: + for coil in coils: + yield await read_coil(coil, timeout) + + mock_connection.read_coil = read_coil + mock_connection.read_coils = read_coils + yield coils diff --git a/tests/components/nibe_heatpump/test_button.py b/tests/components/nibe_heatpump/test_button.py new file mode 100644 index 00000000000..0ced1799b48 --- /dev/null +++ b/tests/components/nibe_heatpump/test_button.py @@ -0,0 +1,96 @@ +"""Test the Nibe Heat Pump config flow.""" +from typing import Any +from unittest.mock import AsyncMock, patch + +from freezegun.api import FrozenDateTimeFactory +from nibe.coil import Coil +from nibe.coil_groups import UNIT_COILGROUPS +from nibe.heatpump import Model +import pytest + +from homeassistant.components.button import DOMAIN as PLATFORM_DOMAIN, SERVICE_PRESS +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, +) +from homeassistant.core import HomeAssistant + +from . import async_add_entry + +from tests.common import async_fire_time_changed + +MOCK_ENTRY_DATA = { + "model": None, + "ip_address": "127.0.0.1", + "listening_port": 9999, + "remote_read_port": 10000, + "remote_write_port": 10001, + "word_swap": True, + "connection_type": "nibegw", +} + + +@pytest.fixture(autouse=True) +async def fixture_single_platform(): + """Only allow this platform to load.""" + with patch("homeassistant.components.nibe_heatpump.PLATFORMS", [Platform.BUTTON]): + yield + + +@pytest.mark.parametrize( + ("model", "entity_id"), + [ + (Model.F1155, "button.f1155_alarm_reset"), + (Model.S320, "button.s320_reset_alarm"), + ], +) +async def test_reset_button( + hass: HomeAssistant, + mock_connection: AsyncMock, + model: Model, + entity_id: str, + coils: dict[int, Any], + freezer: FrozenDateTimeFactory, +): + """Test reset button.""" + + unit = UNIT_COILGROUPS[model.series]["main"] + + # Setup a non alarm state + coils[unit.alarm_reset] = 0 + coils[unit.alarm] = 0 + + await async_add_entry(hass, {**MOCK_ENTRY_DATA, "model": model.name}) + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNAVAILABLE + + # Signal alarm + coils[unit.alarm] = 100 + + freezer.tick(60) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNKNOWN + + # Press button + await hass.services.async_call( + PLATFORM_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + + # Verify reset was written + args = mock_connection.write_coil.call_args + assert args + coil: Coil = args.args[0] + assert coil.address == unit.alarm_reset + assert coil.value == 1 diff --git a/tests/components/nibe_heatpump/test_config_flow.py b/tests/components/nibe_heatpump/test_config_flow.py index 4a0751ea74b..fbad5685994 100644 --- a/tests/components/nibe_heatpump/test_config_flow.py +++ b/tests/components/nibe_heatpump/test_config_flow.py @@ -1,8 +1,7 @@ """Test the Nibe Heat Pump config flow.""" -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import Mock, patch from nibe.coil import Coil -from nibe.connection import Connection from nibe.exceptions import ( AddressInUseException, CoilNotFoundException, @@ -34,28 +33,6 @@ MOCK_FLOW_MODBUS_USERDATA = { } -@fixture(autouse=True, name="mock_connection_constructor") -async def fixture_mock_connection_constructor(): - """Make sure we have a dummy connection.""" - mock_constructor = Mock() - with patch( - "homeassistant.components.nibe_heatpump.config_flow.NibeGW", - new=mock_constructor, - ), patch( - "homeassistant.components.nibe_heatpump.config_flow.Modbus", - new=mock_constructor, - ): - yield mock_constructor - - -@fixture(name="mock_connection") -def fixture_mock_connection(mock_connection_constructor: Mock): - """Make sure we have a dummy connection.""" - mock_connection = AsyncMock(spec=Connection) - mock_connection_constructor.return_value = mock_connection - return mock_connection - - @fixture(autouse=True, name="mock_setup_entry") async def fixture_mock_setup(): """Make sure we never actually run setup.""" diff --git a/tests/components/notion/conftest.py b/tests/components/notion/conftest.py index e29ea83ef2a..3250beffcf5 100644 --- a/tests/components/notion/conftest.py +++ b/tests/components/notion/conftest.py @@ -1,40 +1,42 @@ """Define fixtures for Notion tests.""" import json -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant.components.notion import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture +TEST_USERNAME = "user@host.com" +TEST_PASSWORD = "password123" + @pytest.fixture(name="client") def client_fixture(data_bridge, data_sensor, data_task): """Define a fixture for an aionotion client.""" - client = AsyncMock() - client.bridge.async_all.return_value = data_bridge - client.sensor.async_all.return_value = data_sensor - client.task.async_all.return_value = data_task - return client + return Mock( + bridge=Mock(async_all=AsyncMock(return_value=data_bridge)), + sensor=Mock(async_all=AsyncMock(return_value=data_sensor)), + task=Mock(async_all=AsyncMock(return_value=data_task)), + ) @pytest.fixture(name="config_entry") def config_entry_fixture(hass, config): """Define a config entry fixture.""" - entry = MockConfigEntry(domain=DOMAIN, unique_id=config[CONF_USERNAME], data=config) + entry = MockConfigEntry(domain=DOMAIN, unique_id=TEST_USERNAME, data=config) entry.add_to_hass(hass) return entry @pytest.fixture(name="config") -def config_fixture(hass): +def config_fixture(): """Define a config entry data fixture.""" return { - CONF_USERNAME: "user@host.com", - CONF_PASSWORD: "password123", + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, } @@ -62,14 +64,22 @@ def get_client_fixture(client): return AsyncMock(return_value=client) -@pytest.fixture(name="setup_notion") -async def setup_notion_fixture(hass, config, get_client): - """Define a fixture to set up Notion.""" +@pytest.fixture(name="mock_aionotion") +async def mock_aionotion_fixture(client): + """Define a fixture to patch aionotion.""" with patch( - "homeassistant.components.notion.config_flow.async_get_client", get_client - ), patch("homeassistant.components.notion.async_get_client", get_client), patch( - "homeassistant.components.notion.PLATFORMS", [] + "homeassistant.components.notion.async_get_client", + AsyncMock(return_value=client), + ), patch( + "homeassistant.components.notion.config_flow.async_get_client", + AsyncMock(return_value=client), ): - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() yield + + +@pytest.fixture(name="setup_config_entry") +async def setup_config_entry_fixture(hass, config_entry, mock_aionotion): + """Define a fixture to set up notion.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield diff --git a/tests/components/notion/test_config_flow.py b/tests/components/notion/test_config_flow.py index 0eff3890274..437a88ffda9 100644 --- a/tests/components/notion/test_config_flow.py +++ b/tests/components/notion/test_config_flow.py @@ -9,6 +9,8 @@ from homeassistant.components.notion import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from .conftest import TEST_PASSWORD, TEST_USERNAME + @pytest.mark.parametrize( "get_client_with_exception,errors", @@ -19,7 +21,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME ], ) async def test_create_entry( - hass, client, config, errors, get_client_with_exception, setup_notion + hass, client, config, errors, get_client_with_exception, mock_aionotion ): """Test creating an etry (including recovery from errors).""" result = await hass.config_entries.flow.async_init( @@ -43,14 +45,14 @@ async def test_create_entry( result["flow_id"], user_input=config ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "user@host.com" + assert result["title"] == TEST_USERNAME assert result["data"] == { - CONF_USERNAME: "user@host.com", - CONF_PASSWORD: "password123", + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, } -async def test_duplicate_error(hass, config, config_entry): +async def test_duplicate_error(hass, config, setup_config_entry): """Test that errors are shown when duplicates are added.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config @@ -68,7 +70,7 @@ async def test_duplicate_error(hass, config, config_entry): ], ) async def test_reauth( - hass, config, config_entry, errors, get_client_with_exception, setup_notion + hass, config, config_entry, errors, get_client_with_exception, setup_config_entry ): """Test that re-auth works.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/notion/test_diagnostics.py b/tests/components/notion/test_diagnostics.py index d8b5abcc781..3b45ed535a1 100644 --- a/tests/components/notion/test_diagnostics.py +++ b/tests/components/notion/test_diagnostics.py @@ -4,7 +4,7 @@ from homeassistant.components.diagnostics import REDACTED from tests.components.diagnostics import get_diagnostics_for_config_entry -async def test_entry_diagnostics(hass, config_entry, hass_client, setup_notion): +async def test_entry_diagnostics(hass, config_entry, hass_client, setup_config_entry): """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { diff --git a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py index d8719578957..0fdbbc56ca2 100644 --- a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py +++ b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py @@ -31,7 +31,7 @@ from homeassistant.const import ( CONF_RADIUS, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - LENGTH_KILOMETERS, + UnitOfLength, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -155,7 +155,7 @@ async def test_setup(hass): ATTR_TYPE: "Type 1", ATTR_SIZE: "Size 1", ATTR_RESPONSIBLE_AGENCY: "Agency 1", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "nsw_rural_fire_service_feed", ATTR_ICON: "mdi:fire", } @@ -170,7 +170,7 @@ async def test_setup(hass): ATTR_LONGITUDE: 150.1, ATTR_FRIENDLY_NAME: "Title 2", ATTR_FIRE: False, - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "nsw_rural_fire_service_feed", ATTR_ICON: "mdi:alarm-light", } @@ -185,7 +185,7 @@ async def test_setup(hass): ATTR_LONGITUDE: 150.2, ATTR_FRIENDLY_NAME: "Title 3", ATTR_FIRE: True, - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "nsw_rural_fire_service_feed", ATTR_ICON: "mdi:fire", } diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index f6b2c615123..0cb4b5b9818 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -14,13 +14,18 @@ from homeassistant.components.number import ( NumberEntity, NumberEntityDescription, ) -from homeassistant.components.sensor import SensorDeviceClass +from homeassistant.components.number.const import ( + DEVICE_CLASS_UNITS as NUMBER_DEVICE_CLASS_UNITS, +) +from homeassistant.components.sensor import ( + DEVICE_CLASS_UNITS as SENSOR_DEVICE_CLASS_UNITS, + SensorDeviceClass, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_PLATFORM, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er @@ -437,8 +442,8 @@ async def test_deprecated_methods( [ ( US_CUSTOMARY_SYSTEM, - TEMP_FAHRENHEIT, - TEMP_FAHRENHEIT, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.FAHRENHEIT, 100, 100, 50, @@ -452,8 +457,8 @@ async def test_deprecated_methods( ), ( US_CUSTOMARY_SYSTEM, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, 38, 100, 10, @@ -467,8 +472,8 @@ async def test_deprecated_methods( ), ( METRIC_SYSTEM, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.CELSIUS, 100, 38, 50, @@ -482,8 +487,8 @@ async def test_deprecated_methods( ), ( METRIC_SYSTEM, - TEMP_CELSIUS, - TEMP_CELSIUS, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.CELSIUS, 38, 38, 10, @@ -604,7 +609,7 @@ async def test_restore_number_save_state( native_max_value=200.0, native_min_value=-10.0, native_step=2.0, - native_unit_of_measurement=TEMP_FAHRENHEIT, + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, native_value=123.0, device_class=NumberDeviceClass.TEMPERATURE, ) @@ -699,25 +704,25 @@ async def test_restore_number_restore_state( # Not a supported temperature unit ( NumberDeviceClass.TEMPERATURE, - TEMP_CELSIUS, + UnitOfTemperature.CELSIUS, "my_temperature_unit", - TEMP_CELSIUS, + UnitOfTemperature.CELSIUS, 1000, 1000, ), ( NumberDeviceClass.TEMPERATURE, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_FAHRENHEIT, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.FAHRENHEIT, 37.5, 99.5, ), ( NumberDeviceClass.TEMPERATURE, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.CELSIUS, 100, 38.0, ), @@ -767,25 +772,33 @@ async def test_custom_unit( "native_unit, custom_unit, used_custom_unit, default_unit, native_value, custom_value, default_value", [ ( - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.CELSIUS, 37.5, 99.5, 37.5, ), ( - TEMP_FAHRENHEIT, - TEMP_FAHRENHEIT, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.CELSIUS, 100, 100, 38.0, ), # Not a supported temperature unit - (TEMP_CELSIUS, "no_unit", TEMP_CELSIUS, TEMP_CELSIUS, 1000, 1000, 1000), + ( + UnitOfTemperature.CELSIUS, + "no_unit", + UnitOfTemperature.CELSIUS, + UnitOfTemperature.CELSIUS, + 1000, + 1000, + 1000, + ), ], ) async def test_custom_unit_change( @@ -867,3 +880,11 @@ def test_device_classes_aligned(): assert hasattr(NumberDeviceClass, device_class.name) assert getattr(NumberDeviceClass, device_class.name).value == device_class.value + + for device_class in SENSOR_DEVICE_CLASS_UNITS: + if device_class in non_numeric_device_classes: + continue + assert ( + SENSOR_DEVICE_CLASS_UNITS[device_class] + == NUMBER_DEVICE_CLASS_UNITS[device_class] + ) diff --git a/tests/components/number/test_websocket_api.py b/tests/components/number/test_websocket_api.py new file mode 100644 index 00000000000..1cba89c42a1 --- /dev/null +++ b/tests/components/number/test_websocket_api.py @@ -0,0 +1,49 @@ +"""Test the number websocket API.""" +from pytest_unordered import unordered + +from homeassistant.components.number.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def test_device_class_units(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can get supported units.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + # Device class with units which number allows customizing & converting + await client.send_json( + { + "id": 1, + "type": "number/device_class_convertible_units", + "device_class": "temperature", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"units": unordered(["°F", "°C", "K"])} + + # Device class with units which number doesn't allow customizing & converting + await client.send_json( + { + "id": 2, + "type": "number/device_class_convertible_units", + "device_class": "energy", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"units": []} + + # Unknown device class + await client.send_json( + { + "id": 3, + "type": "number/device_class_convertible_units", + "device_class": "kebabsås", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"units": unordered([])} diff --git a/tests/components/nut/fixtures/EATON5P1550.json b/tests/components/nut/fixtures/EATON5P1550.json new file mode 100644 index 00000000000..cb678bb3058 --- /dev/null +++ b/tests/components/nut/fixtures/EATON5P1550.json @@ -0,0 +1,53 @@ +{ + "battery.charge": "100", + "battery.current": "0", + "battery.runtime": "17820", + "battery.runtime.low": "180", + "battery.temperature": "0", + "battery.voltage": "38", + "device.description": "Eaton 5P 1550", + "device.mfr": "EATON", + "device.model": "Eaton 5P 1550", + "device.type": "ups", + "driver.name": "snmp-ups", + "driver.parameter.mibs": "ietf", + "driver.parameter.pollfreq": "15", + "driver.parameter.pollinterval": "2", + "driver.parameter.snmp_version": "v1", + "driver.parameter.synchronous": "auto", + "driver.version": "2.8.0", + "driver.version.data": "ietf MIB 1.54", + "driver.version.internal": "1.21", + "input.bypass.frequency": "0", + "input.bypass.phases": "0", + "input.current": "0.10", + "input.frequency": "50", + "input.frequency.nominal": "50", + "input.phases": "1", + "input.realpower": "0", + "input.transfer.high": "294", + "input.transfer.low": "160", + "input.voltage": "247", + "input.voltage.nominal": "230", + "output.current": "0", + "output.frequency": "50", + "output.frequency.nominal": "50", + "output.phases": "1", + "output.power.nominal": "1550", + "output.realpower": "0", + "output.realpower.nominal": "1100", + "output.voltage": "247", + "output.voltage.nominal": "230", + "ups.beeper.status": "disabled", + "ups.firmware: INV": "02.14.0026", + "ups.firmware.aux": "Network Management Card V6.00 LE", + "ups.load": "0", + "ups.mfr": "EATON", + "ups.model": "Eaton 5P 1550", + "ups.start.auto": "yes", + "ups.status": "OL", + "ups.test.result": "done and passed", + "ups.timer.reboot": "-1", + "ups.timer.shutdown": "-1", + "ups.timer.start": "-1" +} diff --git a/tests/components/nut/test_sensor.py b/tests/components/nut/test_sensor.py index b36c6e8bcc4..50d77295dfb 100644 --- a/tests/components/nut/test_sensor.py +++ b/tests/components/nut/test_sensor.py @@ -189,6 +189,30 @@ async def test_dl650elcd(hass): ) +async def test_eaton5p1550(hass): + """Test creation of EATON5P1550 sensors.""" + + config_entry = await async_init_integration(hass, "EATON5P1550") + registry = er.async_get(hass) + entry = registry.async_get("sensor.ups1_battery_charge") + assert entry + assert entry.unique_id == f"{config_entry.entry_id}_battery.charge" + + state = hass.states.get("sensor.ups1_battery_charge") + assert state.state == "100" + + expected_attributes = { + "device_class": "battery", + "friendly_name": "Ups1 Battery Charge", + "unit_of_measurement": PERCENTAGE, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == attr for key, attr in expected_attributes.items() + ) + + async def test_blazer_usb(hass): """Test creation of blazer_usb sensors.""" diff --git a/tests/components/nws/const.py b/tests/components/nws/const.py index 850d330c9ae..2048db2a2c3 100644 --- a/tests/components/nws/const.py +++ b/tests/components/nws/const.py @@ -19,21 +19,17 @@ from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, - LENGTH_KILOMETERS, - LENGTH_METERS, - LENGTH_MILES, - PRESSURE_HPA, - PRESSURE_INHG, - PRESSURE_PA, - SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfLength, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, +) +from homeassistant.util.unit_conversion import ( + DistanceConverter, + PressureConverter, + SpeedConverter, + TemperatureConverter, ) -from homeassistant.util.distance import convert as convert_distance -from homeassistant.util.pressure import convert as convert_pressure -from homeassistant.util.speed import convert as convert_speed -from homeassistant.util.temperature import convert as convert_temperature NWS_CONFIG = { CONF_API_KEY: "test", @@ -78,40 +74,83 @@ SENSOR_EXPECTED_OBSERVATION_METRIC = { } SENSOR_EXPECTED_OBSERVATION_IMPERIAL = { - "dewpoint": str(round(convert_temperature(5, TEMP_CELSIUS, TEMP_FAHRENHEIT))), - "temperature": str(round(convert_temperature(10, TEMP_CELSIUS, TEMP_FAHRENHEIT))), - "windChill": str(round(convert_temperature(5, TEMP_CELSIUS, TEMP_FAHRENHEIT))), - "heatIndex": str(round(convert_temperature(15, TEMP_CELSIUS, TEMP_FAHRENHEIT))), + "dewpoint": str( + round( + TemperatureConverter.convert( + 5, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT + ) + ) + ), + "temperature": str( + round( + TemperatureConverter.convert( + 10, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT + ) + ) + ), + "windChill": str( + round( + TemperatureConverter.convert( + 5, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT + ) + ) + ), + "heatIndex": str( + round( + TemperatureConverter.convert( + 15, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT + ) + ) + ), "relativeHumidity": "10", "windSpeed": str( - round(convert_speed(10, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR)) + round( + SpeedConverter.convert( + 10, UnitOfSpeed.KILOMETERS_PER_HOUR, UnitOfSpeed.MILES_PER_HOUR + ) + ) ), "windGust": str( - round(convert_speed(20, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR)) + round( + SpeedConverter.convert( + 20, UnitOfSpeed.KILOMETERS_PER_HOUR, UnitOfSpeed.MILES_PER_HOUR + ) + ) ), "windDirection": "180", "barometricPressure": str( - round(convert_pressure(100000, PRESSURE_PA, PRESSURE_INHG), 2) + round( + PressureConverter.convert(100000, UnitOfPressure.PA, UnitOfPressure.INHG), 2 + ) ), "seaLevelPressure": str( - round(convert_pressure(100000, PRESSURE_PA, PRESSURE_INHG), 2) + round( + PressureConverter.convert(100000, UnitOfPressure.PA, UnitOfPressure.INHG), 2 + ) + ), + "visibility": str( + round(DistanceConverter.convert(10000, UnitOfLength.METERS, UnitOfLength.MILES)) ), - "visibility": str(round(convert_distance(10000, LENGTH_METERS, LENGTH_MILES))), } WEATHER_EXPECTED_OBSERVATION_IMPERIAL = { ATTR_WEATHER_TEMPERATURE: round( - convert_temperature(10, TEMP_CELSIUS, TEMP_FAHRENHEIT) + TemperatureConverter.convert( + 10, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT + ) ), ATTR_WEATHER_WIND_BEARING: 180, ATTR_WEATHER_WIND_SPEED: round( - convert_speed(10, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR), 2 + SpeedConverter.convert( + 10, UnitOfSpeed.KILOMETERS_PER_HOUR, UnitOfSpeed.MILES_PER_HOUR + ), + 2, ), ATTR_WEATHER_PRESSURE: round( - convert_pressure(100000, PRESSURE_PA, PRESSURE_INHG), 2 + PressureConverter.convert(100000, UnitOfPressure.PA, UnitOfPressure.INHG), 2 ), ATTR_WEATHER_VISIBILITY: round( - convert_distance(10000, LENGTH_METERS, LENGTH_MILES), 2 + DistanceConverter.convert(10000, UnitOfLength.METERS, UnitOfLength.MILES), 2 ), ATTR_WEATHER_HUMIDITY: 10, } @@ -120,9 +159,11 @@ WEATHER_EXPECTED_OBSERVATION_METRIC = { ATTR_WEATHER_TEMPERATURE: 10, ATTR_WEATHER_WIND_BEARING: 180, ATTR_WEATHER_WIND_SPEED: 10, - ATTR_WEATHER_PRESSURE: round(convert_pressure(100000, PRESSURE_PA, PRESSURE_HPA)), + ATTR_WEATHER_PRESSURE: round( + PressureConverter.convert(100000, UnitOfPressure.PA, UnitOfPressure.HPA) + ), ATTR_WEATHER_VISIBILITY: round( - convert_distance(10000, LENGTH_METERS, LENGTH_KILOMETERS) + DistanceConverter.convert(10000, UnitOfLength.METERS, UnitOfLength.KILOMETERS) ), ATTR_WEATHER_HUMIDITY: 10, } @@ -158,10 +199,16 @@ EXPECTED_FORECAST_METRIC = { ATTR_FORECAST_CONDITION: ATTR_CONDITION_LIGHTNING_RAINY, ATTR_FORECAST_TIME: "2019-08-12T20:00:00-04:00", ATTR_FORECAST_TEMP: round( - convert_temperature(10, TEMP_FAHRENHEIT, TEMP_CELSIUS), 1 + TemperatureConverter.convert( + 10, UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS + ), + 1, ), ATTR_FORECAST_WIND_SPEED: round( - convert_speed(10, SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR), 2 + SpeedConverter.convert( + 10, UnitOfSpeed.MILES_PER_HOUR, UnitOfSpeed.KILOMETERS_PER_HOUR + ), + 2, ), ATTR_FORECAST_WIND_BEARING: 180, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 90, diff --git a/tests/components/nzbget/test_sensor.py b/tests/components/nzbget/test_sensor.py index 796c72804aa..524133cf957 100644 --- a/tests/components/nzbget/test_sensor.py +++ b/tests/components/nzbget/test_sensor.py @@ -5,8 +5,8 @@ from unittest.mock import patch from homeassistant.components.sensor import SensorDeviceClass from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, - DATA_MEGABYTES, - DATA_RATE_MEGABYTES_PER_SECOND, + UnitOfDataRate, + UnitOfInformation, ) from homeassistant.helpers import entity_registry as er from homeassistant.util import dt as dt_util @@ -28,32 +28,32 @@ async def test_sensors(hass, nzbget_api) -> None: "article_cache": ( "ArticleCacheMB", "64", - DATA_MEGABYTES, + UnitOfInformation.MEGABYTES, SensorDeviceClass.DATA_SIZE, ), "average_speed": ( "AverageDownloadRate", "1.19", - DATA_RATE_MEGABYTES_PER_SECOND, + UnitOfDataRate.MEGABYTES_PER_SECOND, SensorDeviceClass.DATA_RATE, ), "download_paused": ("DownloadPaused", "False", None, None), "speed": ( "DownloadRate", "2.38", - DATA_RATE_MEGABYTES_PER_SECOND, + UnitOfDataRate.MEGABYTES_PER_SECOND, SensorDeviceClass.DATA_RATE, ), "size": ( "DownloadedSizeMB", "256", - DATA_MEGABYTES, + UnitOfInformation.MEGABYTES, SensorDeviceClass.DATA_SIZE, ), "disk_free": ( "FreeDiskSpaceMB", "1024", - DATA_MEGABYTES, + UnitOfInformation.MEGABYTES, SensorDeviceClass.DATA_SIZE, ), "post_processing_jobs": ("PostJobCount", "2", "Jobs", None), @@ -61,14 +61,14 @@ async def test_sensors(hass, nzbget_api) -> None: "queue_size": ( "RemainingSizeMB", "512", - DATA_MEGABYTES, + UnitOfInformation.MEGABYTES, SensorDeviceClass.DATA_SIZE, ), "uptime": ("UpTimeSec", uptime.isoformat(), None, SensorDeviceClass.TIMESTAMP), "speed_limit": ( "DownloadLimit", "0.95", - DATA_RATE_MEGABYTES_PER_SECOND, + UnitOfDataRate.MEGABYTES_PER_SECOND, SensorDeviceClass.DATA_RATE, ), } diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index c28ec017a9b..4e3dac34d2d 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -24,15 +24,14 @@ from homeassistant.const import ( ATTR_STATE, ATTR_UNIT_OF_MEASUREMENT, ATTR_VIA_DEVICE, - ELECTRIC_POTENTIAL_VOLT, LIGHT_LUX, PERCENTAGE, - PRESSURE_CBAR, - PRESSURE_MBAR, STATE_OFF, STATE_ON, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfElectricPotential, + UnitOfPressure, + UnitOfTemperature, ) from homeassistant.helpers.entity import EntityCategory @@ -96,7 +95,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "25.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/10.111111111111/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ], }, @@ -135,7 +134,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "25.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/12.111111111111/TAI8570/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_DEFAULT_DISABLED: True, @@ -145,7 +144,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "1025.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/12.111111111111/TAI8570/pressure", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPressure.MBAR, }, ], Platform.SWITCH: [ @@ -276,7 +275,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/22.111111111111/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ], }, @@ -298,7 +297,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "25.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_DEFAULT_DISABLED: True, @@ -358,7 +357,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "969.3", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/B1-R1-A/pressure", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPressure.MBAR, }, { ATTR_DEFAULT_DISABLED: True, @@ -378,7 +377,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "3.0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/VAD", - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfElectricPotential.VOLT, }, { ATTR_DEFAULT_DISABLED: True, @@ -388,7 +387,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "4.7", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/VDD", - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfElectricPotential.VOLT, }, { ATTR_DEFAULT_DISABLED: True, @@ -398,7 +397,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "0.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/26.111111111111/vis", - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfElectricPotential.VOLT, }, ], Platform.SWITCH: [ @@ -430,7 +429,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "27.0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/28.111111111111/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ], }, @@ -454,7 +453,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "27.0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/28.222222222222/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ], }, @@ -478,7 +477,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "27.0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/28.222222222223/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ], }, @@ -683,7 +682,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "27.0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/30.111111111111/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_DEFAULT_DISABLED: True, @@ -694,7 +693,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "173.8", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/30.111111111111/typeX/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_DEFAULT_DISABLED: True, @@ -704,7 +703,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "3.0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/30.111111111111/volt", - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfElectricPotential.VOLT, }, { ATTR_DEFAULT_DISABLED: True, @@ -714,7 +713,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "0.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/30.111111111111/vis", - ATTR_UNIT_OF_MEASUREMENT: ELECTRIC_POTENTIAL_VOLT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfElectricPotential.VOLT, }, ], }, @@ -779,7 +778,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "28.2", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/3B.111111111111/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ], }, @@ -801,7 +800,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "29.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/42.111111111111/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ], }, @@ -841,7 +840,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "25.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/EF.111111111111/humidity/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, ], }, @@ -885,7 +884,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "43.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/EF.111111111112/moisture/sensor.2", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_CBAR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPressure.CBAR, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.PRESSURE, @@ -894,7 +893,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "44.1", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/EF.111111111112/moisture/sensor.3", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_CBAR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPressure.CBAR, }, ], Platform.SWITCH: [ @@ -1066,7 +1065,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "13.9", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/7E.111111111111/EDS0068/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.PRESSURE, @@ -1075,7 +1074,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "1012.2", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/7E.111111111111/EDS0068/pressure", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPressure.MBAR, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE, @@ -1116,7 +1115,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "13.9", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/7E.222222222222/EDS0066/temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.PRESSURE, @@ -1125,7 +1124,7 @@ MOCK_OWPROXY_DEVICES = { ATTR_STATE: "1012.2", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "/7E.222222222222/EDS0066/pressure", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_MBAR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPressure.MBAR, }, ], }, diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index 636228af143..399fd6ad86e 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for 1-Wire binary sensors.""" +from collections.abc import Generator import logging from unittest.mock import MagicMock, patch @@ -21,7 +22,7 @@ from tests.common import mock_device_registry, mock_registry @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.onewire.PLATFORMS", [Platform.BINARY_SENSOR]): yield @@ -33,7 +34,7 @@ async def test_binary_sensors( owproxy: MagicMock, device_id: str, caplog: pytest.LogCaptureFixture, -): +) -> None: """Test for 1-Wire binary sensor. This test forces all entities to be enabled. diff --git a/tests/components/onewire/test_diagnostics.py b/tests/components/onewire/test_diagnostics.py index e279e3a2633..8840c456b48 100644 --- a/tests/components/onewire/test_diagnostics.py +++ b/tests/components/onewire/test_diagnostics.py @@ -1,4 +1,5 @@ """Test 1-Wire diagnostics.""" +from collections.abc import Generator from unittest.mock import MagicMock, patch import pytest @@ -14,7 +15,7 @@ from tests.components.diagnostics import get_diagnostics_for_config_entry @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.onewire.PLATFORMS", [Platform.SWITCH]): yield @@ -41,7 +42,7 @@ async def test_entry_diagnostics( hass_client, owproxy: MagicMock, device_id: str, -): +) -> None: """Test config entry diagnostics.""" setup_owproxy_mock_devices(owproxy, Platform.SENSOR, [device_id]) await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index a20714fedfc..123ccf7a506 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -1,4 +1,5 @@ """Tests for 1-Wire sensors.""" +from collections.abc import Generator import logging from unittest.mock import MagicMock, patch @@ -21,7 +22,7 @@ from tests.common import mock_device_registry, mock_registry @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.onewire.PLATFORMS", [Platform.SENSOR]): yield @@ -33,7 +34,7 @@ async def test_sensors( owproxy: MagicMock, device_id: str, caplog: pytest.LogCaptureFixture, -): +) -> None: """Test for 1-Wire device. As they would be on a clean setup: all binary-sensors and switches disabled. diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index 8b60232330f..94e145d8cc7 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -1,4 +1,5 @@ """Tests for 1-Wire switches.""" +from collections.abc import Generator import logging from unittest.mock import MagicMock, patch @@ -29,7 +30,7 @@ from tests.common import mock_device_registry, mock_registry @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.onewire.PLATFORMS", [Platform.SWITCH]): yield @@ -41,7 +42,7 @@ async def test_switches( owproxy: MagicMock, device_id: str, caplog: pytest.LogCaptureFixture, -): +) -> None: """Test for 1-Wire switch. This test forces all entities to be enabled. diff --git a/tests/components/open_meteo/test_config_flow.py b/tests/components/open_meteo/test_config_flow.py index 0dd81d35856..c81d4da8c91 100644 --- a/tests/components/open_meteo/test_config_flow.py +++ b/tests/components/open_meteo/test_config_flow.py @@ -21,7 +21,6 @@ async def test_full_user_flow( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/openai_conversation/__init__.py b/tests/components/openai_conversation/__init__.py new file mode 100644 index 00000000000..dda2fe16a63 --- /dev/null +++ b/tests/components/openai_conversation/__init__.py @@ -0,0 +1 @@ +"""Tests for the OpenAI Conversation integration.""" diff --git a/tests/components/openai_conversation/conftest.py b/tests/components/openai_conversation/conftest.py new file mode 100644 index 00000000000..9f00290600e --- /dev/null +++ b/tests/components/openai_conversation/conftest.py @@ -0,0 +1,31 @@ +"""Tests helpers.""" +from unittest.mock import patch + +import pytest + +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry(hass): + """Mock a config entry.""" + entry = MockConfigEntry( + domain="openai_conversation", + data={ + "api_key": "bla", + }, + ) + entry.add_to_hass(hass) + return entry + + +@pytest.fixture +async def mock_init_component(hass, mock_config_entry): + """Initialize integration.""" + with patch( + "openai.Engine.list", + ): + assert await async_setup_component(hass, "openai_conversation", {}) + await hass.async_block_till_done() diff --git a/tests/components/openai_conversation/test_config_flow.py b/tests/components/openai_conversation/test_config_flow.py new file mode 100644 index 00000000000..1510b986b59 --- /dev/null +++ b/tests/components/openai_conversation/test_config_flow.py @@ -0,0 +1,79 @@ +"""Test the OpenAI Conversation config flow.""" +from unittest.mock import patch + +from openai.error import APIConnectionError, AuthenticationError, InvalidRequestError +import pytest + +from homeassistant import config_entries +from homeassistant.components.openai_conversation.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def test_single_instance_allowed( + hass: HomeAssistant, mock_config_entry: config_entries.ConfigEntry +) -> None: + """Test that config flow only allows a single instance.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.openai_conversation.config_flow.openai.Engine.list", + ), patch( + "homeassistant.components.openai_conversation.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": "bla", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["data"] == { + "api_key": "bla", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "side_effect, error", + [ + (APIConnectionError(""), "cannot_connect"), + (AuthenticationError, "invalid_auth"), + (InvalidRequestError, "unknown"), + ], +) +async def test_form_invalid_auth(hass: HomeAssistant, side_effect, error) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.openai_conversation.config_flow.openai.Engine.list", + side_effect=side_effect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "api_key": "bla", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": error} diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py new file mode 100644 index 00000000000..551d493df8e --- /dev/null +++ b/tests/components/openai_conversation/test_init.py @@ -0,0 +1,108 @@ +"""Tests for the OpenAI integration.""" +from unittest.mock import patch + +from openai import error + +from homeassistant.components import conversation +from homeassistant.core import Context +from homeassistant.helpers import area_registry, device_registry, intent + + +async def test_default_prompt(hass, mock_init_component): + """Test that the default prompt works.""" + device_reg = device_registry.async_get(hass) + area_reg = area_registry.async_get(hass) + + for i in range(3): + area_reg.async_create(f"{i}Empty Area") + + device_reg.async_get_or_create( + config_entry_id="1234", + connections={("test", "1234")}, + name="Test Device", + manufacturer="Test Manufacturer", + model="Test Model", + suggested_area="Test Area", + ) + for i in range(3): + device_reg.async_get_or_create( + config_entry_id="1234", + connections={("test", f"{i}abcd")}, + name="Test Service", + manufacturer="Test Manufacturer", + model="Test Model", + suggested_area="Test Area", + entry_type=device_registry.DeviceEntryType.SERVICE, + ) + device_reg.async_get_or_create( + config_entry_id="1234", + connections={("test", "5678")}, + name="Test Device 2", + manufacturer="Test Manufacturer 2", + model="Device 2", + suggested_area="Test Area 2", + ) + device_reg.async_get_or_create( + config_entry_id="1234", + connections={("test", "9876")}, + name="Test Device 3", + manufacturer="Test Manufacturer 3", + model="Test Model 3A", + suggested_area="Test Area 2", + ) + device_reg.async_get_or_create( + config_entry_id="1234", + connections={("test", "qwer")}, + name="Test Device 4", + suggested_area="Test Area 2", + ) + device = device_reg.async_get_or_create( + config_entry_id="1234", + connections={("test", "9876-disabled")}, + name="Test Device 3", + manufacturer="Test Manufacturer 3", + model="Test Model 3A", + suggested_area="Test Area 2", + ) + device_reg.async_update_device( + device.id, disabled_by=device_registry.DeviceEntryDisabler.USER + ) + + with patch("openai.Completion.create") as mock_create: + result = await conversation.async_converse(hass, "hello", None, Context()) + + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + + assert ( + mock_create.mock_calls[0][2]["prompt"] + == """This smart home is controlled by Home Assistant. + +An overview of the areas and the devices in this smart home: + +Test Area: +- Test Device (Test Model) + +Test Area 2: +- Test Device 2 +- Test Device 3 (Test Model 3A) +- Test Device 4 + +Answer the users questions about the world truthfully. + +If the user wants to control a device, reject the request and suggest using the Home Assistant app. + +Now finish this conversation: + +Smart home: How can I assist? +User: hello +Smart home: """ + ) + + +async def test_error_handling(hass, mock_init_component): + """Test that the default prompt works.""" + with patch("openai.Completion.create", side_effect=error.ServiceUnavailableError): + result = await conversation.async_converse(hass, "hello", None, Context()) + + assert result.response.response_type == intent.IntentResponseType.ERROR, result + assert result.response.error_code == "unknown", result diff --git a/tests/components/openuv/conftest.py b/tests/components/openuv/conftest.py index b2c0a6c7ec5..564323d6894 100644 --- a/tests/components/openuv/conftest.py +++ b/tests/components/openuv/conftest.py @@ -1,6 +1,6 @@ """Define test fixtures for OpenUV.""" import json -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch import pytest @@ -11,10 +11,23 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, ) -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture +TEST_API_KEY = "abcde12345" +TEST_ELEVATION = 0 +TEST_LATITUDE = 51.528308 +TEST_LONGITUDE = -0.3817765 + + +@pytest.fixture(name="client") +def client_fixture(data_protection_window, data_uv_index): + """Define a mock Client object.""" + return Mock( + uv_index=AsyncMock(return_value=data_uv_index), + uv_protection_window=AsyncMock(return_value=data_protection_window), + ) + @pytest.fixture(name="config_entry") def config_entry_fixture(hass, config): @@ -30,13 +43,13 @@ def config_entry_fixture(hass, config): @pytest.fixture(name="config") -def config_fixture(hass): +def config_fixture(): """Define a config entry data fixture.""" return { - CONF_API_KEY: "abcde12345", - CONF_ELEVATION: 0, - CONF_LATITUDE: 51.528308, - CONF_LONGITUDE: -0.3817765, + CONF_API_KEY: TEST_API_KEY, + CONF_ELEVATION: TEST_ELEVATION, + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, } @@ -52,17 +65,18 @@ def data_uv_index_fixture(): return json.loads(load_fixture("uv_index_data.json", "openuv")) -@pytest.fixture(name="setup_openuv") -async def setup_openuv_fixture(hass, config, data_protection_window, data_uv_index): - """Define a fixture to set up OpenUV.""" +@pytest.fixture(name="mock_pyopenuv") +async def mock_pyopenuv_fixture(client): + """Define a fixture to patch pyopenuv.""" with patch( - "homeassistant.components.openuv.Client.uv_index", return_value=data_uv_index - ), patch( - "homeassistant.components.openuv.Client.uv_protection_window", - return_value=data_protection_window, - ), patch( - "homeassistant.components.openuv.PLATFORMS", [] - ): - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() + "homeassistant.components.openuv.config_flow.Client", return_value=client + ), patch("homeassistant.components.openuv.Client", return_value=client): yield + + +@pytest.fixture(name="setup_config_entry") +async def setup_config_entry_fixture(hass, config_entry, mock_pyopenuv): + """Define a fixture to set up openuv.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index 555d01e2624..3a4e9753699 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -1,5 +1,5 @@ """Define tests for the OpenUV config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from pyopenuv.errors import InvalidApiKeyError import voluptuous as vol @@ -14,8 +14,41 @@ from homeassistant.const import ( CONF_LONGITUDE, ) +from .conftest import TEST_API_KEY, TEST_ELEVATION, TEST_LATITUDE, TEST_LONGITUDE -async def test_duplicate_error(hass, config, config_entry): + +async def test_create_entry(hass, client, config, mock_pyopenuv): + """Test creating an entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + + # Test an error occurring: + with patch.object(client, "uv_index", AsyncMock(side_effect=InvalidApiKeyError)): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} + + # Test that we can recover from the error: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == f"{TEST_LATITUDE}, {TEST_LONGITUDE}" + assert result["data"] == { + CONF_API_KEY: TEST_API_KEY, + CONF_ELEVATION: TEST_ELEVATION, + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, + } + + +async def test_duplicate_error(hass, config, config_entry, setup_config_entry): """Test that errors are shown when duplicates are added.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config @@ -24,60 +57,45 @@ async def test_duplicate_error(hass, config, config_entry): assert result["reason"] == "already_configured" -async def test_invalid_api_key(hass, config): - """Test that an invalid API key throws an error.""" - with patch( - "homeassistant.components.openuv.Client.uv_index", - side_effect=InvalidApiKeyError, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} - - -def _get_schema_marker(data_schema: vol.Schema, key: str) -> vol.Marker: - for k in data_schema.schema: - if k == key and isinstance(k, vol.Marker): - return k - return None - - -async def test_options_flow(hass, config_entry): +async def test_options_flow(hass, config_entry, setup_config_entry): """Test config flow options.""" - with patch("homeassistant.components.openuv.async_setup_entry", return_value=True): - await hass.config_entries.async_setup(config_entry.entry_id) - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "init" - # Original schema uses defaults for suggested values - assert _get_schema_marker( - result["data_schema"], CONF_FROM_WINDOW - ).description == {"suggested_value": 3.5} - assert _get_schema_marker( - result["data_schema"], CONF_TO_WINDOW - ).description == {"suggested_value": 3.5} + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "init" - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 2.0} - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert config_entry.options == {CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 2.0} + def get_schema_marker(data_schema: vol.Schema, key: str) -> vol.Marker: + for k in data_schema.schema: + if k == key and isinstance(k, vol.Marker): + return k + return None - # Subsequent schema uses previous input for suggested values - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "init" - assert _get_schema_marker( - result["data_schema"], CONF_FROM_WINDOW - ).description == {"suggested_value": 3.5} - assert _get_schema_marker( - result["data_schema"], CONF_TO_WINDOW - ).description == {"suggested_value": 2.0} + # Original schema uses defaults for suggested values: + assert get_schema_marker(result["data_schema"], CONF_FROM_WINDOW).description == { + "suggested_value": 3.5 + } + assert get_schema_marker(result["data_schema"], CONF_TO_WINDOW).description == { + "suggested_value": 3.5 + } + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 2.0} + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert config_entry.options == {CONF_FROM_WINDOW: 3.5, CONF_TO_WINDOW: 2.0} + + # Subsequent schema uses previous input for suggested values: + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "init" + assert get_schema_marker(result["data_schema"], CONF_FROM_WINDOW).description == { + "suggested_value": 3.5 + } + assert get_schema_marker(result["data_schema"], CONF_TO_WINDOW).description == { + "suggested_value": 2.0 + } -async def test_step_reauth(hass, config, config_entry, setup_openuv): +async def test_step_reauth(hass, config, config_entry, setup_config_entry): """Test that the reauth step works.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data=config @@ -88,33 +106,9 @@ async def test_step_reauth(hass, config, config_entry, setup_openuv): assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" - with patch("homeassistant.components.openuv.async_setup_entry", return_value=True): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_API_KEY: "new_api_key"} - ) - await hass.async_block_till_done() - + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={CONF_API_KEY: "new_api_key"} + ) assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 - - -async def test_step_user(hass, config, setup_openuv): - """Test that the user step works.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "51.528308, -0.3817765" - assert result["data"] == { - CONF_API_KEY: "abcde12345", - CONF_ELEVATION: 0, - CONF_LATITUDE: 51.528308, - CONF_LONGITUDE: -0.3817765, - } diff --git a/tests/components/openuv/test_diagnostics.py b/tests/components/openuv/test_diagnostics.py index 84e8a691255..8dfb44f8694 100644 --- a/tests/components/openuv/test_diagnostics.py +++ b/tests/components/openuv/test_diagnostics.py @@ -5,7 +5,7 @@ from homeassistant.setup import async_setup_component from tests.components.diagnostics import get_diagnostics_for_config_entry -async def test_entry_diagnostics(hass, config_entry, hass_client, setup_openuv): +async def test_entry_diagnostics(hass, config_entry, hass_client, setup_config_entry): """Test config entry diagnostics.""" await async_setup_component(hass, "homeassistant", {}) assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { diff --git a/tests/components/oralb/__init__.py b/tests/components/oralb/__init__.py index 5525a859f21..d3f1b526fb8 100644 --- a/tests/components/oralb/__init__.py +++ b/tests/components/oralb/__init__.py @@ -1,8 +1,12 @@ """Tests for the OralB integration.""" +from bleak.backends.device import BLEDevice +from home_assistant_bluetooth import BluetoothServiceInfoBleak from homeassistant.helpers.service_info.bluetooth import BluetoothServiceInfo +from tests.components.bluetooth import generate_advertisement_data + NOT_ORALB_SERVICE_INFO = BluetoothServiceInfo( name="Not it", address="61DE521B-F0BF-9F44-64D4-75BBE1738105", @@ -33,3 +37,17 @@ ORALB_IO_SERIES_4_SERVICE_INFO = BluetoothServiceInfo( service_data={}, source="local", ) + +ORALB_IO_SERIES_6_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Oral-B Toothbrush", + address="B0:D2:78:20:1D:CF", + device=BLEDevice("B0:D2:78:20:1D:CF", "Oral-B Toothbrush"), + rssi=-56, + manufacturer_data={220: b"\x062k\x02r\x00\x00\x02\x01\x00\x04"}, + service_data={"a0f0ff00-5047-4d53-8208-4f72616c2d42": bytearray(b"1\x00\x00\x00")}, + service_uuids=["a0f0ff00-5047-4d53-8208-4f72616c2d42"], + source="local", + advertisement=generate_advertisement_data(local_name="Not it"), + time=0, + connectable=True, +) diff --git a/tests/components/oralb/conftest.py b/tests/components/oralb/conftest.py index 454cb7af726..690444d3fb1 100644 --- a/tests/components/oralb/conftest.py +++ b/tests/components/oralb/conftest.py @@ -1,8 +1,53 @@ """OralB session fixtures.""" +from unittest import mock + import pytest +class MockServices: + """Mock GATTServicesCollection.""" + + def get_characteristic(self, key: str) -> str: + """Mock GATTServicesCollection.get_characteristic.""" + return key + + +class MockBleakClient: + """Mock BleakClient.""" + + services = MockServices() + + def __init__(self, *args, **kwargs): + """Mock BleakClient.""" + + async def __aenter__(self, *args, **kwargs): + """Mock BleakClient.__aenter__.""" + return self + + async def __aexit__(self, *args, **kwargs): + """Mock BleakClient.__aexit__.""" + + async def connect(self, *args, **kwargs): + """Mock BleakClient.connect.""" + + async def disconnect(self, *args, **kwargs): + """Mock BleakClient.disconnect.""" + + +class MockBleakClientBattery49(MockBleakClient): + """Mock BleakClient that returns a battery level of 49.""" + + async def read_gatt_char(self, *args, **kwargs) -> bytes: + """Mock BleakClient.read_gatt_char.""" + return b"\x31\x00" + + @pytest.fixture(autouse=True) def mock_bluetooth(enable_bluetooth): """Auto mock bluetooth.""" + + with mock.patch( + "oralb_ble.parser.BleakClientWithServiceCache", MockBleakClientBattery49 + ): + yield diff --git a/tests/components/oralb/test_sensor.py b/tests/components/oralb/test_sensor.py index 2122ad9bbff..cdd1d2461d2 100644 --- a/tests/components/oralb/test_sensor.py +++ b/tests/components/oralb/test_sensor.py @@ -4,10 +4,17 @@ from homeassistant.components.oralb.const import DOMAIN from homeassistant.const import ATTR_FRIENDLY_NAME -from . import ORALB_IO_SERIES_4_SERVICE_INFO, ORALB_SERVICE_INFO +from . import ( + ORALB_IO_SERIES_4_SERVICE_INFO, + ORALB_IO_SERIES_6_SERVICE_INFO, + ORALB_SERVICE_INFO, +) from tests.common import MockConfigEntry -from tests.components.bluetooth import inject_bluetooth_service_info +from tests.components.bluetooth import ( + inject_bluetooth_service_info, + inject_bluetooth_service_info_bleak, +) async def test_sensors(hass, entity_registry_enabled_by_default): @@ -24,7 +31,7 @@ async def test_sensors(hass, entity_registry_enabled_by_default): assert len(hass.states.async_all("sensor")) == 0 inject_bluetooth_service_info(hass, ORALB_SERVICE_INFO) await hass.async_block_till_done() - assert len(hass.states.async_all("sensor")) == 8 + assert len(hass.states.async_all("sensor")) == 9 toothbrush_sensor = hass.states.get( "sensor.smart_series_7000_48be_toothbrush_state" @@ -54,7 +61,7 @@ async def test_sensors_io_series_4(hass, entity_registry_enabled_by_default): assert len(hass.states.async_all("sensor")) == 0 inject_bluetooth_service_info(hass, ORALB_IO_SERIES_4_SERVICE_INFO) await hass.async_block_till_done() - assert len(hass.states.async_all("sensor")) == 8 + assert len(hass.states.async_all("sensor")) == 9 toothbrush_sensor = hass.states.get("sensor.io_series_4_48be_mode") toothbrush_sensor_attrs = toothbrush_sensor.attributes @@ -63,3 +70,24 @@ async def test_sensors_io_series_4(hass, entity_registry_enabled_by_default): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_sensors_battery(hass): + """Test receiving battery percentage.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=ORALB_IO_SERIES_6_SERVICE_INFO.address, + ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak(hass, ORALB_IO_SERIES_6_SERVICE_INFO) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 7 + + bat_sensor = hass.states.get("sensor.io_series_6_7_1dcf_battery") + assert bat_sensor.state == "49" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/otbr/__init__.py b/tests/components/otbr/__init__.py new file mode 100644 index 00000000000..ab62c8c6327 --- /dev/null +++ b/tests/components/otbr/__init__.py @@ -0,0 +1,2 @@ +"""Tests for the Open Thread Border Router integration.""" +BASE_URL = "http://core-silabs-multiprotocol:8081" diff --git a/tests/components/otbr/conftest.py b/tests/components/otbr/conftest.py new file mode 100644 index 00000000000..93421660b1f --- /dev/null +++ b/tests/components/otbr/conftest.py @@ -0,0 +1,24 @@ +"""Test fixtures for the Open Thread Border Router integration.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components import otbr + +from tests.common import MockConfigEntry + +CONFIG_ENTRY_DATA = {"url": "http://core-silabs-multiprotocol:8081"} + + +@pytest.fixture(name="otbr_config_entry") +async def otbr_config_entry_fixture(hass): + """Mock Open Thread Border Router config entry.""" + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=otbr.DOMAIN, + options={}, + title="Open Thread Border Router", + ) + config_entry.add_to_hass(hass) + with patch("python_otbr_api.OTBR.get_active_dataset_tlvs"): + assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/otbr/test_config_flow.py b/tests/components/otbr/test_config_flow.py new file mode 100644 index 00000000000..31e5ce3be9b --- /dev/null +++ b/tests/components/otbr/test_config_flow.py @@ -0,0 +1,133 @@ +"""Test the Open Thread Border Router config flow.""" +from http import HTTPStatus +from unittest.mock import patch + +import pytest + +from homeassistant.components import hassio, otbr +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry, MockModule, mock_integration +from tests.test_util.aiohttp import AiohttpClientMocker + +HASSIO_DATA = hassio.HassioServiceInfo( + config={"host": "blah", "port": "bluh"}, + name="blah", + slug="blah", +) + + +async def test_user_flow( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the user flow.""" + url = "http://custom_url:1234" + aioclient_mock.get(f"{url}/node/dataset/active", text="aa") + result = await hass.config_entries.flow.async_init( + otbr.DOMAIN, context={"source": "user"} + ) + + expected_data = {"url": url} + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.otbr.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": url, + }, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Open Thread Border Router" + assert result["data"] == expected_data + assert result["options"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] + assert config_entry.data == expected_data + assert config_entry.options == {} + assert config_entry.title == "Open Thread Border Router" + assert config_entry.unique_id == otbr.DOMAIN + + +async def test_user_flow_404( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the user flow.""" + url = "http://custom_url:1234" + aioclient_mock.get(f"{url}/node/dataset/active", status=HTTPStatus.NOT_FOUND) + result = await hass.config_entries.flow.async_init( + otbr.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": url, + }, + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_hassio_discovery_flow(hass: HomeAssistant) -> None: + """Test the hassio discovery flow.""" + with patch( + "homeassistant.components.otbr.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + otbr.DOMAIN, context={"source": "hassio"}, data=HASSIO_DATA + ) + + expected_data = { + "url": f"http://{HASSIO_DATA.config['host']}:{HASSIO_DATA.config['port']}", + } + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Open Thread Border Router" + assert result["data"] == expected_data + assert result["options"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] + assert config_entry.data == expected_data + assert config_entry.options == {} + assert config_entry.title == "Open Thread Border Router" + assert config_entry.unique_id == otbr.DOMAIN + + +@pytest.mark.parametrize("source", ("hassio", "user")) +async def test_config_flow_single_entry(hass: HomeAssistant, source: str) -> None: + """Test only a single entry is allowed.""" + mock_integration(hass, MockModule("hassio")) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=otbr.DOMAIN, + options={}, + title="Open Thread Border Router", + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.homeassistant_yellow.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + otbr.DOMAIN, context={"source": source} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" + mock_setup_entry.assert_not_called() diff --git a/tests/components/otbr/test_init.py b/tests/components/otbr/test_init.py new file mode 100644 index 00000000000..c31ad274b7b --- /dev/null +++ b/tests/components/otbr/test_init.py @@ -0,0 +1,93 @@ +"""Test the Open Thread Border Router integration.""" + +from http import HTTPStatus + +import pytest + +from homeassistant.components import otbr +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from . import BASE_URL + +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_remove_entry( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry +): + """Test async_get_thread_state.""" + + aioclient_mock.get(f"{BASE_URL}/node/dataset/active", text="0E") + + assert await otbr.async_get_active_dataset_tlvs(hass) == bytes.fromhex("0E") + + config_entry = hass.config_entries.async_entries(otbr.DOMAIN)[0] + await hass.config_entries.async_remove(config_entry.entry_id) + + with pytest.raises(HomeAssistantError): + assert await otbr.async_get_active_dataset_tlvs(hass) + + +async def test_get_active_dataset_tlvs( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry +): + """Test async_get_active_dataset_tlvs.""" + + mock_response = ( + "0E080000000000010000000300001035060004001FFFE00208F642646DA209B1C00708FDF57B5A" + "0FE2AAF60510DE98B5BA1A528FEE049D4B4B01835375030D4F70656E5468726561642048410102" + "25A40410F5DD18371BFD29E1A601EF6FFAD94C030C0402A0F7F8" + ) + + aioclient_mock.get(f"{BASE_URL}/node/dataset/active", text=mock_response) + + assert await otbr.async_get_active_dataset_tlvs(hass) == bytes.fromhex( + mock_response + ) + + +async def test_get_active_dataset_tlvs_empty( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry +): + """Test async_get_active_dataset_tlvs.""" + + aioclient_mock.get(f"{BASE_URL}/node/dataset/active", status=HTTPStatus.NO_CONTENT) + assert await otbr.async_get_active_dataset_tlvs(hass) is None + + +async def test_get_active_dataset_tlvs_addon_not_installed(hass: HomeAssistant): + """Test async_get_active_dataset_tlvs when the multi-PAN addon is not installed.""" + + with pytest.raises(HomeAssistantError): + await otbr.async_get_active_dataset_tlvs(hass) + + +async def test_get_active_dataset_tlvs_404( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry +): + """Test async_get_active_dataset_tlvs with error.""" + + aioclient_mock.get(f"{BASE_URL}/node/dataset/active", status=HTTPStatus.NOT_FOUND) + with pytest.raises(HomeAssistantError): + await otbr.async_get_active_dataset_tlvs(hass) + + +async def test_get_active_dataset_tlvs_201( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry +): + """Test async_get_active_dataset_tlvs with error.""" + + aioclient_mock.get(f"{BASE_URL}/node/dataset/active", status=HTTPStatus.CREATED) + with pytest.raises(HomeAssistantError): + assert await otbr.async_get_active_dataset_tlvs(hass) is None + + +async def test_get_active_dataset_tlvs_invalid( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, otbr_config_entry +): + """Test async_get_active_dataset_tlvs with error.""" + + aioclient_mock.get(f"{BASE_URL}/node/dataset/active", text="unexpected") + with pytest.raises(HomeAssistantError): + assert await otbr.async_get_active_dataset_tlvs(hass) is None diff --git a/tests/components/otbr/test_websocket_api.py b/tests/components/otbr/test_websocket_api.py new file mode 100644 index 00000000000..72eb312aff9 --- /dev/null +++ b/tests/components/otbr/test_websocket_api.py @@ -0,0 +1,96 @@ +"""Test OTBR Websocket API.""" +from unittest.mock import patch + +import pytest + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component + +from . import BASE_URL + +from tests.test_util.aiohttp import AiohttpClientMocker + + +@pytest.fixture +async def websocket_client(hass, hass_ws_client): + """Create a websocket client.""" + return await hass_ws_client(hass) + + +async def test_get_info( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + otbr_config_entry, + websocket_client, +): + """Test async_get_info.""" + + mock_response = ( + "0E080000000000010000000300001035060004001FFFE00208F642646DA209B1C00708FDF57B5A" + "0FE2AAF60510DE98B5BA1A528FEE049D4B4B01835375030D4F70656E5468726561642048410102" + "25A40410F5DD18371BFD29E1A601EF6FFAD94C030C0402A0F7F8" + ) + + aioclient_mock.get(f"{BASE_URL}/node/dataset/active", text=mock_response) + + await websocket_client.send_json( + { + "id": 5, + "type": "otbr/info", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["success"] + assert msg["result"] == { + "url": BASE_URL, + "active_dataset_tlvs": mock_response.lower(), + } + + +async def test_get_info_no_entry( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + websocket_client, +): + """Test async_get_info.""" + await async_setup_component(hass, "otbr", {}) + await websocket_client.send_json( + { + "id": 5, + "type": "otbr/info", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert not msg["success"] + assert msg["error"]["code"] == "not_loaded" + + +async def test_get_info_fetch_fails( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + otbr_config_entry, + websocket_client, +): + """Test async_get_info.""" + await async_setup_component(hass, "otbr", {}) + + with patch( + "homeassistant.components.otbr.OTBRData.get_active_dataset_tlvs", + side_effect=HomeAssistantError, + ): + await websocket_client.send_json( + { + "id": 5, + "type": "otbr/info", + } + ) + msg = await websocket_client.receive_json() + + assert msg["id"] == 5 + assert not msg["success"] + assert msg["error"]["code"] == "get_dataset_failed" diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index d2ed1b64779..ef36d4a9e28 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -277,7 +277,7 @@ BAD_MESSAGE = {"_type": "unsupported", "tst": 1} BAD_JSON_PREFIX = "--$this is bad json#--" BAD_JSON_SUFFIX = "** and it ends here ^^" -# pylint: disable=invalid-name, len-as-condition, redefined-outer-name +# pylint: disable=invalid-name, len-as-condition @pytest.fixture diff --git a/tests/components/p1_monitor/test_config_flow.py b/tests/components/p1_monitor/test_config_flow.py index d7d0608e5b3..98dfe184c13 100644 --- a/tests/components/p1_monitor/test_config_flow.py +++ b/tests/components/p1_monitor/test_config_flow.py @@ -18,7 +18,6 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result with patch( "homeassistant.components.p1_monitor.config_flow.P1Monitor.smartmeter" diff --git a/tests/components/p1_monitor/test_sensor.py b/tests/components/p1_monitor/test_sensor.py index fe9560c9cb6..14ff3b1e519 100644 --- a/tests/components/p1_monitor/test_sensor.py +++ b/tests/components/p1_monitor/test_sensor.py @@ -16,11 +16,11 @@ from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CURRENCY_EURO, - ELECTRIC_CURRENT_AMPERE, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, - VOLUME_LITERS, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfVolume, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -45,7 +45,7 @@ async def test_smartmeter( assert state.state == "877" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Consumption" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes @@ -59,7 +59,7 @@ async def test_smartmeter( state.attributes.get(ATTR_FRIENDLY_NAME) == "Energy Consumption - High Tariff" ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -102,7 +102,9 @@ async def test_phases( assert state.state == "233.6" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Voltage Phase L1" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_POTENTIAL_VOLT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricPotential.VOLT + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE assert ATTR_ICON not in state.attributes @@ -114,7 +116,9 @@ async def test_phases( assert state.state == "1.6" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Current Phase L1" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_AMPERE + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricCurrent.AMPERE + ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT assert ATTR_ICON not in state.attributes @@ -126,7 +130,7 @@ async def test_phases( assert state.state == "315" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Power Consumed Phase L1" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes @@ -160,7 +164,7 @@ async def test_settings( assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" + == f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}" ) state = hass.states.get("sensor.settings_energy_production_price_low") @@ -173,7 +177,7 @@ async def test_settings( assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" + == f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}" ) assert entry.device_id @@ -203,7 +207,7 @@ async def test_watermeter( assert state.state == "112.0" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Consumption Day" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == VOLUME_LITERS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.LITERS assert entry.device_id device_entry = device_registry.async_get(entry.device_id) diff --git a/tests/components/pi_hole/__init__.py b/tests/components/pi_hole/__init__.py index 8658fc84ffe..8295f933d46 100644 --- a/tests/components/pi_hole/__init__.py +++ b/tests/components/pi_hole/__init__.py @@ -4,7 +4,6 @@ from unittest.mock import AsyncMock, MagicMock, patch from hole.exceptions import HoleError from homeassistant.components.pi_hole.const import ( - CONF_STATISTICS_ONLY, DEFAULT_LOCATION, DEFAULT_NAME, DEFAULT_SSL, @@ -100,9 +99,6 @@ CONFIG_ENTRY_WITHOUT_API_KEY = { CONF_SSL: SSL, CONF_VERIFY_SSL: VERIFY_SSL, } - -CONFIG_ENTRY_IMPORTED = {**CONFIG_ENTRY_WITH_API_KEY, CONF_STATISTICS_ONLY: False} - SWITCH_ENTITY_ID = "switch.pi_hole" diff --git a/tests/components/pi_hole/test_config_flow.py b/tests/components/pi_hole/test_config_flow.py index 8c38803de64..05df5c2d322 100644 --- a/tests/components/pi_hole/test_config_flow.py +++ b/tests/components/pi_hole/test_config_flow.py @@ -1,16 +1,13 @@ """Test pi_hole config flow.""" -import logging - +from homeassistant.components import pi_hole from homeassistant.components.pi_hole.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from . import ( - CONFIG_DATA, CONFIG_DATA_DEFAULTS, - CONFIG_ENTRY_IMPORTED, CONFIG_ENTRY_WITH_API_KEY, CONFIG_ENTRY_WITHOUT_API_KEY, CONFIG_FLOW_API_KEY, @@ -26,37 +23,6 @@ from . import ( from tests.common import MockConfigEntry -async def test_flow_import(hass: HomeAssistant, caplog): - """Test import flow.""" - mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_setup_hole(): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=CONFIG_DATA - ) - assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == NAME - assert result["data"] == CONFIG_ENTRY_IMPORTED - - # duplicated server - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=CONFIG_DATA - ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" - - -async def test_flow_import_invalid(hass: HomeAssistant, caplog): - """Test import flow with invalid server.""" - mocked_hole = _create_mocked_hole(True) - with _patch_config_flow_hole(mocked_hole), _patch_setup_hole(): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=CONFIG_DATA - ) - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "cannot_connect" - assert len([x for x in caplog.records if x.levelno == logging.ERROR]) == 1 - - async def test_flow_user_with_api_key(hass: HomeAssistant): """Test user initialized flow with api key needed.""" mocked_hole = _create_mocked_hole(has_data=False) @@ -142,7 +108,7 @@ async def test_flow_user_invalid(hass: HomeAssistant): async def test_flow_reauth(hass: HomeAssistant): """Test reauth flow.""" mocked_hole = _create_mocked_hole(has_data=False) - entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_DATA_DEFAULTS) + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONFIG_DATA_DEFAULTS) entry.add_to_hass(hass) with _patch_init_hole(mocked_hole), _patch_config_flow_hole(mocked_hole): assert not await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/pi_hole/test_init.py b/tests/components/pi_hole/test_init.py index b863898db25..52ca64a63af 100644 --- a/tests/components/pi_hole/test_init.py +++ b/tests/components/pi_hole/test_init.py @@ -10,30 +10,29 @@ from homeassistant.components.pi_hole.const import ( SERVICE_DISABLE, SERVICE_DISABLE_ATTR_DURATION, ) -from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from . import ( + CONFIG_DATA, CONFIG_DATA_DEFAULTS, SWITCH_ENTITY_ID, _create_mocked_hole, - _patch_config_flow_hole, _patch_init_hole, ) from tests.common import MockConfigEntry -async def test_setup_minimal_config(hass: HomeAssistant): - """Tests component setup with minimal config.""" +async def test_setup_with_defaults(hass: HomeAssistant): + """Tests component setup with default config.""" mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): - assert await async_setup_component( - hass, pi_hole.DOMAIN, {pi_hole.DOMAIN: [{"host": "pi.hole"}]} - ) - - await hass.async_block_till_done() + entry = MockConfigEntry( + domain=pi_hole.DOMAIN, data={**CONFIG_DATA_DEFAULTS, CONF_STATISTICS_ONLY: True} + ) + entry.add_to_hass(hass) + with _patch_init_hole(mocked_hole): + assert await hass.config_entries.async_setup(entry.entry_id) state = hass.states.get("sensor.pi_hole_ads_blocked_today") assert state.name == "Pi-Hole Ads Blocked Today" @@ -79,12 +78,12 @@ async def test_setup_minimal_config(hass: HomeAssistant): async def test_setup_name_config(hass: HomeAssistant): """Tests component setup with a custom name.""" mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): - assert await async_setup_component( - hass, - pi_hole.DOMAIN, - {pi_hole.DOMAIN: [{"host": "pi.hole", "name": "Custom"}]}, - ) + entry = MockConfigEntry( + domain=pi_hole.DOMAIN, data={**CONFIG_DATA_DEFAULTS, CONF_NAME: "Custom"} + ) + entry.add_to_hass(hass) + with _patch_init_hole(mocked_hole): + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -97,12 +96,11 @@ async def test_setup_name_config(hass: HomeAssistant): async def test_switch(hass: HomeAssistant, caplog): """Test Pi-hole switch.""" mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): - assert await async_setup_component( - hass, - pi_hole.DOMAIN, - {pi_hole.DOMAIN: [{"host": "pi.hole1", "api_key": "1"}]}, - ) + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONFIG_DATA) + entry.add_to_hass(hass) + + with _patch_init_hole(mocked_hole): + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -144,18 +142,18 @@ async def test_switch(hass: HomeAssistant, caplog): async def test_disable_service_call(hass: HomeAssistant): """Test disable service call with no Pi-hole named.""" + mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): - assert await async_setup_component( - hass, - pi_hole.DOMAIN, - { - pi_hole.DOMAIN: [ - {"host": "pi.hole1", "api_key": "1"}, - {"host": "pi.hole2", "name": "Custom"}, - ] - }, + with _patch_init_hole(mocked_hole): + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONFIG_DATA) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + + entry = MockConfigEntry( + domain=pi_hole.DOMAIN, data={**CONFIG_DATA_DEFAULTS, CONF_NAME: "Custom"} ) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -179,13 +177,14 @@ async def test_unload(hass: HomeAssistant): ) entry.add_to_hass(hass) mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): + with _patch_init_hole(mocked_hole): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert entry.entry_id in hass.data[pi_hole.DOMAIN] - assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert entry.entry_id not in hass.data[pi_hole.DOMAIN] diff --git a/tests/components/pi_hole/test_update.py b/tests/components/pi_hole/test_update.py index 8da4ec263d5..62b7410544c 100644 --- a/tests/components/pi_hole/test_update.py +++ b/tests/components/pi_hole/test_update.py @@ -2,18 +2,20 @@ from homeassistant.components import pi_hole from homeassistant.const import STATE_ON, STATE_UNKNOWN -from homeassistant.setup import async_setup_component +from homeassistant.core import HomeAssistant -from . import _create_mocked_hole, _patch_config_flow_hole, _patch_init_hole +from . import CONFIG_DATA_DEFAULTS, _create_mocked_hole, _patch_init_hole + +from tests.common import MockConfigEntry -async def test_update(hass): +async def test_update(hass: HomeAssistant): """Tests update entity.""" mocked_hole = _create_mocked_hole() - with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): - assert await async_setup_component( - hass, pi_hole.DOMAIN, {pi_hole.DOMAIN: [{"host": "pi.hole"}]} - ) + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONFIG_DATA_DEFAULTS) + entry.add_to_hass(hass) + with _patch_init_hole(mocked_hole): + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() @@ -48,13 +50,13 @@ async def test_update(hass): ) -async def test_update_no_versions(hass): +async def test_update_no_versions(hass: HomeAssistant): """Tests update entity when no version data available.""" mocked_hole = _create_mocked_hole(has_versions=False) - with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): - assert await async_setup_component( - hass, pi_hole.DOMAIN, {pi_hole.DOMAIN: [{"host": "pi.hole"}]} - ) + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONFIG_DATA_DEFAULTS) + entry.add_to_hass(hass) + with _patch_init_hole(mocked_hole): + assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index 0c70a2bed6d..f0a114a3de4 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -247,6 +247,31 @@ def mock_smile_p1() -> Generator[None, MagicMock, None]: yield smile +@pytest.fixture +def mock_smile_p1_2() -> Generator[None, MagicMock, None]: + """Create a Mock P1 3-phase DSMR environment for testing exceptions.""" + chosen_env = "p1v4_3ph" + with patch( + "homeassistant.components.plugwise.coordinator.Smile", autospec=True + ) as smile_mock: + smile = smile_mock.return_value + + smile.gateway_id = "03e65b16e4b247a29ae0d75a78cb492e" + smile.heater_id = None + smile.smile_version = "4.4.2" + smile.smile_type = "power" + smile.smile_hostname = "smile98765" + smile.smile_model = "Gateway" + smile.smile_name = "Smile P1" + + smile.connect.return_value = True + + 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.""" diff --git a/tests/components/plugwise/fixtures/p1v4_3ph/all_data.json b/tests/components/plugwise/fixtures/p1v4_3ph/all_data.json new file mode 100644 index 00000000000..852ca2857cd --- /dev/null +++ b/tests/components/plugwise/fixtures/p1v4_3ph/all_data.json @@ -0,0 +1,57 @@ +[ + { + "smile_name": "Smile P1", + "gateway_id": "03e65b16e4b247a29ae0d75a78cb492e", + "notifications": {} + }, + { + "03e65b16e4b247a29ae0d75a78cb492e": { + "dev_class": "gateway", + "firmware": "4.4.2", + "hardware": "AME Smile 2.0 board", + "location": "03e65b16e4b247a29ae0d75a78cb492e", + "mac_address": "012345670001", + "model": "Gateway", + "name": "Smile P1", + "vendor": "Plugwise", + "binary_sensors": { + "plugwise_notification": false + } + }, + "b82b6b3322484f2ea4e25e0bd5f3d61f": { + "dev_class": "smartmeter", + "location": "03e65b16e4b247a29ae0d75a78cb492e", + "model": "XMX5LGF0010453051839", + "name": "P1", + "vendor": "XEMEX NV", + "available": true, + "sensors": { + "net_electricity_point": 5553, + "electricity_consumed_peak_point": 0, + "electricity_consumed_off_peak_point": 5553, + "net_electricity_cumulative": 231866.539, + "electricity_consumed_peak_cumulative": 161328.641, + "electricity_consumed_off_peak_cumulative": 70537.898, + "electricity_consumed_peak_interval": 0, + "electricity_consumed_off_peak_interval": 314, + "electricity_produced_peak_point": 0, + "electricity_produced_off_peak_point": 0, + "electricity_produced_peak_cumulative": 0.0, + "electricity_produced_off_peak_cumulative": 0.0, + "electricity_produced_peak_interval": 0, + "electricity_produced_off_peak_interval": 0, + "electricity_phase_one_consumed": 1763, + "electricity_phase_two_consumed": 1703, + "electricity_phase_three_consumed": 2080, + "electricity_phase_one_produced": 0, + "electricity_phase_two_produced": 0, + "electricity_phase_three_produced": 0, + "gas_consumed_cumulative": 16811.37, + "gas_consumed_interval": 0.06, + "voltage_phase_one": 233.2, + "voltage_phase_two": 234.4, + "voltage_phase_three": 234.7 + } + } + } +] diff --git a/tests/components/plugwise/fixtures/p1v4_3ph/notifications.json b/tests/components/plugwise/fixtures/p1v4_3ph/notifications.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/tests/components/plugwise/fixtures/p1v4_3ph/notifications.json @@ -0,0 +1 @@ +{} diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index b569cb08ddf..200ab304ce7 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -121,7 +121,6 @@ async def test_form( assert result.get("type") == FlowResultType.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"], @@ -169,7 +168,6 @@ async def test_zeroconf_flow( assert result.get("type") == FlowResultType.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"], @@ -205,7 +203,6 @@ async def test_zeroconf_flow_stretch( assert result.get("type") == FlowResultType.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"], @@ -297,7 +294,6 @@ async def test_flow_errors( assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == "user" - assert "flow_id" in result mock_smile_config_flow.connect.side_effect = side_effect result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py index b08c2113d80..0c7483c19bd 100644 --- a/tests/components/plugwise/test_sensor.py +++ b/tests/components/plugwise/test_sensor.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_component import async_update_entity +from homeassistant.helpers.entity_registry import async_get from tests.common import MockConfigEntry @@ -94,6 +95,38 @@ async def test_p1_dsmr_sensor_entities( assert float(state.state) == 584.85 +async def test_p1_3ph_dsmr_sensor_entities( + hass: HomeAssistant, mock_smile_p1_2: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test creation of power related sensor entities.""" + state = hass.states.get("sensor.p1_electricity_phase_one_consumed") + assert state + assert float(state.state) == 1763.0 + + state = hass.states.get("sensor.p1_electricity_phase_two_consumed") + assert state + assert float(state.state) == 1703.0 + + state = hass.states.get("sensor.p1_electricity_phase_three_consumed") + assert state + assert float(state.state) == 2080.0 + + entity_id = "sensor.p1_voltage_phase_one" + state = hass.states.get(entity_id) + assert not state + + entity_registry = async_get(hass) + entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None) + await hass.async_block_till_done() + + await hass.config_entries.async_reload(init_integration.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.p1_voltage_phase_one") + assert state + assert float(state.state) == 233.2 + + async def test_stretch_sensor_entities( hass: HomeAssistant, mock_stretch: MagicMock, init_integration: MockConfigEntry ) -> None: diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index e9587592175..c8172ea3b6f 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -13,7 +13,7 @@ def init_config_flow(hass, side_effect=None): """Init a configuration flow.""" config_flow.register_flow_implementation(hass, DOMAIN, "id", "secret") flow = config_flow.PointFlowHandler() - flow._get_authorization_url = AsyncMock( # pylint: disable=protected-access + flow._get_authorization_url = AsyncMock( return_value="https://example.com", side_effect=side_effect ) flow.hass = hass @@ -27,7 +27,7 @@ def is_authorized(): @pytest.fixture -def mock_pypoint(is_authorized): # pylint: disable=redefined-outer-name +def mock_pypoint(is_authorized): """Mock pypoint.""" with patch( "homeassistant.components.point.config_flow.PointSession" @@ -67,9 +67,7 @@ async def test_abort_if_already_setup(hass): assert result["reason"] == "already_setup" -async def test_full_flow_implementation( - hass, mock_pypoint # pylint: disable=redefined-outer-name -): +async def test_full_flow_implementation(hass, mock_pypoint): """Test registering an implementation and finishing flow works.""" config_flow.register_flow_implementation(hass, "test-other", None, None) flow = init_config_flow(hass) @@ -95,7 +93,7 @@ async def test_full_flow_implementation( assert result["data"]["token"] == {"access_token": "boo"} -async def test_step_import(hass, mock_pypoint): # pylint: disable=redefined-outer-name +async def test_step_import(hass, mock_pypoint): """Test that we trigger import when configuring with client.""" flow = init_config_flow(hass) @@ -105,9 +103,7 @@ async def test_step_import(hass, mock_pypoint): # pylint: disable=redefined-out @pytest.mark.parametrize("is_authorized", [False]) -async def test_wrong_code_flow_implementation( - hass, mock_pypoint -): # pylint: disable=redefined-outer-name +async def test_wrong_code_flow_implementation(hass, mock_pypoint): """Test wrong code.""" flow = init_config_flow(hass) diff --git a/tests/components/powerwall/test_switch.py b/tests/components/powerwall/test_switch.py new file mode 100644 index 00000000000..bc1e6cd1e52 --- /dev/null +++ b/tests/components/powerwall/test_switch.py @@ -0,0 +1,104 @@ +"""Test for Powerwall off-grid switch.""" + +from unittest.mock import Mock, patch + +import pytest +from tesla_powerwall import GridStatus, PowerwallError + +from homeassistant.components.powerwall.const import DOMAIN +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ATTR_ENTITY_ID, CONF_IP_ADDRESS, STATE_OFF, STATE_ON +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as ent_reg + +from .mocks import _mock_powerwall_with_fixtures + +from tests.common import MockConfigEntry + +ENTITY_ID = "switch.mysite_off_grid_operation" + + +@pytest.fixture(name="mock_powerwall") +async def mock_powerwall_fixture(hass): + """Set up base powerwall fixture.""" + + mock_powerwall = await _mock_powerwall_with_fixtures(hass) + + config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"}) + config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield mock_powerwall + + +async def test_entity_registry(hass, mock_powerwall): + """Test powerwall off-grid switch device.""" + + mock_powerwall.get_grid_status = Mock(return_value=GridStatus.CONNECTED) + entity_registry = ent_reg.async_get(hass) + + assert ENTITY_ID in entity_registry.entities + + +async def test_initial(hass, mock_powerwall): + """Test initial grid status without off grid switch selected.""" + + mock_powerwall.get_grid_status = Mock(return_value=GridStatus.CONNECTED) + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + + +async def test_on(hass, mock_powerwall): + """Test state once offgrid switch has been turned on.""" + + mock_powerwall.get_grid_status = Mock(return_value=GridStatus.ISLANDED) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + + +async def test_off(hass, mock_powerwall): + """Test state once offgrid switch has been turned off.""" + + mock_powerwall.get_grid_status = Mock(return_value=GridStatus.CONNECTED) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + + +async def test_exception_on_powerwall_error(hass, mock_powerwall): + """Ensure that an exception in the tesla_powerwall library causes a HomeAssistantError.""" + + with pytest.raises(HomeAssistantError, match="Setting off-grid operation to"): + mock_powerwall.set_island_mode = Mock( + side_effect=PowerwallError("Mock exception") + ) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index e49e0eb779d..d322568f0d5 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -42,7 +42,6 @@ from homeassistant.const import ( CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONTENT_TYPE_TEXT_PLAIN, DEGREE, - ENERGY_KILO_WATT_HOUR, EVENT_STATE_CHANGED, PERCENTAGE, STATE_CLOSED, @@ -55,8 +54,8 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, STATE_UNLOCKED, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfEnergy, + UnitOfTemperature, ) from homeassistant.core import split_entity_id from homeassistant.helpers import entity_registry @@ -898,7 +897,7 @@ async def sensor_fixture(hass, registry): domain=sensor.DOMAIN, platform="test", unique_id="sensor_1", - unit_of_measurement=TEMP_CELSIUS, + unit_of_measurement=UnitOfTemperature.CELSIUS, original_device_class=SensorDeviceClass.TEMPERATURE, suggested_object_id="outside_temperature", original_name="Outside Temperature", @@ -924,7 +923,7 @@ async def sensor_fixture(hass, registry): domain=sensor.DOMAIN, platform="test", unique_id="sensor_3", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, original_device_class=SensorDeviceClass.POWER, suggested_object_id="radio_energy", original_name="Radio Energy", @@ -940,7 +939,7 @@ async def sensor_fixture(hass, registry): domain=sensor.DOMAIN, platform="test", unique_id="sensor_4", - unit_of_measurement=ENERGY_KILO_WATT_HOUR, + unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_object_id="television_energy", original_name="Television Energy", ) @@ -951,7 +950,7 @@ async def sensor_fixture(hass, registry): domain=sensor.DOMAIN, platform="test", unique_id="sensor_5", - unit_of_measurement=f"SEK/{ENERGY_KILO_WATT_HOUR}", + unit_of_measurement=f"SEK/{UnitOfEnergy.KILO_WATT_HOUR}", suggested_object_id="electricity_price", original_name="Electricity price", ) @@ -1015,7 +1014,7 @@ async def sensor_fixture(hass, registry): domain=sensor.DOMAIN, platform="test", unique_id="sensor_11", - unit_of_measurement=TEMP_FAHRENHEIT, + unit_of_measurement=UnitOfTemperature.FAHRENHEIT, original_device_class=SensorDeviceClass.TEMPERATURE, suggested_object_id="fahrenheit", original_name="Fahrenheit", @@ -1035,7 +1034,7 @@ async def climate_fixture(hass, registry): domain=climate.DOMAIN, platform="test", unique_id="climate_1", - unit_of_measurement=TEMP_CELSIUS, + unit_of_measurement=UnitOfTemperature.CELSIUS, suggested_object_id="heatpump", original_name="HeatPump", ) @@ -1054,7 +1053,7 @@ async def climate_fixture(hass, registry): domain=climate.DOMAIN, platform="test", unique_id="climate_2", - unit_of_measurement=TEMP_CELSIUS, + unit_of_measurement=UnitOfTemperature.CELSIUS, suggested_object_id="ecobee", original_name="Ecobee", ) @@ -1075,7 +1074,7 @@ async def climate_fixture(hass, registry): domain=climate.DOMAIN, platform="test", unique_id="climate_3", - unit_of_measurement=TEMP_CELSIUS, + unit_of_measurement=UnitOfTemperature.CELSIUS, suggested_object_id="fritzdect", original_name="Fritz!DECT", ) @@ -1275,7 +1274,7 @@ async def input_number_fixture(hass, registry): unique_id="input_number_3", suggested_object_id="target_temperature", original_name="Target temperature", - unit_of_measurement=TEMP_CELSIUS, + unit_of_measurement=UnitOfTemperature.CELSIUS, ) set_state_with_entry(hass, input_number_3, 22.7) data["input_number_3"] = input_number_3 diff --git a/tests/components/prusalink/test_sensor.py b/tests/components/prusalink/test_sensor.py index f5e4b801d30..7e2a1349c3c 100644 --- a/tests/components/prusalink/test_sensor.py +++ b/tests/components/prusalink/test_sensor.py @@ -14,8 +14,8 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, - TEMP_CELSIUS, Platform, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -52,14 +52,14 @@ async def test_sensors_no_job(hass: HomeAssistant, mock_config_entry, mock_api): state = hass.states.get("sensor.mock_title_heatbed") assert state is not None assert state.state == "41.9" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT state = hass.states.get("sensor.mock_title_nozzle_temperature") assert state is not None assert state.state == "47.8" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT diff --git a/tests/components/pure_energie/test_config_flow.py b/tests/components/pure_energie/test_config_flow.py index d1ed8eeb578..ebd7aefff20 100644 --- a/tests/components/pure_energie/test_config_flow.py +++ b/tests/components/pure_energie/test_config_flow.py @@ -24,7 +24,6 @@ async def test_full_user_flow_implementation( assert result.get("step_id") == SOURCE_USER assert result.get("type") == FlowResultType.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"} @@ -64,7 +63,6 @@ async def test_full_zeroconf_flow_implementationn( } assert result.get("step_id") == "zeroconf_confirm" assert result.get("type") == FlowResultType.FORM - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} diff --git a/tests/components/pure_energie/test_sensor.py b/tests/components/pure_energie/test_sensor.py index 60894ac09f8..2881bf28d8f 100644 --- a/tests/components/pure_energie/test_sensor.py +++ b/tests/components/pure_energie/test_sensor.py @@ -11,8 +11,8 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - ENERGY_KILO_WATT_HOUR, - POWER_WATT, + UnitOfEnergy, + UnitOfPower, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -36,7 +36,7 @@ async def test_sensors( 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_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -48,7 +48,7 @@ async def test_sensors( 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_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert ATTR_ICON not in state.attributes @@ -60,7 +60,7 @@ async def test_sensors( 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_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER assert ATTR_ICON not in state.attributes diff --git a/tests/components/purpleair/conftest.py b/tests/components/purpleair/conftest.py index c19ff62fdb7..e5d6376a208 100644 --- a/tests/components/purpleair/conftest.py +++ b/tests/components/purpleair/conftest.py @@ -6,24 +6,29 @@ from aiopurpleair.models.sensors import GetSensorsResponse import pytest from homeassistant.components.purpleair import DOMAIN -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture +TEST_API_KEY = "abcde12345" +TEST_SENSOR_INDEX1 = 123456 +TEST_SENSOR_INDEX2 = 567890 + @pytest.fixture(name="api") -def api_fixture(check_api_key, get_nearby_sensors, get_sensors): +def api_fixture(get_sensors_response): """Define a fixture to return a mocked aiopurple API object.""" - api = Mock(async_check_api_key=check_api_key) - api.sensors.async_get_nearby_sensors = get_nearby_sensors - api.sensors.async_get_sensors = get_sensors - return api - - -@pytest.fixture(name="check_api_key") -def check_api_key_fixture(): - """Define a fixture to mock the method to check an API key's validity.""" - return AsyncMock() + return Mock( + async_check_api_key=AsyncMock(), + sensors=Mock( + async_get_nearby_sensors=AsyncMock( + return_value=[ + NearbySensorResult(sensor=sensor, distance=1.0) + for sensor in get_sensors_response.data.values() + ] + ), + async_get_sensors=AsyncMock(return_value=get_sensors_response), + ), + ) @pytest.fixture(name="config_entry") @@ -32,7 +37,7 @@ def config_entry_fixture(hass, config_entry_data, config_entry_options): entry = MockConfigEntry( domain=DOMAIN, title="abcde", - unique_id="abcde12345", + unique_id=TEST_API_KEY, data=config_entry_data, options=config_entry_options, ) @@ -44,7 +49,7 @@ def config_entry_fixture(hass, config_entry_data, config_entry_options): def config_entry_data_fixture(): """Define a config entry data fixture.""" return { - "api_key": "abcde12345", + "api_key": TEST_API_KEY, } @@ -52,27 +57,10 @@ def config_entry_data_fixture(): def config_entry_options_fixture(): """Define a config entry options fixture.""" return { - "sensor_indices": [123456], + "sensor_indices": [TEST_SENSOR_INDEX1], } -@pytest.fixture(name="get_nearby_sensors") -def get_nearby_sensors_fixture(get_sensors_response): - """Define a mocked API.sensors.async_get_nearby_sensors.""" - return AsyncMock( - return_value=[ - NearbySensorResult(sensor=sensor, distance=1.0) - for sensor in get_sensors_response.data.values() - ] - ) - - -@pytest.fixture(name="get_sensors") -def get_sensors_fixture(get_sensors_response): - """Define a mocked API.sensors.async_get_sensors.""" - return AsyncMock(return_value=get_sensors_response) - - @pytest.fixture(name="get_sensors_response", scope="package") def get_sensors_response_fixture(): """Define a fixture to mock an aiopurpleair GetSensorsResponse object.""" @@ -81,12 +69,18 @@ def get_sensors_response_fixture(): ) -@pytest.fixture(name="setup_purpleair") -async def setup_purpleair_fixture(hass, api, config_entry_data): - """Define a fixture to set up PurpleAir.""" +@pytest.fixture(name="mock_aiopurpleair") +async def mock_aiopurpleair_fixture(api): + """Define a fixture to patch aiopurpleair.""" with patch( "homeassistant.components.purpleair.config_flow.API", return_value=api ), patch("homeassistant.components.purpleair.coordinator.API", return_value=api): - assert await async_setup_component(hass, DOMAIN, config_entry_data) - await hass.async_block_till_done() yield + + +@pytest.fixture(name="setup_config_entry") +async def setup_config_entry_fixture(hass, config_entry, mock_aiopurpleair): + """Define a fixture to set up purpleair.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield diff --git a/tests/components/purpleair/test_config_flow.py b/tests/components/purpleair/test_config_flow.py index 2f4af57a3c5..066706afb50 100644 --- a/tests/components/purpleair/test_config_flow.py +++ b/tests/components/purpleair/test_config_flow.py @@ -9,14 +9,10 @@ from homeassistant.components.purpleair import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.helpers import device_registry as dr +from .conftest import TEST_API_KEY, TEST_SENSOR_INDEX1, TEST_SENSOR_INDEX2 -async def test_duplicate_error(hass, config_entry, setup_purpleair): - """Test that the proper error is shown when adding a duplicate config entry.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data={"api_key": "abcde12345"} - ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "already_configured" +TEST_LATITUDE = 51.5285582 +TEST_LONGITUDE = -0.2416796 @pytest.mark.parametrize( @@ -42,7 +38,7 @@ async def test_create_entry_by_coordinates( check_api_key_mock, get_nearby_sensors_errors, get_nearby_sensors_mock, - setup_purpleair, + mock_aiopurpleair, ): """Test creating an entry by entering a latitude/longitude (including errors).""" result = await hass.config_entries.flow.async_init( @@ -54,13 +50,13 @@ async def test_create_entry_by_coordinates( # Test errors that can arise when checking the API key: with patch.object(api, "async_check_api_key", check_api_key_mock): result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"api_key": "abcde12345"} + result["flow_id"], user_input={"api_key": TEST_API_KEY} ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == check_api_key_errors result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"api_key": "abcde12345"} + result["flow_id"], user_input={"api_key": TEST_API_KEY} ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "by_coordinates" @@ -70,8 +66,8 @@ async def test_create_entry_by_coordinates( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - "latitude": 51.5285582, - "longitude": -0.2416796, + "latitude": TEST_LATITUDE, + "longitude": TEST_LONGITUDE, "distance": 5, }, ) @@ -81,8 +77,8 @@ async def test_create_entry_by_coordinates( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - "latitude": 51.5285582, - "longitude": -0.2416796, + "latitude": TEST_LATITUDE, + "longitude": TEST_LONGITUDE, "distance": 5, }, ) @@ -92,19 +88,28 @@ async def test_create_entry_by_coordinates( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - "sensor_index": "123456", + "sensor_index": str(TEST_SENSOR_INDEX1), }, ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == "abcde" assert result["data"] == { - "api_key": "abcde12345", + "api_key": TEST_API_KEY, } assert result["options"] == { - "sensor_indices": [123456], + "sensor_indices": [TEST_SENSOR_INDEX1], } +async def test_duplicate_error(hass, config_entry, setup_config_entry): + """Test that the proper error is shown when adding a duplicate config entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data={"api_key": TEST_API_KEY} + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + + @pytest.mark.parametrize( "check_api_key_mock,check_api_key_errors", [ @@ -114,7 +119,12 @@ async def test_create_entry_by_coordinates( ], ) async def test_reauth( - hass, api, check_api_key_errors, check_api_key_mock, config_entry, setup_purpleair + hass, + api, + check_api_key_errors, + check_api_key_mock, + config_entry, + setup_config_entry, ): """Test re-auth (including errors).""" result = await hass.config_entries.flow.async_init( @@ -124,7 +134,7 @@ async def test_reauth( "entry_id": config_entry.entry_id, "unique_id": config_entry.unique_id, }, - data={"api_key": "abcde12345"}, + data={"api_key": TEST_API_KEY}, ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" @@ -160,7 +170,7 @@ async def test_options_add_sensor( config_entry, get_nearby_sensors_errors, get_nearby_sensors_mock, - setup_purpleair, + setup_config_entry, ): """Test adding a sensor via the options flow (including errors).""" result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -178,8 +188,8 @@ async def test_options_add_sensor( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "latitude": 51.5285582, - "longitude": -0.2416796, + "latitude": TEST_LATITUDE, + "longitude": TEST_LONGITUDE, "distance": 5, }, ) @@ -189,8 +199,8 @@ async def test_options_add_sensor( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "latitude": 51.5285582, - "longitude": -0.2416796, + "latitude": TEST_LATITUDE, + "longitude": TEST_LONGITUDE, "distance": 5, }, ) @@ -200,18 +210,21 @@ async def test_options_add_sensor( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "sensor_index": "567890", + "sensor_index": str(TEST_SENSOR_INDEX2), }, ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == { - "sensor_indices": [123456, 567890], + "sensor_indices": [TEST_SENSOR_INDEX1, TEST_SENSOR_INDEX2], } - assert config_entry.options["sensor_indices"] == [123456, 567890] + assert config_entry.options["sensor_indices"] == [ + TEST_SENSOR_INDEX1, + TEST_SENSOR_INDEX2, + ] -async def test_options_add_sensor_duplicate(hass, config_entry, setup_purpleair): +async def test_options_add_sensor_duplicate(hass, config_entry, setup_config_entry): """Test adding a duplicate sensor via the options flow.""" result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.FlowResultType.MENU @@ -226,8 +239,8 @@ async def test_options_add_sensor_duplicate(hass, config_entry, setup_purpleair) result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "latitude": 51.5285582, - "longitude": -0.2416796, + "latitude": TEST_LATITUDE, + "longitude": TEST_LONGITUDE, "distance": 5, }, ) @@ -237,14 +250,14 @@ async def test_options_add_sensor_duplicate(hass, config_entry, setup_purpleair) result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - "sensor_index": "123456", + "sensor_index": str(TEST_SENSOR_INDEX1), }, ) assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "already_configured" -async def test_options_remove_sensor(hass, config_entry, setup_purpleair): +async def test_options_remove_sensor(hass, config_entry, setup_config_entry): """Test removing a sensor via the options flow.""" result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.FlowResultType.MENU @@ -257,7 +270,7 @@ async def test_options_remove_sensor(hass, config_entry, setup_purpleair): assert result["step_id"] == "remove_sensor" device_registry = dr.async_get(hass) - device_entry = device_registry.async_get_device({(DOMAIN, "123456")}) + device_entry = device_registry.async_get_device({(DOMAIN, str(TEST_SENSOR_INDEX1))}) result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"sensor_device_id": device_entry.id}, diff --git a/tests/components/purpleair/test_diagnostics.py b/tests/components/purpleair/test_diagnostics.py index ee17a2889b8..4ca4934236a 100644 --- a/tests/components/purpleair/test_diagnostics.py +++ b/tests/components/purpleair/test_diagnostics.py @@ -4,7 +4,7 @@ from homeassistant.components.diagnostics import REDACTED from tests.components.diagnostics import get_diagnostics_for_config_entry -async def test_entry_diagnostics(hass, config_entry, hass_client, setup_purpleair): +async def test_entry_diagnostics(hass, config_entry, hass_client, setup_config_entry): """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { diff --git a/tests/components/pushover/test_init.py b/tests/components/pushover/test_init.py index 635aec520b5..ae49881a741 100644 --- a/tests/components/pushover/test_init.py +++ b/tests/components/pushover/test_init.py @@ -1,6 +1,5 @@ """Test pushbullet integration.""" -from collections.abc import Awaitable -from typing import Callable +from collections.abc import Awaitable, Callable from unittest.mock import MagicMock, patch import aiohttp diff --git a/tests/components/pvoutput/test_config_flow.py b/tests/components/pvoutput/test_config_flow.py index 9d6162e4d46..36a783f86fb 100644 --- a/tests/components/pvoutput/test_config_flow.py +++ b/tests/components/pvoutput/test_config_flow.py @@ -25,7 +25,6 @@ async def test_full_user_flow( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -62,7 +61,6 @@ async def test_full_flow_with_authentication_error( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_pvoutput_config_flow.system.side_effect = PVOutputAuthenticationError result2 = await hass.config_entries.flow.async_configure( @@ -76,7 +74,6 @@ async def test_full_flow_with_authentication_error( assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": "invalid_auth"} - assert "flow_id" in result2 assert len(mock_setup_entry.mock_calls) == 0 assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 @@ -133,7 +130,6 @@ async def test_already_configured( 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"], @@ -167,7 +163,6 @@ async def test_reauth_flow( ) assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -210,7 +205,6 @@ async def test_reauth_with_authentication_error( ) assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" - assert "flow_id" in result mock_pvoutput_config_flow.system.side_effect = PVOutputAuthenticationError result2 = await hass.config_entries.flow.async_configure( @@ -222,7 +216,6 @@ async def test_reauth_with_authentication_error( assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "invalid_auth"} - assert "flow_id" in result2 assert len(mock_setup_entry.mock_calls) == 0 assert len(mock_pvoutput_config_flow.system.mock_calls) == 1 @@ -264,7 +257,6 @@ async def test_reauth_api_error( ) assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" - assert "flow_id" in result mock_pvoutput_config_flow.system.side_effect = PVOutputConnectionError result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/pvoutput/test_sensor.py b/tests/components/pvoutput/test_sensor.py index 54d1d3e641f..afba339195a 100644 --- a/tests/components/pvoutput/test_sensor.py +++ b/tests/components/pvoutput/test_sensor.py @@ -10,12 +10,10 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, - ENERGY_WATT_HOUR, - POWER_KILO_WATT, - POWER_WATT, - TEMP_CELSIUS, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -44,7 +42,7 @@ async def test_sensors( == "Frenck's Solar Farm Energy consumed" ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.WATT_HOUR assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.frenck_s_solar_farm_energy_generated") @@ -60,7 +58,7 @@ async def test_sensors( == "Frenck's Solar Farm Energy generated" ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.WATT_HOUR assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.frenck_s_solar_farm_efficiency") @@ -74,7 +72,7 @@ async def test_sensors( assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - == f"{ENERGY_KILO_WATT_HOUR}/{POWER_KILO_WATT}" + == f"{UnitOfEnergy.KILO_WATT_HOUR}/{UnitOfPower.KILO_WATT}" ) assert ATTR_DEVICE_CLASS not in state.attributes assert ATTR_ICON not in state.attributes @@ -91,7 +89,7 @@ async def test_sensors( state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's Solar Farm Power consumed" ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.frenck_s_solar_farm_power_generated") @@ -107,7 +105,7 @@ async def test_sensors( == "Frenck's Solar Farm Power generated" ) assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.frenck_s_solar_farm_temperature") @@ -120,7 +118,7 @@ async def test_sensors( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TEMPERATURE assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's Solar Farm Temperature" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert ATTR_ICON not in state.attributes state = hass.states.get("sensor.frenck_s_solar_farm_voltage") @@ -133,7 +131,9 @@ async def test_sensors( assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.VOLTAGE assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's Solar Farm Voltage" assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_POTENTIAL_VOLT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfElectricPotential.VOLT + ) assert ATTR_ICON not in state.attributes assert entry.device_id diff --git a/tests/components/pvpc_hourly_pricing/conftest.py b/tests/components/pvpc_hourly_pricing/conftest.py index 632284774ee..fb2c9188ce7 100644 --- a/tests/components/pvpc_hourly_pricing/conftest.py +++ b/tests/components/pvpc_hourly_pricing/conftest.py @@ -4,16 +4,12 @@ from http import HTTPStatus import pytest from homeassistant.components.pvpc_hourly_pricing import ATTR_TARIFF, DOMAIN -from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, - CURRENCY_EURO, - ENERGY_KILO_WATT_HOUR, -) +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, CURRENCY_EURO, UnitOfEnergy from tests.common import load_fixture from tests.test_util.aiohttp import AiohttpClientMocker -FIXTURE_JSON_DATA_2021_06_01 = "PVPC_DATA_2021_06_01.json" +FIXTURE_JSON_PUBLIC_DATA_2023_01_06 = "PVPC_DATA_2023_01_06.json" def check_valid_state(state, tariff: str, value=None, key_attr=None): @@ -21,7 +17,7 @@ def check_valid_state(state, tariff: str, value=None, key_attr=None): assert state assert ( state.attributes[ATTR_UNIT_OF_MEASUREMENT] - == f"{CURRENCY_EURO}/{ENERGY_KILO_WATT_HOUR}" + == f"{CURRENCY_EURO}/{UnitOfEnergy.KILO_WATT_HOUR}" ) try: _ = float(state.state) @@ -42,17 +38,18 @@ def check_valid_state(state, tariff: str, value=None, key_attr=None): @pytest.fixture def pvpc_aioclient_mock(aioclient_mock: AiohttpClientMocker): """Create a mock config entry.""" - mask_url = "https://apidatos.ree.es/es/datos/mercados/precios-mercados-tiempo-real" - mask_url += "?time_trunc=hour&geo_ids={0}&start_date={1}T00:00&end_date={1}T23:59" + mask_url_public = ( + "https://api.esios.ree.es/archives/70/download_json?locale=es&date={0}" + ) # new format for prices >= 2021-06-01 - sample_data = load_fixture(f"{DOMAIN}/{FIXTURE_JSON_DATA_2021_06_01}") - - # tariff variant with different geo_ids=8744 - aioclient_mock.get(mask_url.format(8741, "2021-06-01"), text=sample_data) - aioclient_mock.get(mask_url.format(8744, "2021-06-01"), text=sample_data) - # simulate missing day + example_day = "2023-01-06" aioclient_mock.get( - mask_url.format(8741, "2021-06-02"), + mask_url_public.format(example_day), + text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_PUBLIC_DATA_2023_01_06}"), + ) + # simulate missing days + aioclient_mock.get( + mask_url_public.format("2023-01-07"), status=HTTPStatus.BAD_GATEWAY, text=( '{"errors":[{"code":502,"status":"502","title":"Bad response from ESIOS",' diff --git a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2021_06_01.json b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2021_06_01.json deleted file mode 100644 index eb2fb6f19f5..00000000000 --- a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2021_06_01.json +++ /dev/null @@ -1,154 +0,0 @@ -{ - "data": { - "type": "Precios mercado peninsular en tiempo real", - "id": "mer13", - "attributes": { - "title": "Precios mercado peninsular en tiempo real", - "last-update": "2021-05-31T20:19:18.000+02:00", - "description": null - }, - "meta": { - "cache-control": { - "cache": "MISS" - } - } - }, - "included": [ - { - "type": "PVPC (\u20ac/MWh)", - "id": "1001", - "groupId": null, - "attributes": { - "title": "PVPC (\u20ac/MWh)", - "description": null, - "color": "#ffcf09", - "type": null, - "magnitude": "price", - "composite": false, - "last-update": "2021-05-31T20:19:18.000+02:00", - "values": [ - { - "value": 116.33, - "percentage": 1, - "datetime": "2021-06-01T00:00:00.000+02:00" - }, - { - "value": 115.95, - "percentage": 1, - "datetime": "2021-06-01T01:00:00.000+02:00" - }, - { - "value": 114.89, - "percentage": 1, - "datetime": "2021-06-01T02:00:00.000+02:00" - }, - { - "value": 114.96, - "percentage": 1, - "datetime": "2021-06-01T03:00:00.000+02:00" - }, - { - "value": 114.84, - "percentage": 1, - "datetime": "2021-06-01T04:00:00.000+02:00" - }, - { - "value": 116.03, - "percentage": 1, - "datetime": "2021-06-01T05:00:00.000+02:00" - }, - { - "value": 116.29, - "percentage": 1, - "datetime": "2021-06-01T06:00:00.000+02:00" - }, - { - "value": 115.7, - "percentage": 1, - "datetime": "2021-06-01T07:00:00.000+02:00" - }, - { - "value": 152.89, - "percentage": 1, - "datetime": "2021-06-01T08:00:00.000+02:00" - }, - { - "value": 150.83, - "percentage": 1, - "datetime": "2021-06-01T09:00:00.000+02:00" - }, - { - "value": 149.28, - "percentage": 1, - "datetime": "2021-06-01T10:00:00.000+02:00" - }, - { - "value": 240.5, - "percentage": 1, - "datetime": "2021-06-01T11:00:00.000+02:00" - }, - { - "value": 238.09, - "percentage": 1, - "datetime": "2021-06-01T12:00:00.000+02:00" - }, - { - "value": 235.3, - "percentage": 1, - "datetime": "2021-06-01T13:00:00.000+02:00" - }, - { - "value": 231.28, - "percentage": 1, - "datetime": "2021-06-01T14:00:00.000+02:00" - }, - { - "value": 132.88, - "percentage": 1, - "datetime": "2021-06-01T15:00:00.000+02:00" - }, - { - "value": 131.93, - "percentage": 1, - "datetime": "2021-06-01T16:00:00.000+02:00" - }, - { - "value": 135.99, - "percentage": 1, - "datetime": "2021-06-01T17:00:00.000+02:00" - }, - { - "value": 138.13, - "percentage": 1, - "datetime": "2021-06-01T18:00:00.000+02:00" - }, - { - "value": 240.4, - "percentage": 1, - "datetime": "2021-06-01T19:00:00.000+02:00" - }, - { - "value": 246.2, - "percentage": 1, - "datetime": "2021-06-01T20:00:00.000+02:00" - }, - { - "value": 248.08, - "percentage": 1, - "datetime": "2021-06-01T21:00:00.000+02:00" - }, - { - "value": 249.41, - "percentage": 1, - "datetime": "2021-06-01T22:00:00.000+02:00" - }, - { - "value": 156.5, - "percentage": 1, - "datetime": "2021-06-01T23:00:00.000+02:00" - } - ] - } - } - ] -} diff --git a/tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2023_01_06.json b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2023_01_06.json new file mode 100644 index 00000000000..501a40d8a54 --- /dev/null +++ b/tests/components/pvpc_hourly_pricing/fixtures/PVPC_DATA_2023_01_06.json @@ -0,0 +1,652 @@ +{ + "PVPC": [ + { + "Dia": "06/01/2023", + "Hora": "00-01", + "PCB": "159,69", + "CYM": "159,69", + "COF2TD": "0,000132685792000000", + "PMHPCB": "131,22", + "PMHCYM": "131,22", + "SAHPCB": "14,49", + "SAHCYM": "14,49", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,36", + "CCVCYM": "3,36", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "7,21", + "EDCGASCYM": "7,21" + }, + { + "Dia": "06/01/2023", + "Hora": "01-02", + "PCB": "155,71", + "CYM": "155,71", + "COF2TD": "0,000111367791000000", + "PMHPCB": "124,44", + "PMHCYM": "124,44", + "SAHPCB": "18,44", + "SAHCYM": "18,44", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,30", + "CCVCYM": "3,30", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "6,11", + "EDCGASCYM": "6,11" + }, + { + "Dia": "06/01/2023", + "Hora": "02-03", + "PCB": "154,41", + "CYM": "154,41", + "COF2TD": "0,000095082106000000", + "PMHPCB": "121,01", + "PMHCYM": "121,01", + "SAHPCB": "20,12", + "SAHCYM": "20,12", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,29", + "CCVCYM": "3,29", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "6,57", + "EDCGASCYM": "6,57" + }, + { + "Dia": "06/01/2023", + "Hora": "03-04", + "PCB": "139,37", + "CYM": "139,37", + "COF2TD": "0,000085049382000000", + "PMHPCB": "102,36", + "PMHCYM": "102,36", + "SAHPCB": "24,98", + "SAHCYM": "24,98", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,07", + "CCVCYM": "3,07", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "5,52", + "EDCGASCYM": "5,52" + }, + { + "Dia": "06/01/2023", + "Hora": "04-05", + "PCB": "134,02", + "CYM": "134,02", + "COF2TD": "0,000079934351000000", + "PMHPCB": "95,57", + "PMHCYM": "95,57", + "SAHPCB": "26,37", + "SAHCYM": "26,37", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "2,99", + "CCVCYM": "2,99", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "5,66", + "EDCGASCYM": "5,66" + }, + { + "Dia": "06/01/2023", + "Hora": "05-06", + "PCB": "140,02", + "CYM": "140,02", + "COF2TD": "0,000078802072000000", + "PMHPCB": "102,11", + "PMHCYM": "102,11", + "SAHPCB": "25,68", + "SAHCYM": "25,68", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,08", + "CCVCYM": "3,08", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "5,73", + "EDCGASCYM": "5,73" + }, + { + "Dia": "06/01/2023", + "Hora": "06-07", + "PCB": "154,05", + "CYM": "154,05", + "COF2TD": "0,000080430301000000", + "PMHPCB": "121,91", + "PMHCYM": "121,91", + "SAHPCB": "19,87", + "SAHCYM": "19,87", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,28", + "CCVCYM": "3,28", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "5,56", + "EDCGASCYM": "5,56" + }, + { + "Dia": "06/01/2023", + "Hora": "07-08", + "PCB": "163,15", + "CYM": "163,15", + "COF2TD": "0,000084537404000000", + "PMHPCB": "133,05", + "PMHCYM": "133,05", + "SAHPCB": "17,96", + "SAHCYM": "17,96", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,42", + "CCVCYM": "3,42", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "5,30", + "EDCGASCYM": "5,30" + }, + { + "Dia": "06/01/2023", + "Hora": "08-09", + "PCB": "180,50", + "CYM": "180,50", + "COF2TD": "0,000092612528000000", + "PMHPCB": "151,14", + "PMHCYM": "151,14", + "SAHPCB": "17,14", + "SAHCYM": "17,14", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,68", + "CCVCYM": "3,68", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "5,12", + "EDCGASCYM": "5,12" + }, + { + "Dia": "06/01/2023", + "Hora": "09-10", + "PCB": "174,90", + "CYM": "174,90", + "COF2TD": "0,000114375020000000", + "PMHPCB": "152,13", + "PMHCYM": "152,13", + "SAHPCB": "12,34", + "SAHCYM": "12,34", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,58", + "CCVCYM": "3,58", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "3,44", + "EDCGASCYM": "3,44" + }, + { + "Dia": "06/01/2023", + "Hora": "10-11", + "PCB": "166,47", + "CYM": "166,47", + "COF2TD": "0,000137288470000000", + "PMHPCB": "144,86", + "PMHCYM": "144,86", + "SAHPCB": "12,11", + "SAHCYM": "12,11", + "FOMPCB": "0,04", + "FOMCYM": "0,04", + "FOSPCB": "0,19", + "FOSCYM": "0,19", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,44", + "CCVCYM": "3,44", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "2,65", + "EDCGASCYM": "2,65" + }, + { + "Dia": "06/01/2023", + "Hora": "11-12", + "PCB": "152,30", + "CYM": "152,30", + "COF2TD": "0,000147389186000000", + "PMHPCB": "132,46", + "PMHCYM": "132,46", + "SAHPCB": "10,92", + "SAHCYM": "10,92", + "FOMPCB": "0,04", + "FOMCYM": "0,04", + "FOSPCB": "0,19", + "FOSCYM": "0,19", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,22", + "CCVCYM": "3,22", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "2,29", + "EDCGASCYM": "2,29" + }, + { + "Dia": "06/01/2023", + "Hora": "12-13", + "PCB": "144,54", + "CYM": "144,54", + "COF2TD": "0,000147566474000000", + "PMHPCB": "124,33", + "PMHCYM": "124,33", + "SAHPCB": "11,39", + "SAHCYM": "11,39", + "FOMPCB": "0,04", + "FOMCYM": "0,04", + "FOSPCB": "0,19", + "FOSCYM": "0,19", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,11", + "CCVCYM": "3,11", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "2,30", + "EDCGASCYM": "2,30" + }, + { + "Dia": "06/01/2023", + "Hora": "13-14", + "PCB": "132,08", + "CYM": "132,08", + "COF2TD": "0,000152593999000000", + "PMHPCB": "108,55", + "PMHCYM": "108,55", + "SAHPCB": "15,11", + "SAHCYM": "15,11", + "FOMPCB": "0,04", + "FOMCYM": "0,04", + "FOSPCB": "0,19", + "FOSCYM": "0,19", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "2,92", + "CCVCYM": "2,92", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "2,10", + "EDCGASCYM": "2,10" + }, + { + "Dia": "06/01/2023", + "Hora": "14-15", + "PCB": "119,60", + "CYM": "119,60", + "COF2TD": "0,000151275074000000", + "PMHPCB": "96,79", + "PMHCYM": "96,79", + "SAHPCB": "15,31", + "SAHCYM": "15,31", + "FOMPCB": "0,04", + "FOMCYM": "0,04", + "FOSPCB": "0,19", + "FOSCYM": "0,19", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "2,73", + "CCVCYM": "2,73", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "1,35", + "EDCGASCYM": "1,35" + }, + { + "Dia": "06/01/2023", + "Hora": "15-16", + "PCB": "108,74", + "CYM": "108,74", + "COF2TD": "0,000136801693000000", + "PMHPCB": "85,31", + "PMHCYM": "85,31", + "SAHPCB": "16,78", + "SAHCYM": "16,78", + "FOMPCB": "0,04", + "FOMCYM": "0,04", + "FOSPCB": "0,19", + "FOSCYM": "0,19", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "2,57", + "CCVCYM": "2,57", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "0,66", + "EDCGASCYM": "0,66" + }, + { + "Dia": "06/01/2023", + "Hora": "16-17", + "PCB": "123,79", + "CYM": "123,79", + "COF2TD": "0,000129250075000000", + "PMHPCB": "98,80", + "PMHCYM": "98,80", + "SAHPCB": "17,70", + "SAHCYM": "17,70", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,19", + "FOSCYM": "0,19", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "2,80", + "CCVCYM": "2,80", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "1,07", + "EDCGASCYM": "1,07" + }, + { + "Dia": "06/01/2023", + "Hora": "17-18", + "PCB": "166,41", + "CYM": "166,41", + "COF2TD": "0,000131159722000000", + "PMHPCB": "148,24", + "PMHCYM": "148,24", + "SAHPCB": "9,16", + "SAHCYM": "9,16", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,45", + "CCVCYM": "3,45", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "2,15", + "EDCGASCYM": "2,15" + }, + { + "Dia": "06/01/2023", + "Hora": "18-19", + "PCB": "173,49", + "CYM": "173,49", + "COF2TD": "0,000151915481000000", + "PMHPCB": "157,13", + "PMHCYM": "157,13", + "SAHPCB": "7,31", + "SAHCYM": "7,31", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,55", + "CCVCYM": "3,55", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "2,08", + "EDCGASCYM": "2,08" + }, + { + "Dia": "06/01/2023", + "Hora": "19-20", + "PCB": "186,17", + "CYM": "186,17", + "COF2TD": "0,000166375982000000", + "PMHPCB": "168,44", + "PMHCYM": "168,44", + "SAHPCB": "6,56", + "SAHCYM": "6,56", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,74", + "CCVCYM": "3,74", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "4,02", + "EDCGASCYM": "4,02" + }, + { + "Dia": "06/01/2023", + "Hora": "20-21", + "PCB": "186,11", + "CYM": "186,11", + "COF2TD": "0,000177449572000000", + "PMHPCB": "168,44", + "PMHCYM": "168,44", + "SAHPCB": "6,52", + "SAHCYM": "6,52", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,74", + "CCVCYM": "3,74", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "3,99", + "EDCGASCYM": "3,99" + }, + { + "Dia": "06/01/2023", + "Hora": "21-22", + "PCB": "178,45", + "CYM": "178,45", + "COF2TD": "0,000181996443000000", + "PMHPCB": "161,70", + "PMHCYM": "161,70", + "SAHPCB": "6,65", + "SAHCYM": "6,65", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,63", + "CCVCYM": "3,63", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "3,05", + "EDCGASCYM": "3,05" + }, + { + "Dia": "06/01/2023", + "Hora": "22-23", + "PCB": "139,37", + "CYM": "139,37", + "COF2TD": "0,000170132849000000", + "PMHPCB": "122,91", + "PMHCYM": "122,91", + "SAHPCB": "7,67", + "SAHCYM": "7,67", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "3,05", + "CCVCYM": "3,05", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "2,33", + "EDCGASCYM": "2,33" + }, + { + "Dia": "06/01/2023", + "Hora": "23-24", + "PCB": "129,35", + "CYM": "129,35", + "COF2TD": "0,000146932566000000", + "PMHPCB": "113,05", + "PMHCYM": "113,05", + "SAHPCB": "8,35", + "SAHCYM": "8,35", + "FOMPCB": "0,05", + "FOMCYM": "0,05", + "FOSPCB": "0,20", + "FOSCYM": "0,20", + "INTPCB": "0,00", + "INTCYM": "0,00", + "PCAPPCB": "0,00", + "PCAPCYM": "0,00", + "TEUPCB": "3,18", + "TEUCYM": "3,18", + "CCVPCB": "2,90", + "CCVCYM": "2,90", + "EDSRPCB": "0,00", + "EDSRCYM": "0,00", + "EDCGASPCB": "1,63", + "EDCGASCYM": "1,63" + } + ] +} diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index e67aca154c4..125214066a5 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -1,5 +1,5 @@ """Tests for the pvpc_hourly_pricing config_flow.""" -from datetime import datetime +from datetime import datetime, timedelta from freezegun import freeze_time @@ -16,9 +16,11 @@ from homeassistant.helpers import entity_registry as er from .conftest import check_valid_state -from tests.common import date_util +from tests.common import async_fire_time_changed, date_util from tests.test_util.aiohttp import AiohttpClientMocker +_MOCK_TIME_VALID_RESPONSES = datetime(2023, 1, 6, 12, 0, tzinfo=date_util.UTC) + async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): """ @@ -37,9 +39,8 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): ATTR_POWER: 4.6, ATTR_POWER_P3: 5.75, } - mock_data = {"return_time": datetime(2021, 6, 1, 12, 0, tzinfo=date_util.UTC)} - with freeze_time(mock_data["return_time"]): + with freeze_time(_MOCK_TIME_VALID_RESPONSES) as mock_time: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -86,9 +87,9 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): state = hass.states.get("sensor.test") check_valid_state(state, tariff=TARIFFS[1]) assert pvpc_aioclient_mock.call_count == 2 - assert state.attributes["period"] == "P1" + assert state.attributes["period"] == "P3" assert state.attributes["next_period"] == "P2" - assert state.attributes["available_power"] == 4600 + assert state.attributes["available_power"] == 5750 # check options flow current_entries = hass.config_entries.async_entries(DOMAIN) @@ -101,12 +102,22 @@ async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={ATTR_TARIFF: TARIFFS[0], ATTR_POWER: 3.0, ATTR_POWER_P3: 4.6}, + user_input={ATTR_POWER: 3.0, ATTR_POWER_P3: 4.6}, ) await hass.async_block_till_done() state = hass.states.get("sensor.test") - check_valid_state(state, tariff=TARIFFS[0]) + check_valid_state(state, tariff=TARIFFS[1]) assert pvpc_aioclient_mock.call_count == 3 - assert state.attributes["period"] == "P2" - assert state.attributes["next_period"] == "P1" - assert state.attributes["available_power"] == 3000 + assert state.attributes["period"] == "P3" + assert state.attributes["next_period"] == "P2" + assert state.attributes["available_power"] == 4600 + + # check update failed + ts_future = _MOCK_TIME_VALID_RESPONSES + timedelta(days=1) + mock_time.move_to(ts_future) + async_fire_time_changed(hass, ts_future) + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + check_valid_state(state, tariff=TARIFFS[0], value="unavailable") + assert "period" not in state.attributes + assert pvpc_aioclient_mock.call_count == 4 diff --git a/tests/components/pvpc_hourly_pricing/test_sensor.py b/tests/components/pvpc_hourly_pricing/test_sensor.py deleted file mode 100644 index 5153e9cedbd..00000000000 --- a/tests/components/pvpc_hourly_pricing/test_sensor.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Tests for the pvpc_hourly_pricing sensor component.""" -from datetime import datetime, timedelta -import logging - -from freezegun import freeze_time - -from homeassistant.components.pvpc_hourly_pricing import ( - ATTR_POWER, - ATTR_POWER_P3, - ATTR_TARIFF, - DOMAIN, - TARIFFS, -) -from homeassistant.const import CONF_NAME - -from .conftest import check_valid_state - -from tests.common import ( - MockConfigEntry, - async_fire_time_changed, - date_util, - mock_registry, -) -from tests.test_util.aiohttp import AiohttpClientMocker - - -async def test_multi_sensor_migration( - hass, caplog, pvpc_aioclient_mock: AiohttpClientMocker -): - """Test tariff migration when there are >1 old sensors.""" - entity_reg = mock_registry(hass) - hass.config.set_time_zone("Europe/Madrid") - uid_1 = "discrimination" - uid_2 = "normal" - old_conf_1 = {CONF_NAME: "test_pvpc_1", ATTR_TARIFF: uid_1} - old_conf_2 = {CONF_NAME: "test_pvpc_2", ATTR_TARIFF: uid_2} - - config_entry_1 = MockConfigEntry(domain=DOMAIN, data=old_conf_1, unique_id=uid_1) - config_entry_1.add_to_hass(hass) - entity1 = entity_reg.async_get_or_create( - domain="sensor", - platform=DOMAIN, - unique_id=uid_1, - config_entry=config_entry_1, - suggested_object_id="test_pvpc_1", - ) - - config_entry_2 = MockConfigEntry(domain=DOMAIN, data=old_conf_2, unique_id=uid_2) - config_entry_2.add_to_hass(hass) - entity2 = entity_reg.async_get_or_create( - domain="sensor", - platform=DOMAIN, - unique_id=uid_2, - config_entry=config_entry_2, - suggested_object_id="test_pvpc_2", - ) - - assert len(hass.config_entries.async_entries(DOMAIN)) == 2 - assert len(entity_reg.entities) == 2 - - mock_data = {"return_time": datetime(2021, 6, 1, 21, tzinfo=date_util.UTC)} - - caplog.clear() - with caplog.at_level(logging.WARNING): - with freeze_time(mock_data["return_time"]): - assert await hass.config_entries.async_setup(config_entry_1.entry_id) - assert any("Migrating PVPC" in message for message in caplog.messages) - assert any( - "Old PVPC Sensor sensor.test_pvpc_2 is removed" in message - for message in caplog.messages - ) - - # check migration with removal of extra sensors - assert len(entity_reg.entities) == 1 - assert entity1.entity_id in entity_reg.entities - assert entity2.entity_id not in entity_reg.entities - - current_entries = hass.config_entries.async_entries(DOMAIN) - assert len(current_entries) == 1 - migrated_entry = current_entries[0] - assert migrated_entry.version == 1 - assert migrated_entry.data[ATTR_POWER] == migrated_entry.data[ATTR_POWER_P3] - assert migrated_entry.data[ATTR_TARIFF] == TARIFFS[0] - - await hass.async_block_till_done() - assert pvpc_aioclient_mock.call_count == 2 - - # check state and availability - state = hass.states.get("sensor.test_pvpc_1") - check_valid_state(state, tariff=TARIFFS[0], value=0.1565) - - with freeze_time(mock_data["return_time"] + timedelta(minutes=60)): - async_fire_time_changed(hass, mock_data["return_time"]) - await list(hass.data[DOMAIN].values())[0].async_refresh() - await hass.async_block_till_done() - state = hass.states.get("sensor.test_pvpc_1") - check_valid_state(state, tariff=TARIFFS[0], value="unavailable") - assert pvpc_aioclient_mock.call_count == 3 diff --git a/tests/components/qld_bushfire/test_geo_location.py b/tests/components/qld_bushfire/test_geo_location.py index 58974c55b45..ebe0664efad 100644 --- a/tests/components/qld_bushfire/test_geo_location.py +++ b/tests/components/qld_bushfire/test_geo_location.py @@ -23,7 +23,7 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_RADIUS, EVENT_HOMEASSISTANT_START, - LENGTH_KILOMETERS, + UnitOfLength, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -124,7 +124,7 @@ async def test_setup(hass): 2018, 9, 22, 8, 10, tzinfo=datetime.timezone.utc ), ATTR_STATUS: "Status 1", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "qld_bushfire", ATTR_ICON: "mdi:fire", } @@ -138,7 +138,7 @@ async def test_setup(hass): ATTR_LATITUDE: 38.1, ATTR_LONGITUDE: -3.1, ATTR_FRIENDLY_NAME: "Title 2", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "qld_bushfire", ATTR_ICON: "mdi:fire", } @@ -152,7 +152,7 @@ async def test_setup(hass): ATTR_LATITUDE: 38.2, ATTR_LONGITUDE: -3.2, ATTR_FRIENDLY_NAME: "Title 3", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "qld_bushfire", ATTR_ICON: "mdi:fire", } diff --git a/tests/components/qnap_qsw/test_sensor.py b/tests/components/qnap_qsw/test_sensor.py index b37a45e441e..902f65d9258 100644 --- a/tests/components/qnap_qsw/test_sensor.py +++ b/tests/components/qnap_qsw/test_sensor.py @@ -47,3 +47,329 @@ async def test_qnap_qsw_create_sensors( state = hass.states.get("sensor.qsw_m408_4c_uptime") assert state.state == "91" + + # LACP Ports + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_1_link_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_1_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_1_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_1_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_1_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_1_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_2_link_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_2_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_2_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_2_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_2_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_2_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_3_link_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_3_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_3_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_3_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_3_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_3_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_4_link_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_4_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_4_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_4_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_4_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_4_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_5_link_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_5_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_5_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_5_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_5_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_5_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_6_link_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_6_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_6_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_6_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_6_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_lacp_port_6_tx_speed") + assert state.state == "0" + + # Ports + state = hass.states.get("sensor.qsw_m408_4c_port_1_link_speed") + assert state.state == "10000" + + state = hass.states.get("sensor.qsw_m408_4c_port_1_rx") + assert state.state == "20000" + + state = hass.states.get("sensor.qsw_m408_4c_port_1_rx_errors") + assert state.state == "20" + + state = hass.states.get("sensor.qsw_m408_4c_port_1_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_1_tx") + assert state.state == "10000" + + state = hass.states.get("sensor.qsw_m408_4c_port_1_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_2_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_2_rx") + assert state.state == "2000" + + state = hass.states.get("sensor.qsw_m408_4c_port_2_rx_errors") + assert state.state == "2" + + state = hass.states.get("sensor.qsw_m408_4c_port_2_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_2_tx") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_2_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_3_link_speed") + assert state.state == "100" + + state = hass.states.get("sensor.qsw_m408_4c_port_3_rx") + assert state.state == "200" + + state = hass.states.get("sensor.qsw_m408_4c_port_3_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_3_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_3_tx") + assert state.state == "100" + + state = hass.states.get("sensor.qsw_m408_4c_port_3_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_4_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_4_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_4_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_4_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_4_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_4_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_5_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_5_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_5_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_5_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_5_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_5_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_6_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_6_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_6_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_6_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_6_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_6_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_7_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_7_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_7_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_7_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_7_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_7_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_8_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_8_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_8_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_8_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_8_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_8_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_9_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_9_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_9_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_9_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_9_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_9_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_10_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_10_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_10_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_10_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_10_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_10_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_11_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_11_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_11_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_11_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_11_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_11_tx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_12_link_speed") + assert state.state == "1000" + + state = hass.states.get("sensor.qsw_m408_4c_port_12_rx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_12_rx_errors") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_12_rx_speed") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_12_tx") + assert state.state == "0" + + state = hass.states.get("sensor.qsw_m408_4c_port_12_tx_speed") + assert state.state == "0" diff --git a/tests/components/qwikswitch/test_init.py b/tests/components/qwikswitch/test_init.py index 74e600b50e4..1f9efec92e7 100644 --- a/tests/components/qwikswitch/test_init.py +++ b/tests/components/qwikswitch/test_init.py @@ -7,6 +7,7 @@ import pytest from yarl import URL from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH +from homeassistant.const import STATE_UNKNOWN from homeassistant.setup import async_setup_component from tests.test_util.aiohttp import AiohttpClientMockResponse, MockLongPollSideEffect @@ -105,7 +106,7 @@ async def test_sensor_device(hass, aioclient_mock, qs_devices): await hass.async_block_till_done() state_obj = hass.states.get("sensor.ss1") - assert state_obj.state == "None" + assert state_obj.state == STATE_UNKNOWN # receive command that sets the sensor value listen_mock.queue_response( diff --git a/tests/components/radio_browser/test_config_flow.py b/tests/components/radio_browser/test_config_flow.py index 56ed98f145b..d958efc4e50 100644 --- a/tests/components/radio_browser/test_config_flow.py +++ b/tests/components/radio_browser/test_config_flow.py @@ -16,7 +16,6 @@ async def test_full_user_flow(hass: HomeAssistant, mock_setup_entry: AsyncMock) ) assert result.get("type") == FlowResultType.FORM assert result.get("errors") is None - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/rainbird/conftest.py b/tests/components/rainbird/conftest.py index 660307f1c60..22f238ce553 100644 --- a/tests/components/rainbird/conftest.py +++ b/tests/components/rainbird/conftest.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Awaitable, Callable, Generator +from http import HTTPStatus from typing import Any from unittest.mock import patch @@ -10,10 +11,15 @@ from pyrainbird import encryption import pytest from homeassistant.components.rainbird import DOMAIN +from homeassistant.components.rainbird.const import ( + ATTR_DURATION, + DEFAULT_TRIGGER_TIME_MINUTES, +) from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse ComponentSetup = Callable[[], Awaitable[bool]] @@ -21,6 +27,7 @@ ComponentSetup = Callable[[], Awaitable[bool]] HOST = "example.com" URL = "http://example.com/stick" PASSWORD = "password" +SERIAL_NUMBER = 0x12635436566 # # Response payloads below come from pyrainbird test cases. @@ -45,14 +52,28 @@ RAIN_DELAY_OFF = "B60000" # ACK command 0x10, Echo 0x06 ACK_ECHO = "0106" + CONFIG = { DOMAIN: { "host": HOST, "password": PASSWORD, - "trigger_time": 360, + "trigger_time": { + "minutes": 6, + }, } } +CONFIG_ENTRY_DATA = { + "host": HOST, + "password": PASSWORD, + "serial_number": SERIAL_NUMBER, +} + + +UNAVAILABLE_RESPONSE = AiohttpClientMockResponse( + "POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE +) + @pytest.fixture def platforms() -> list[Platform]: @@ -63,7 +84,37 @@ def platforms() -> list[Platform]: @pytest.fixture def yaml_config() -> dict[str, Any]: """Fixture for configuration.yaml.""" - return CONFIG + return {} + + +@pytest.fixture +async def config_entry_data() -> dict[str, Any]: + """Fixture for MockConfigEntry data.""" + return CONFIG_ENTRY_DATA + + +@pytest.fixture +async def config_entry( + config_entry_data: dict[str, Any] | None +) -> MockConfigEntry | None: + """Fixture for MockConfigEntry.""" + if config_entry_data is None: + return None + return MockConfigEntry( + unique_id=SERIAL_NUMBER, + domain=DOMAIN, + data=config_entry_data, + options={ATTR_DURATION: DEFAULT_TRIGGER_TIME_MINUTES}, + ) + + +@pytest.fixture(autouse=True) +async def add_config_entry( + hass: HomeAssistant, config_entry: MockConfigEntry | None +) -> None: + """Fixture to add the config entry.""" + if config_entry: + config_entry.add_to_hass(hass) @pytest.fixture @@ -97,10 +148,48 @@ def mock_response(data: str) -> AiohttpClientMockResponse: return AiohttpClientMockResponse("POST", URL, response=rainbird_response(data)) +@pytest.fixture(name="stations_response") +def mock_station_response() -> str: + """Mock response to return available stations.""" + return AVAILABLE_STATIONS_RESPONSE + + +@pytest.fixture(name="zone_state_response") +def mock_zone_state_response() -> str: + """Mock response to return zone states.""" + return ZONE_STATE_OFF_RESPONSE + + +@pytest.fixture(name="rain_response") +def mock_rain_response() -> str: + """Mock response to return rain sensor state.""" + return RAIN_SENSOR_OFF + + +@pytest.fixture(name="rain_delay_response") +def mock_rain_delay_response() -> str: + """Mock response to return rain delay state.""" + return RAIN_DELAY_OFF + + +@pytest.fixture(name="api_responses") +def mock_api_responses( + stations_response: str, + zone_state_response: str, + rain_response: str, + rain_delay_response: str, +) -> list[str]: + """Fixture to set up a list of fake API responsees for tests to extend. + + These are returned in the order they are requested by the update coordinator. + """ + return [stations_response, zone_state_response, rain_response, rain_delay_response] + + @pytest.fixture(name="responses") -def mock_responses() -> list[AiohttpClientMockResponse]: +def mock_responses(api_responses: list[str]) -> list[AiohttpClientMockResponse]: """Fixture to set up a list of fake API responsees for tests to extend.""" - return [mock_response(SERIAL_RESPONSE)] + return [mock_response(api_response) for api_response in api_responses] @pytest.fixture(autouse=True) diff --git a/tests/components/rainbird/test_binary_sensor.py b/tests/components/rainbird/test_binary_sensor.py index 7ed6f2d1a29..2cb49de49e1 100644 --- a/tests/components/rainbird/test_binary_sensor.py +++ b/tests/components/rainbird/test_binary_sensor.py @@ -6,14 +6,7 @@ import pytest from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .conftest import ( - RAIN_DELAY, - RAIN_DELAY_OFF, - RAIN_SENSOR_OFF, - RAIN_SENSOR_ON, - ComponentSetup, - mock_response, -) +from .conftest import RAIN_SENSOR_OFF, RAIN_SENSOR_ON, ComponentSetup from tests.test_util.aiohttp import AiohttpClientMockResponse @@ -25,54 +18,23 @@ def platforms() -> list[Platform]: @pytest.mark.parametrize( - "sensor_payload,expected_state", + "rain_response,expected_state", [(RAIN_SENSOR_OFF, "off"), (RAIN_SENSOR_ON, "on")], ) async def test_rainsensor( hass: HomeAssistant, setup_integration: ComponentSetup, responses: list[AiohttpClientMockResponse], - sensor_payload: str, expected_state: bool, ) -> None: """Test rainsensor binary sensor.""" - responses.extend( - [ - mock_response(sensor_payload), - mock_response(RAIN_DELAY), - ] - ) - assert await setup_integration() rainsensor = hass.states.get("binary_sensor.rainsensor") assert rainsensor is not None assert rainsensor.state == expected_state - - -@pytest.mark.parametrize( - "sensor_payload,expected_state", - [(RAIN_DELAY_OFF, "off"), (RAIN_DELAY, "on")], -) -async def test_raindelay( - hass: HomeAssistant, - setup_integration: ComponentSetup, - responses: list[AiohttpClientMockResponse], - sensor_payload: str, - expected_state: bool, -) -> None: - """Test raindelay binary sensor.""" - - responses.extend( - [ - mock_response(RAIN_SENSOR_OFF), - mock_response(sensor_payload), - ] - ) - - assert await setup_integration() - - raindelay = hass.states.get("binary_sensor.raindelay") - assert raindelay is not None - assert raindelay.state == expected_state + assert rainsensor.attributes == { + "friendly_name": "Rainsensor", + "icon": "mdi:water", + } diff --git a/tests/components/rainbird/test_config_flow.py b/tests/components/rainbird/test_config_flow.py new file mode 100644 index 00000000000..31650a0828a --- /dev/null +++ b/tests/components/rainbird/test_config_flow.py @@ -0,0 +1,149 @@ +"""Tests for the Rain Bird config flow.""" + +import asyncio +from collections.abc import Generator +from http import HTTPStatus +from unittest.mock import Mock, patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.rainbird import DOMAIN +from homeassistant.components.rainbird.const import ATTR_DURATION +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_HOST, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult, FlowResultType + +from .conftest import ( + CONFIG_ENTRY_DATA, + HOST, + PASSWORD, + SERIAL_RESPONSE, + URL, + mock_response, +) + +from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse + + +@pytest.fixture(name="responses") +def mock_responses() -> list[AiohttpClientMockResponse]: + """Set up fake serial number response when testing the connection.""" + return [mock_response(SERIAL_RESPONSE)] + + +@pytest.fixture(autouse=True) +async def config_entry_data() -> None: + """Fixture to disable config entry setup for exercising config flow.""" + return None + + +@pytest.fixture(autouse=True) +async def mock_setup() -> Generator[Mock, None, None]: + """Fixture for patching out integration setup.""" + + with patch( + "homeassistant.components.rainbird.async_setup_entry", + return_value=True, + ) as mock_setup: + yield mock_setup + + +async def complete_flow(hass: HomeAssistant) -> FlowResult: + """Start the config flow and enter the host and password.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "user" + assert not result.get("errors") + assert "flow_id" in result + + return await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: HOST, CONF_PASSWORD: PASSWORD}, + ) + + +async def test_controller_flow(hass: HomeAssistant, mock_setup: Mock) -> None: + """Test the controller is setup correctly.""" + + result = await complete_flow(hass) + assert result.get("type") == "create_entry" + assert result.get("title") == HOST + assert "result" in result + assert result["result"].data == CONFIG_ENTRY_DATA + assert result["result"].options == {ATTR_DURATION: 6} + + assert len(mock_setup.mock_calls) == 1 + + +async def test_controller_cannot_connect( + hass: HomeAssistant, + mock_setup: Mock, + responses: list[AiohttpClientMockResponse], + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test an error talking to the controller.""" + + # Controller response with a failure + responses.clear() + responses.append( + AiohttpClientMockResponse("POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE) + ) + + result = await complete_flow(hass) + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "user" + assert result.get("errors") == {"base": "cannot_connect"} + + assert not mock_setup.mock_calls + + +async def test_controller_timeout( + hass: HomeAssistant, + mock_setup: Mock, +) -> None: + """Test an error talking to the controller.""" + + with patch( + "homeassistant.components.rainbird.config_flow.async_timeout.timeout", + side_effect=asyncio.TimeoutError, + ): + result = await complete_flow(hass) + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "user" + assert result.get("errors") == {"base": "timeout_connect"} + + assert not mock_setup.mock_calls + + +async def test_options_flow(hass: HomeAssistant, mock_setup: Mock) -> None: + """Test config flow options.""" + + # Setup config flow + result = await complete_flow(hass) + assert result.get("type") == "create_entry" + assert result.get("title") == HOST + assert "result" in result + assert result["result"].data == CONFIG_ENTRY_DATA + assert result["result"].options == {ATTR_DURATION: 6} + + # Assert single config entry is loaded + config_entry = next(iter(hass.config_entries.async_entries(DOMAIN))) + assert config_entry.state == ConfigEntryState.LOADED + + # Initiate the options flow + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == "init" + + # Change the default duration + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={ATTR_DURATION: 5} + ) + assert result.get("type") == FlowResultType.CREATE_ENTRY + assert config_entry.options == { + ATTR_DURATION: 5, + } diff --git a/tests/components/rainbird/test_init.py b/tests/components/rainbird/test_init.py index acf6a92d4a5..e8e9f76d312 100644 --- a/tests/components/rainbird/test_init.py +++ b/tests/components/rainbird/test_init.py @@ -1,34 +1,164 @@ """Tests for rainbird initialization.""" -from http import HTTPStatus +from __future__ import annotations +import pytest + +from homeassistant.components.rainbird import DOMAIN +from homeassistant.components.rainbird.const import ATTR_CONFIG_ENTRY_ID, ATTR_DURATION +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr, issue_registry as ir -from .conftest import URL, ComponentSetup +from .conftest import ( + ACK_ECHO, + CONFIG, + CONFIG_ENTRY_DATA, + SERIAL_NUMBER, + SERIAL_RESPONSE, + UNAVAILABLE_RESPONSE, + ComponentSetup, + mock_response, +) from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse -async def test_setup_success( - hass: HomeAssistant, - setup_integration: ComponentSetup, -) -> None: - """Test successful setup and unload.""" - - assert await setup_integration() - - -async def test_setup_communication_failure( +@pytest.mark.parametrize( + "yaml_config,config_entry_data,initial_response", + [ + ({}, CONFIG_ENTRY_DATA, None), + ( + CONFIG, + None, + mock_response(SERIAL_RESPONSE), # Extra import request + ), + ( + CONFIG, + CONFIG_ENTRY_DATA, + None, + ), + ], + ids=["config_entry", "yaml", "already_exists"], +) +async def test_init_success( hass: HomeAssistant, setup_integration: ComponentSetup, responses: list[AiohttpClientMockResponse], - aioclient_mock: AiohttpClientMocker, + initial_response: AiohttpClientMockResponse | None, +) -> None: + """Test successful setup and unload.""" + if initial_response: + responses.insert(0, initial_response) + + assert await setup_integration() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ConfigEntryState.LOADED + + await hass.config_entries.async_unload(entries[0].entry_id) + await hass.async_block_till_done() + assert entries[0].state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize( + "yaml_config,config_entry_data,responses,config_entry_states", + [ + ({}, CONFIG_ENTRY_DATA, [UNAVAILABLE_RESPONSE], [ConfigEntryState.SETUP_RETRY]), + ( + CONFIG, + None, + [ + UNAVAILABLE_RESPONSE, # Failure when importing yaml + ], + [], + ), + ( + CONFIG, + None, + [ + mock_response(SERIAL_RESPONSE), # Import succeeds + UNAVAILABLE_RESPONSE, # Failure on integration setup + ], + [ConfigEntryState.SETUP_RETRY], + ), + ], + ids=["config_entry_failure", "yaml_import_failure", "yaml_init_failure"], +) +async def test_communication_failure( + hass: HomeAssistant, + setup_integration: ComponentSetup, + config_entry_states: list[ConfigEntryState], ) -> None: """Test unable to talk to server on startup, which permanently fails setup.""" - responses.clear() - responses.append( - AiohttpClientMockResponse("POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE) + assert await setup_integration() + + assert [ + entry.state for entry in hass.config_entries.async_entries(DOMAIN) + ] == config_entry_states + + +@pytest.mark.parametrize("platforms", [[Platform.NUMBER, Platform.SENSOR]]) +async def test_rain_delay_service( + hass: HomeAssistant, + setup_integration: ComponentSetup, + aioclient_mock: AiohttpClientMocker, + responses: list[str], + config_entry: ConfigEntry, +) -> None: + """Test calling the rain delay service.""" + + assert await setup_integration() + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, SERIAL_NUMBER)}) + assert device + assert device.name == "Rain Bird Controller" + + aioclient_mock.mock_calls.clear() + responses.append(mock_response(ACK_ECHO)) + + await hass.services.async_call( + DOMAIN, + "set_rain_delay", + {ATTR_CONFIG_ENTRY_ID: config_entry.entry_id, ATTR_DURATION: 3}, + blocking=True, ) - assert not await setup_integration() + assert len(aioclient_mock.mock_calls) == 1 + + issue_registry: ir.IssueRegistry = ir.async_get(hass) + issue = issue_registry.async_get_issue( + domain=DOMAIN, issue_id="deprecated_raindelay" + ) + assert issue + assert issue.translation_placeholders == { + "alternate_target": "number.rain_bird_controller_rain_delay" + } + + +async def test_rain_delay_invalid_config_entry( + hass: HomeAssistant, + setup_integration: ComponentSetup, + aioclient_mock: AiohttpClientMocker, + config_entry: ConfigEntry, +) -> None: + """Test calling the rain delay service.""" + + assert await setup_integration() + + aioclient_mock.mock_calls.clear() + + with pytest.raises(HomeAssistantError, match="Config entry id does not exist"): + await hass.services.async_call( + DOMAIN, + "set_rain_delay", + {ATTR_CONFIG_ENTRY_ID: "invalid", ATTR_DURATION: 3}, + blocking=True, + ) + + assert len(aioclient_mock.mock_calls) == 0 diff --git a/tests/components/rainbird/test_number.py b/tests/components/rainbird/test_number.py new file mode 100644 index 00000000000..e5480da6ee3 --- /dev/null +++ b/tests/components/rainbird/test_number.py @@ -0,0 +1,87 @@ +"""Tests for rainbird number platform.""" + + +import pytest + +from homeassistant.components import number +from homeassistant.components.rainbird import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .conftest import ( + ACK_ECHO, + RAIN_DELAY, + RAIN_DELAY_OFF, + SERIAL_NUMBER, + ComponentSetup, + mock_response, +) + +from tests.test_util.aiohttp import AiohttpClientMocker + + +@pytest.fixture +def platforms() -> list[str]: + """Fixture to specify platforms to test.""" + return [Platform.NUMBER] + + +@pytest.mark.parametrize( + "rain_delay_response,expected_state", + [(RAIN_DELAY, "16"), (RAIN_DELAY_OFF, "0")], +) +async def test_number_values( + hass: HomeAssistant, + setup_integration: ComponentSetup, + expected_state: str, +) -> None: + """Test sensor platform.""" + + assert await setup_integration() + + raindelay = hass.states.get("number.rain_bird_controller_rain_delay") + assert raindelay is not None + assert raindelay.state == expected_state + assert raindelay.attributes == { + "friendly_name": "Rain Bird Controller Rain delay", + "icon": "mdi:water-off", + "min": 0, + "max": 14, + "mode": "auto", + "step": 1, + "unit_of_measurement": "d", + } + + +async def test_set_value( + hass: HomeAssistant, + setup_integration: ComponentSetup, + aioclient_mock: AiohttpClientMocker, + responses: list[str], + config_entry: ConfigEntry, +) -> None: + """Test setting the rain delay number.""" + + assert await setup_integration() + + device_registry = dr.async_get(hass) + device = device_registry.async_get_device({(DOMAIN, SERIAL_NUMBER)}) + assert device + assert device.name == "Rain Bird Controller" + + aioclient_mock.mock_calls.clear() + responses.append(mock_response(ACK_ECHO)) + + await hass.services.async_call( + number.DOMAIN, + number.SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: "number.rain_bird_controller_rain_delay", + number.ATTR_VALUE: 3, + }, + blocking=True, + ) + + assert len(aioclient_mock.mock_calls) == 1 diff --git a/tests/components/rainbird/test_sensor.py b/tests/components/rainbird/test_sensor.py index b80e014b236..694c7245b38 100644 --- a/tests/components/rainbird/test_sensor.py +++ b/tests/components/rainbird/test_sensor.py @@ -6,15 +6,7 @@ import pytest from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .conftest import ( - RAIN_DELAY, - RAIN_SENSOR_OFF, - RAIN_SENSOR_ON, - ComponentSetup, - mock_response, -) - -from tests.test_util.aiohttp import AiohttpClientMockResponse +from .conftest import RAIN_DELAY, RAIN_DELAY_OFF, ComponentSetup @pytest.fixture @@ -24,26 +16,22 @@ def platforms() -> list[str]: @pytest.mark.parametrize( - "sensor_payload,expected_state", - [(RAIN_SENSOR_OFF, "False"), (RAIN_SENSOR_ON, "True")], + "rain_delay_response,expected_state", + [(RAIN_DELAY, "16"), (RAIN_DELAY_OFF, "0")], ) async def test_sensors( hass: HomeAssistant, setup_integration: ComponentSetup, - responses: list[AiohttpClientMockResponse], - sensor_payload: str, - expected_state: bool, + expected_state: str, ) -> None: """Test sensor platform.""" - responses.extend([mock_response(sensor_payload), mock_response(RAIN_DELAY)]) - assert await setup_integration() - rainsensor = hass.states.get("sensor.rainsensor") - assert rainsensor is not None - assert rainsensor.state == expected_state - raindelay = hass.states.get("sensor.raindelay") assert raindelay is not None - assert raindelay.state == "16" + assert raindelay.state == expected_state + assert raindelay.attributes == { + "friendly_name": "Raindelay", + "icon": "mdi:water-off", + } diff --git a/tests/components/rainbird/test_switch.py b/tests/components/rainbird/test_switch.py index d6e89c58527..5f84c5d154e 100644 --- a/tests/components/rainbird/test_switch.py +++ b/tests/components/rainbird/test_switch.py @@ -1,9 +1,6 @@ """Tests for rainbird sensor platform.""" -from http import HTTPStatus -import logging - import pytest from homeassistant.components.rainbird import DOMAIN @@ -12,11 +9,12 @@ from homeassistant.core import HomeAssistant from .conftest import ( ACK_ECHO, - AVAILABLE_STATIONS_RESPONSE, EMPTY_STATIONS_RESPONSE, HOST, PASSWORD, - URL, + RAIN_DELAY_OFF, + RAIN_SENSOR_OFF, + SERIAL_RESPONSE, ZONE_3_ON_RESPONSE, ZONE_5_ON_RESPONSE, ZONE_OFF_RESPONSE, @@ -34,20 +32,26 @@ def platforms() -> list[str]: return [Platform.SWITCH] +@pytest.mark.parametrize( + "stations_response", + [EMPTY_STATIONS_RESPONSE], +) async def test_no_zones( hass: HomeAssistant, setup_integration: ComponentSetup, - responses: list[AiohttpClientMockResponse], ) -> None: """Test case where listing stations returns no stations.""" - responses.append(mock_response(EMPTY_STATIONS_RESPONSE)) assert await setup_integration() - zone = hass.states.get("switch.sprinkler_1") + zone = hass.states.get("switch.rain_bird_sprinkler_1") assert zone is None +@pytest.mark.parametrize( + "zone_state_response", + [ZONE_5_ON_RESPONSE], +) async def test_zones( hass: HomeAssistant, setup_integration: ComponentSetup, @@ -55,41 +59,45 @@ async def test_zones( ) -> None: """Test switch platform with fake data that creates 7 zones with one enabled.""" - responses.extend( - [mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_5_ON_RESPONSE)] - ) - assert await setup_integration() - zone = hass.states.get("switch.sprinkler_1") + zone = hass.states.get("switch.rain_bird_sprinkler_1") + assert zone is not None + assert zone.state == "off" + assert zone.attributes == { + "friendly_name": "Rain Bird Sprinkler 1", + "zone": 1, + } + + zone = hass.states.get("switch.rain_bird_sprinkler_2") + assert zone is not None + assert zone.state == "off" + assert zone.attributes == { + "friendly_name": "Rain Bird Sprinkler 2", + "zone": 2, + } + + zone = hass.states.get("switch.rain_bird_sprinkler_3") assert zone is not None assert zone.state == "off" - zone = hass.states.get("switch.sprinkler_2") + zone = hass.states.get("switch.rain_bird_sprinkler_4") assert zone is not None assert zone.state == "off" - zone = hass.states.get("switch.sprinkler_3") - assert zone is not None - assert zone.state == "off" - - zone = hass.states.get("switch.sprinkler_4") - assert zone is not None - assert zone.state == "off" - - zone = hass.states.get("switch.sprinkler_5") + zone = hass.states.get("switch.rain_bird_sprinkler_5") assert zone is not None assert zone.state == "on" - zone = hass.states.get("switch.sprinkler_6") + zone = hass.states.get("switch.rain_bird_sprinkler_6") assert zone is not None assert zone.state == "off" - zone = hass.states.get("switch.sprinkler_7") + zone = hass.states.get("switch.rain_bird_sprinkler_7") assert zone is not None assert zone.state == "off" - assert not hass.states.get("switch.sprinkler_8") + assert not hass.states.get("switch.rain_bird_sprinkler_8") async def test_switch_on( @@ -100,14 +108,11 @@ async def test_switch_on( ) -> None: """Test turning on irrigation switch.""" - responses.extend( - [mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_OFF_RESPONSE)] - ) assert await setup_integration() # Initially all zones are off. Pick zone3 as an arbitrary to assert # state, then update below as a switch. - zone = hass.states.get("switch.sprinkler_3") + zone = hass.states.get("switch.rain_bird_sprinkler_3") assert zone is not None assert zone.state == "off" @@ -115,20 +120,25 @@ async def test_switch_on( responses.extend( [ mock_response(ACK_ECHO), # Switch on response - mock_response(ZONE_3_ON_RESPONSE), # Updated zone state + # API responses when state is refreshed + mock_response(ZONE_3_ON_RESPONSE), + mock_response(RAIN_SENSOR_OFF), + mock_response(RAIN_DELAY_OFF), ] ) - await switch_common.async_turn_on(hass, "switch.sprinkler_3") + await switch_common.async_turn_on(hass, "switch.rain_bird_sprinkler_3") await hass.async_block_till_done() - assert len(aioclient_mock.mock_calls) == 2 - aioclient_mock.mock_calls.clear() # Verify switch state is updated - zone = hass.states.get("switch.sprinkler_3") + zone = hass.states.get("switch.rain_bird_sprinkler_3") assert zone is not None assert zone.state == "on" +@pytest.mark.parametrize( + "zone_state_response", + [ZONE_3_ON_RESPONSE], +) async def test_switch_off( hass: HomeAssistant, setup_integration: ComponentSetup, @@ -137,13 +147,10 @@ async def test_switch_off( ) -> None: """Test turning off irrigation switch.""" - responses.extend( - [mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_3_ON_RESPONSE)] - ) assert await setup_integration() # Initially the test zone is on - zone = hass.states.get("switch.sprinkler_3") + zone = hass.states.get("switch.rain_bird_sprinkler_3") assert zone is not None assert zone.state == "on" @@ -152,16 +159,15 @@ async def test_switch_off( [ mock_response(ACK_ECHO), # Switch off response mock_response(ZONE_OFF_RESPONSE), # Updated zone state + mock_response(RAIN_SENSOR_OFF), + mock_response(RAIN_DELAY_OFF), ] ) - await switch_common.async_turn_off(hass, "switch.sprinkler_3") + await switch_common.async_turn_off(hass, "switch.rain_bird_sprinkler_3") await hass.async_block_till_done() - # One call to change the service and one to refresh state - assert len(aioclient_mock.mock_calls) == 2 - # Verify switch state is updated - zone = hass.states.get("switch.sprinkler_3") + zone = hass.states.get("switch.rain_bird_sprinkler_3") assert zone is not None assert zone.state == "off" @@ -171,114 +177,60 @@ async def test_irrigation_service( setup_integration: ComponentSetup, aioclient_mock: AiohttpClientMocker, responses: list[AiohttpClientMockResponse], + api_responses: list[str], ) -> None: """Test calling the irrigation service.""" - responses.extend( - [mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_3_ON_RESPONSE)] - ) assert await setup_integration() - aioclient_mock.mock_calls.clear() - responses.extend([mock_response(ACK_ECHO), mock_response(ZONE_OFF_RESPONSE)]) - - await hass.services.async_call( - DOMAIN, - "start_irrigation", - {ATTR_ENTITY_ID: "switch.sprinkler_5", "duration": 30}, - blocking=True, - ) - - # One call to change the service and one to refresh state - assert len(aioclient_mock.mock_calls) == 2 - - -async def test_rain_delay_service( - hass: HomeAssistant, - setup_integration: ComponentSetup, - aioclient_mock: AiohttpClientMocker, - responses: list[AiohttpClientMockResponse], -) -> None: - """Test calling the rain delay service.""" - - responses.extend( - [mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_3_ON_RESPONSE)] - ) - assert await setup_integration() + zone = hass.states.get("switch.rain_bird_sprinkler_3") + assert zone is not None + assert zone.state == "off" aioclient_mock.mock_calls.clear() responses.extend( [ mock_response(ACK_ECHO), + # API responses when state is refreshed + mock_response(ZONE_3_ON_RESPONSE), + mock_response(RAIN_SENSOR_OFF), + mock_response(RAIN_DELAY_OFF), ] ) await hass.services.async_call( - DOMAIN, "set_rain_delay", {"duration": 30}, blocking=True + DOMAIN, + "start_irrigation", + {ATTR_ENTITY_ID: "switch.rain_bird_sprinkler_3", "duration": 30}, + blocking=True, ) - assert len(aioclient_mock.mock_calls) == 1 - - -async def test_platform_unavailable( - hass: HomeAssistant, - setup_integration: ComponentSetup, - responses: list[AiohttpClientMockResponse], - caplog: pytest.LogCaptureFixture, -) -> None: - """Test failure while listing the stations when setting up the platform.""" - - responses.append( - AiohttpClientMockResponse("POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE) - ) - - with caplog.at_level(logging.WARNING): - assert await setup_integration() - - assert "Failed to get stations" in caplog.text - - -async def test_coordinator_unavailable( - hass: HomeAssistant, - setup_integration: ComponentSetup, - responses: list[AiohttpClientMockResponse], - caplog: pytest.LogCaptureFixture, -) -> None: - """Test failure to refresh the update coordinator.""" - - responses.extend( - [ - mock_response(AVAILABLE_STATIONS_RESPONSE), - AiohttpClientMockResponse( - "POST", URL, status=HTTPStatus.SERVICE_UNAVAILABLE - ), - ], - ) - - with caplog.at_level(logging.WARNING): - assert await setup_integration() - - assert "Failed to load zone state" in caplog.text + zone = hass.states.get("switch.rain_bird_sprinkler_3") + assert zone is not None + assert zone.state == "on" @pytest.mark.parametrize( - "yaml_config", + "yaml_config,config_entry_data", [ - { - DOMAIN: { - "host": HOST, - "password": PASSWORD, - "trigger_time": 360, - "zones": { - 1: { - "friendly_name": "Garden Sprinkler", + ( + { + DOMAIN: { + "host": HOST, + "password": PASSWORD, + "trigger_time": 360, + "zones": { + 1: { + "friendly_name": "Garden Sprinkler", + }, + 2: { + "friendly_name": "Back Yard", + }, }, - 2: { - "friendly_name": "Back Yard", - }, - }, - } - }, + } + }, + None, + ) ], ) async def test_yaml_config( @@ -287,15 +239,11 @@ async def test_yaml_config( responses: list[AiohttpClientMockResponse], ) -> None: """Test switch platform with fake data that creates 7 zones with one enabled.""" - - responses.extend( - [mock_response(AVAILABLE_STATIONS_RESPONSE), mock_response(ZONE_5_ON_RESPONSE)] - ) - + responses.insert(0, mock_response(SERIAL_RESPONSE)) # Extra import request assert await setup_integration() assert hass.states.get("switch.garden_sprinkler") - assert not hass.states.get("switch.sprinkler_1") + assert not hass.states.get("switch.rain_bird_sprinkler_1") assert hass.states.get("switch.back_yard") - assert not hass.states.get("switch.sprinkler_2") - assert hass.states.get("switch.sprinkler_3") + assert not hass.states.get("switch.rain_bird_sprinkler_2") + assert hass.states.get("switch.rain_bird_sprinkler_3") diff --git a/tests/components/rdw/test_config_flow.py b/tests/components/rdw/test_config_flow.py index 0fe40c29dfa..8dc0dac8b9d 100644 --- a/tests/components/rdw/test_config_flow.py +++ b/tests/components/rdw/test_config_flow.py @@ -20,7 +20,6 @@ async def test_full_user_flow( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -48,7 +47,6 @@ async def test_full_flow_with_authentication_error( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_rdw_config_flow.vehicle.side_effect = RDWUnknownLicensePlateError result2 = await hass.config_entries.flow.async_configure( @@ -61,7 +59,6 @@ async def test_full_flow_with_authentication_error( assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": "unknown_license_plate"} - assert "flow_id" in result2 mock_rdw_config_flow.vehicle.side_effect = None result3 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/recollect_waste/conftest.py b/tests/components/recollect_waste/conftest.py index 9373a9aa969..39bcb7f4e07 100644 --- a/tests/components/recollect_waste/conftest.py +++ b/tests/components/recollect_waste/conftest.py @@ -1,6 +1,6 @@ """Define test fixtures for ReCollect Waste.""" from datetime import date -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch from aiorecollect.client import PickupEvent, PickupType import pytest @@ -10,48 +10,64 @@ from homeassistant.components.recollect_waste.const import ( CONF_SERVICE_ID, DOMAIN, ) -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry +TEST_PLACE_ID = "12345" +TEST_SERVICE_ID = "67890" + + +@pytest.fixture(name="client") +def client_fixture(pickup_events): + """Define a fixture to return a mocked aiopurple API object.""" + return Mock(async_get_pickup_events=AsyncMock(return_value=pickup_events)) + @pytest.fixture(name="config_entry") def config_entry_fixture(hass, config): """Define a config entry fixture.""" entry = MockConfigEntry( - domain=DOMAIN, - unique_id=f"{config[CONF_PLACE_ID]}, {config[CONF_SERVICE_ID]}", - data=config, + domain=DOMAIN, unique_id=f"{TEST_PLACE_ID}, {TEST_SERVICE_ID}", data=config ) entry.add_to_hass(hass) return entry @pytest.fixture(name="config") -def config_fixture(hass): +def config_fixture(): """Define a config entry data fixture.""" return { - CONF_PLACE_ID: "12345", - CONF_SERVICE_ID: "12345", + CONF_PLACE_ID: TEST_PLACE_ID, + CONF_SERVICE_ID: TEST_SERVICE_ID, } -@pytest.fixture(name="setup_recollect_waste") -async def setup_recollect_waste_fixture(hass, config): - """Define a fixture to set up ReCollect Waste.""" - pickup_event = PickupEvent( - date(2022, 1, 23), [PickupType("garbage", "Trash Collection")], "The Sun" - ) +@pytest.fixture(name="pickup_events") +def pickup_events_fixture(): + """Define a list of pickup events.""" + return [ + PickupEvent( + date(2022, 1, 23), [PickupType("garbage", "Trash Collection")], "The Sun" + ) + ] + +@pytest.fixture(name="mock_aiorecollect") +async def mock_aiorecollect_fixture(client): + """Define a fixture to patch aiorecollect.""" with patch( - "homeassistant.components.recollect_waste.Client.async_get_pickup_events", - return_value=[pickup_event], + "homeassistant.components.recollect_waste.Client", + return_value=client, ), patch( - "homeassistant.components.recollect_waste.config_flow.Client.async_get_pickup_events", - return_value=[pickup_event], - ), patch( - "homeassistant.components.recollect_waste.PLATFORMS", [] + "homeassistant.components.recollect_waste.config_flow.Client", + return_value=client, ): - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() yield + + +@pytest.fixture(name="setup_config_entry") +async def setup_config_entry_fixture(hass, config_entry, mock_aiorecollect): + """Define a fixture to set up recollect_waste.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield diff --git a/tests/components/recollect_waste/test_config_flow.py b/tests/components/recollect_waste/test_config_flow.py index ba09a2f6d6b..64f71ace42f 100644 --- a/tests/components/recollect_waste/test_config_flow.py +++ b/tests/components/recollect_waste/test_config_flow.py @@ -1,7 +1,8 @@ """Define tests for the ReCollect Waste config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from aiorecollect.errors import RecollectError +import pytest from homeassistant import data_entry_flow from homeassistant.components.recollect_waste import ( @@ -12,8 +13,53 @@ from homeassistant.components.recollect_waste import ( from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_FRIENDLY_NAME +from .conftest import TEST_PLACE_ID, TEST_SERVICE_ID -async def test_duplicate_error(hass, config, config_entry): + +@pytest.mark.parametrize( + "get_pickup_events_mock,get_pickup_events_errors", + [ + ( + AsyncMock(side_effect=RecollectError), + {"base": "invalid_place_or_service_id"}, + ), + ], +) +async def test_create_entry( + hass, + client, + config, + get_pickup_events_errors, + get_pickup_events_mock, + mock_aiorecollect, +): + """Test creating an entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + + # Test errors that can arise when checking the API key: + with patch.object(client, "async_get_pickup_events", get_pickup_events_mock): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == get_pickup_events_errors + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == f"{TEST_PLACE_ID}, {TEST_SERVICE_ID}" + assert result["data"] == { + CONF_PLACE_ID: TEST_PLACE_ID, + CONF_SERVICE_ID: TEST_SERVICE_ID, + } + + +async def test_duplicate_error(hass, config, setup_config_entry): """Test that errors are shown when duplicates are added.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config @@ -22,51 +68,14 @@ async def test_duplicate_error(hass, config, config_entry): assert result["reason"] == "already_configured" -async def test_invalid_place_or_service_id(hass, config): - """Test that an invalid Place or Service ID throws an error.""" - with patch( - "homeassistant.components.recollect_waste.config_flow.Client.async_get_pickup_events", - side_effect=RecollectError, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {"base": "invalid_place_or_service_id"} - - -async def test_options_flow(hass, config, config_entry): +async def test_options_flow(hass, config, config_entry, setup_config_entry): """Test config flow options.""" - with patch( - "homeassistant.components.recollect_waste.async_setup_entry", return_value=True - ): - await hass.config_entries.async_setup(config_entry.entry_id) - result = await hass.config_entries.options.async_init(config_entry.entry_id) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "init" - - result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_FRIENDLY_NAME: True} - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert config_entry.options == {CONF_FRIENDLY_NAME: True} - - -async def test_show_form(hass): - """Test that the form is served with no input.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) + result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" + assert result["step_id"] == "init" - -async def test_step_user(hass, config, setup_recollect_waste): - """Test that the user step works.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_FRIENDLY_NAME: True} ) - await hass.async_block_till_done() assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "12345, 12345" - assert result["data"] == {CONF_PLACE_ID: "12345", CONF_SERVICE_ID: "12345"} + assert config_entry.options == {CONF_FRIENDLY_NAME: True} diff --git a/tests/components/recollect_waste/test_diagnostics.py b/tests/components/recollect_waste/test_diagnostics.py index 93978135681..8942fdc4ec1 100644 --- a/tests/components/recollect_waste/test_diagnostics.py +++ b/tests/components/recollect_waste/test_diagnostics.py @@ -1,12 +1,12 @@ """Test ReCollect Waste diagnostics.""" from homeassistant.components.diagnostics import REDACTED +from .conftest import TEST_SERVICE_ID + from tests.components.diagnostics import get_diagnostics_for_config_entry -async def test_entry_diagnostics( - hass, config_entry, hass_client, setup_recollect_waste -): +async def test_entry_diagnostics(hass, config_entry, hass_client, setup_config_entry): """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { @@ -14,7 +14,7 @@ async def test_entry_diagnostics( "version": 2, "domain": "recollect_waste", "title": REDACTED, - "data": {"place_id": REDACTED, "service_id": "12345"}, + "data": {"place_id": REDACTED, "service_id": TEST_SERVICE_ID}, "options": {}, "pref_disable_new_entities": False, "pref_disable_polling": False, diff --git a/tests/components/recorder/db_schema_23_with_newer_columns.py b/tests/components/recorder/db_schema_23_with_newer_columns.py index a086aa588d4..d63e8d59d25 100644 --- a/tests/components/recorder/db_schema_23_with_newer_columns.py +++ b/tests/components/recorder/db_schema_23_with_newer_columns.py @@ -14,6 +14,7 @@ from __future__ import annotations from datetime import datetime, timedelta import json import logging +import time from typing import TypedDict, overload from sqlalchemy import ( @@ -89,6 +90,8 @@ DOUBLE_TYPE = ( .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") ) +TIMESTAMP_TYPE = DOUBLE_TYPE + class Events(Base): # type: ignore """Event history data.""" @@ -108,6 +111,9 @@ class Events(Base): # type: ignore SmallInteger ) # *** Not originally in v23, only added for recorder to startup ok time_fired = Column(DATETIME_TYPE, index=True) + time_fired_ts = Column( + TIMESTAMP_TYPE, index=True + ) # *** Not originally in v23, only added for recorder to startup ok created = Column(DATETIME_TYPE, default=dt_util.utcnow) context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) @@ -197,7 +203,13 @@ class States(Base): # type: ignore Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True ) last_changed = Column(DATETIME_TYPE, default=dt_util.utcnow) + last_updated_ts = Column( + TIMESTAMP_TYPE, default=time.time + ) # *** Not originally in v23, only added for recorder to startup ok last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) + last_updated_ts = Column( + TIMESTAMP_TYPE, default=time.time, index=True + ) # *** Not originally in v23, only added for recorder to startup ok created = Column(DATETIME_TYPE, default=dt_util.utcnow) old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) event = relationship("Events", uselist=False) diff --git a/tests/components/recorder/db_schema_30.py b/tests/components/recorder/db_schema_30.py new file mode 100644 index 00000000000..01c31807ff7 --- /dev/null +++ b/tests/components/recorder/db_schema_30.py @@ -0,0 +1,679 @@ +"""Models for SQLAlchemy. + +This file contains the model definitions for schema version 30. +It is used to test the schema migration logic. +""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime, timedelta +import logging +from typing import Any, TypedDict, TypeVar, cast, overload + +import ciso8601 +from fnvhash import fnv1a_32 +from sqlalchemy import ( + JSON, + BigInteger, + Boolean, + Column, + DateTime, + Float, + ForeignKey, + Identity, + Index, + Integer, + SmallInteger, + String, + Text, + distinct, + type_coerce, +) +from sqlalchemy.dialects import mysql, oracle, postgresql, sqlite +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.orm import aliased, declarative_base, relationship +from sqlalchemy.orm.session import Session + +from homeassistant.components.recorder.const import SupportedDialect +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_RESTORED, + ATTR_SUPPORTED_FEATURES, + MAX_LENGTH_EVENT_CONTEXT_ID, + MAX_LENGTH_EVENT_EVENT_TYPE, + MAX_LENGTH_EVENT_ORIGIN, + MAX_LENGTH_STATE_ENTITY_ID, + MAX_LENGTH_STATE_STATE, +) +from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id +from homeassistant.helpers.json import ( + JSON_DECODE_EXCEPTIONS, + JSON_DUMP, + json_bytes, + json_loads, +) +import homeassistant.util.dt as dt_util + +ALL_DOMAIN_EXCLUDE_ATTRS = {ATTR_ATTRIBUTION, ATTR_RESTORED, ATTR_SUPPORTED_FEATURES} + +# SQLAlchemy Schema +# pylint: disable=invalid-name +Base = declarative_base() + +SCHEMA_VERSION = 30 + +_StatisticsBaseSelfT = TypeVar("_StatisticsBaseSelfT", bound="StatisticsBase") + +_LOGGER = logging.getLogger(__name__) + +TABLE_EVENTS = "events" +TABLE_EVENT_DATA = "event_data" +TABLE_STATES = "states" +TABLE_STATE_ATTRIBUTES = "state_attributes" +TABLE_RECORDER_RUNS = "recorder_runs" +TABLE_SCHEMA_CHANGES = "schema_changes" +TABLE_STATISTICS = "statistics" +TABLE_STATISTICS_META = "statistics_meta" +TABLE_STATISTICS_RUNS = "statistics_runs" +TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" + +ALL_TABLES = [ + TABLE_STATES, + TABLE_STATE_ATTRIBUTES, + TABLE_EVENTS, + TABLE_EVENT_DATA, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + TABLE_STATISTICS, + TABLE_STATISTICS_META, + TABLE_STATISTICS_RUNS, + TABLE_STATISTICS_SHORT_TERM, +] + +TABLES_TO_CHECK = [ + TABLE_STATES, + TABLE_EVENTS, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, +] + +LAST_UPDATED_INDEX = "ix_states_last_updated" +ENTITY_ID_LAST_UPDATED_INDEX = "ix_states_entity_id_last_updated" +EVENTS_CONTEXT_ID_INDEX = "ix_events_context_id" +STATES_CONTEXT_ID_INDEX = "ix_states_context_id" + + +class FAST_PYSQLITE_DATETIME(sqlite.DATETIME): # type: ignore[misc] + """Use ciso8601 to parse datetimes instead of sqlalchemy built-in regex.""" + + def result_processor(self, dialect, coltype): # type: ignore[no-untyped-def] + """Offload the datetime parsing to ciso8601.""" + return lambda value: None if value is None else ciso8601.parse_datetime(value) + + +JSON_VARIANT_CAST = Text().with_variant( + postgresql.JSON(none_as_null=True), "postgresql" +) +JSONB_VARIANT_CAST = Text().with_variant( + postgresql.JSONB(none_as_null=True), "postgresql" +) +DATETIME_TYPE = ( + DateTime(timezone=True) + .with_variant(mysql.DATETIME(timezone=True, fsp=6), "mysql") + .with_variant(FAST_PYSQLITE_DATETIME(), "sqlite") +) +DOUBLE_TYPE = ( + Float() + .with_variant(mysql.DOUBLE(asdecimal=False), "mysql") + .with_variant(oracle.DOUBLE_PRECISION(), "oracle") + .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") +) + +TIMESTAMP_TYPE = DOUBLE_TYPE + + +class UnsupportedDialect(Exception): + """The dialect or its version is not supported.""" + + +class StatisticResult(TypedDict): + """Statistic result data class. + + Allows multiple datapoints for the same statistic_id. + """ + + meta: StatisticMetaData + stat: StatisticData + + +class StatisticDataBase(TypedDict): + """Mandatory fields for statistic data class.""" + + start: datetime + + +class StatisticData(StatisticDataBase, total=False): + """Statistic data class.""" + + mean: float + min: float + max: float + last_reset: datetime | None + state: float + sum: float + + +class StatisticMetaData(TypedDict): + """Statistic meta data class.""" + + has_mean: bool + has_sum: bool + name: str | None + source: str + statistic_id: str + unit_of_measurement: str | None + + +class JSONLiteral(JSON): # type: ignore[misc] + """Teach SA how to literalize json.""" + + def literal_processor(self, dialect: str) -> Callable[[Any], str]: + """Processor to convert a value to JSON.""" + + def process(value: Any) -> str: + """Dump json.""" + return JSON_DUMP(value) + + return process + + +EVENT_ORIGIN_ORDER = [EventOrigin.local, EventOrigin.remote] +EVENT_ORIGIN_TO_IDX = {origin: idx for idx, origin in enumerate(EVENT_ORIGIN_ORDER)} + + +class Events(Base): # type: ignore[misc,valid-type] + """Event history data.""" + + __table_args__ = ( + # Used for fetching events at a specific time + # see logbook + Index("ix_events_event_type_time_fired", "event_type", "time_fired"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_EVENTS + event_id = Column(Integer, Identity(), primary_key=True) + event_type = Column(String(MAX_LENGTH_EVENT_EVENT_TYPE)) + event_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + origin = Column(String(MAX_LENGTH_EVENT_ORIGIN)) # no longer used for new rows + origin_idx = Column(SmallInteger) + time_fired = Column(DATETIME_TYPE, index=True) + context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) + context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) + data_id = Column(Integer, ForeignKey("event_data.data_id"), index=True) + event_data_rel = relationship("EventData") + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + @staticmethod + def from_event(event: Event) -> Events: + """Create an event database object from a native event.""" + return Events( + event_type=event.event_type, + event_data=None, + origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), + time_fired=event.time_fired, + context_id=event.context.id, + context_user_id=event.context.user_id, + context_parent_id=event.context.parent_id, + ) + + def to_native(self, validate_entity_id: bool = True) -> Event | None: + """Convert to a native HA Event.""" + context = Context( + id=self.context_id, + user_id=self.context_user_id, + parent_id=self.context_parent_id, + ) + try: + return Event( + self.event_type, + json_loads(self.event_data) if self.event_data else {}, + EventOrigin(self.origin) + if self.origin + else EVENT_ORIGIN_ORDER[self.origin_idx], + process_timestamp(self.time_fired), + context=context, + ) + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails + _LOGGER.exception("Error converting to event: %s", self) + return None + + +class EventData(Base): # type: ignore[misc,valid-type] + """Event data history.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_EVENT_DATA + data_id = Column(Integer, Identity(), primary_key=True) + hash = Column(BigInteger, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_data = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + @staticmethod + def from_event(event: Event) -> EventData: + """Create object from an event.""" + shared_data = json_bytes(event.data) + return EventData( + shared_data=shared_data.decode("utf-8"), + hash=EventData.hash_shared_data_bytes(shared_data), + ) + + @staticmethod + def shared_data_bytes_from_event( + event: Event, dialect: SupportedDialect | None + ) -> bytes: + """Create shared_data from an event.""" + return json_bytes(event.data) + + @staticmethod + def hash_shared_data_bytes(shared_data_bytes: bytes) -> int: + """Return the hash of json encoded shared data.""" + return cast(int, fnv1a_32(shared_data_bytes)) + + def to_native(self) -> dict[str, Any]: + """Convert to an HA state object.""" + try: + return cast(dict[str, Any], json_loads(self.shared_data)) + except JSON_DECODE_EXCEPTIONS: + _LOGGER.exception("Error converting row to event data: %s", self) + return {} + + +class States(Base): # type: ignore[misc,valid-type] + """State change history.""" + + __table_args__ = ( + # Used for fetching the state of entities at a specific time + # (get_states in history.py) + Index(ENTITY_ID_LAST_UPDATED_INDEX, "entity_id", "last_updated"), + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATES + state_id = Column(Integer, Identity(), primary_key=True) + entity_id = Column(String(MAX_LENGTH_STATE_ENTITY_ID)) + state = Column(String(MAX_LENGTH_STATE_STATE)) + attributes = Column( + Text().with_variant(mysql.LONGTEXT, "mysql") + ) # no longer used for new rows + event_id = Column( # no longer used for new rows + Integer, ForeignKey("events.event_id", ondelete="CASCADE"), index=True + ) + last_changed = Column(DATETIME_TYPE) + last_updated = Column(DATETIME_TYPE, default=dt_util.utcnow, index=True) + old_state_id = Column(Integer, ForeignKey("states.state_id"), index=True) + attributes_id = Column( + Integer, ForeignKey("state_attributes.attributes_id"), index=True + ) + context_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID), index=True) + context_user_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) + context_parent_id = Column(String(MAX_LENGTH_EVENT_CONTEXT_ID)) + origin_idx = Column(SmallInteger) # 0 is local, 1 is remote + old_state = relationship("States", remote_side=[state_id]) + state_attributes = relationship("StateAttributes") + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> States: + """Create object from a state_changed event.""" + entity_id = event.data["entity_id"] + state: State | None = event.data.get("new_state") + dbstate = States( + entity_id=entity_id, + attributes=None, + context_id=event.context.id, + context_user_id=event.context.user_id, + context_parent_id=event.context.parent_id, + origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), + ) + + # None state means the state was removed from the state machine + if state is None: + dbstate.state = "" + dbstate.last_updated = event.time_fired + dbstate.last_changed = None + return dbstate + + dbstate.state = state.state + dbstate.last_updated = state.last_updated + if state.last_updated == state.last_changed: + dbstate.last_changed = None + else: + dbstate.last_changed = state.last_changed + + return dbstate + + def to_native(self, validate_entity_id: bool = True) -> State | None: + """Convert to an HA state object.""" + context = Context( + id=self.context_id, + user_id=self.context_user_id, + parent_id=self.context_parent_id, + ) + try: + attrs = json_loads(self.attributes) if self.attributes else {} + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails + _LOGGER.exception("Error converting row to state: %s", self) + return None + if self.last_changed is None or self.last_changed == self.last_updated: + last_changed = last_updated = process_timestamp(self.last_updated) + else: + last_updated = process_timestamp(self.last_updated) + last_changed = process_timestamp(self.last_changed) + return State( + self.entity_id, + self.state, + # Join the state_attributes table on attributes_id to get the attributes + # for newer states + attrs, + last_changed, + last_updated, + context=context, + validate_entity_id=validate_entity_id, + ) + + +class StateAttributes(Base): # type: ignore[misc,valid-type] + """State attribute change history.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATE_ATTRIBUTES + attributes_id = Column(Integer, Identity(), primary_key=True) + hash = Column(BigInteger, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_attrs = Column(Text().with_variant(mysql.LONGTEXT, "mysql")) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def from_event(event: Event) -> StateAttributes: + """Create object from a state_changed event.""" + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + attr_bytes = b"{}" if state is None else json_bytes(state.attributes) + dbstate = StateAttributes(shared_attrs=attr_bytes.decode("utf-8")) + dbstate.hash = StateAttributes.hash_shared_attrs_bytes(attr_bytes) + return dbstate + + @staticmethod + def shared_attrs_bytes_from_event( + event: Event, + exclude_attrs_by_domain: dict[str, set[str]], + dialect: SupportedDialect | None, + ) -> bytes: + """Create shared_attrs from a state_changed event.""" + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + if state is None: + return b"{}" + domain = split_entity_id(state.entity_id)[0] + exclude_attrs = ( + exclude_attrs_by_domain.get(domain, set()) | ALL_DOMAIN_EXCLUDE_ATTRS + ) + return json_bytes( + {k: v for k, v in state.attributes.items() if k not in exclude_attrs} + ) + + @staticmethod + def hash_shared_attrs_bytes(shared_attrs_bytes: bytes) -> int: + """Return the hash of json encoded shared attributes.""" + return cast(int, fnv1a_32(shared_attrs_bytes)) + + def to_native(self) -> dict[str, Any]: + """Convert to an HA state object.""" + try: + return cast(dict[str, Any], json_loads(self.shared_attrs)) + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails + _LOGGER.exception("Error converting row to state attributes: %s", self) + return {} + + +class StatisticsBase: + """Statistics base class.""" + + id = Column(Integer, Identity(), primary_key=True) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + + @declared_attr # type: ignore[misc] + def metadata_id(self) -> Column: + """Define the metadata_id column for sub classes.""" + return Column( + Integer, + ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), + index=True, + ) + + start = Column(DATETIME_TYPE, index=True) + mean = Column(DOUBLE_TYPE) + min = Column(DOUBLE_TYPE) + max = Column(DOUBLE_TYPE) + last_reset = Column(DATETIME_TYPE) + state = Column(DOUBLE_TYPE) + sum = Column(DOUBLE_TYPE) + + @classmethod + def from_stats( + cls: type[_StatisticsBaseSelfT], metadata_id: int, stats: StatisticData + ) -> _StatisticsBaseSelfT: + """Create object from a statistics.""" + return cls( # type: ignore[call-arg,misc] + metadata_id=metadata_id, + **stats, + ) + + +class Statistics(Base, StatisticsBase): # type: ignore[misc,valid-type] + """Long term statistics.""" + + duration = timedelta(hours=1) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index("ix_statistics_statistic_id_start", "metadata_id", "start", unique=True), + ) + __tablename__ = TABLE_STATISTICS + + +class StatisticsShortTerm(Base, StatisticsBase): # type: ignore[misc,valid-type] + """Short term statistics.""" + + duration = timedelta(minutes=5) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index( + "ix_statistics_short_term_statistic_id_start", + "metadata_id", + "start", + unique=True, + ), + ) + __tablename__ = TABLE_STATISTICS_SHORT_TERM + + +class StatisticsMeta(Base): # type: ignore[misc,valid-type] + """Statistics meta data.""" + + __table_args__ = ( + {"mysql_default_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, + ) + __tablename__ = TABLE_STATISTICS_META + id = Column(Integer, Identity(), primary_key=True) + statistic_id = Column(String(255), index=True, unique=True) + source = Column(String(32)) + unit_of_measurement = Column(String(255)) + has_mean = Column(Boolean) + has_sum = Column(Boolean) + name = Column(String(255)) + + @staticmethod + def from_meta(meta: StatisticMetaData) -> StatisticsMeta: + """Create object from meta data.""" + return StatisticsMeta(**meta) + + +class RecorderRuns(Base): # type: ignore[misc,valid-type] + """Representation of recorder run.""" + + __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) + __tablename__ = TABLE_RECORDER_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DATETIME_TYPE, default=dt_util.utcnow) + end = Column(DATETIME_TYPE) + closed_incorrect = Column(Boolean, default=False) + created = Column(DATETIME_TYPE, default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + end = ( + f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None + ) + return ( + f"" + ) + + def entity_ids(self, point_in_time: datetime | None = None) -> list[str]: + """Return the entity ids that existed in this run. + + Specify point_in_time if you want to know which existed at that point + in time inside the run. + """ + session = Session.object_session(self) + + assert session is not None, "RecorderRuns need to be persisted" + + query = session.query(distinct(States.entity_id)).filter( + States.last_updated >= self.start + ) + + if point_in_time is not None: + query = query.filter(States.last_updated < point_in_time) + elif self.end is not None: + query = query.filter(States.last_updated < self.end) + + return [row[0] for row in query] + + def to_native(self, validate_entity_id: bool = True) -> RecorderRuns: + """Return self, native format is this model.""" + return self + + +class SchemaChanges(Base): # type: ignore[misc,valid-type] + """Representation of schema version changes.""" + + __tablename__ = TABLE_SCHEMA_CHANGES + change_id = Column(Integer, Identity(), primary_key=True) + schema_version = Column(Integer) + changed = Column(DATETIME_TYPE, default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + +class StatisticsRuns(Base): # type: ignore[misc,valid-type] + """Representation of statistics run.""" + + __tablename__ = TABLE_STATISTICS_RUNS + run_id = Column(Integer, Identity(), primary_key=True) + start = Column(DATETIME_TYPE, index=True) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +EVENT_DATA_JSON = type_coerce( + EventData.shared_data.cast(JSONB_VARIANT_CAST), JSONLiteral(none_as_null=True) +) +OLD_FORMAT_EVENT_DATA_JSON = type_coerce( + Events.event_data.cast(JSONB_VARIANT_CAST), JSONLiteral(none_as_null=True) +) + +SHARED_ATTRS_JSON = type_coerce( + StateAttributes.shared_attrs.cast(JSON_VARIANT_CAST), JSON(none_as_null=True) +) +OLD_FORMAT_ATTRS_JSON = type_coerce( + States.attributes.cast(JSON_VARIANT_CAST), JSON(none_as_null=True) +) + +ENTITY_ID_IN_EVENT: Column = EVENT_DATA_JSON["entity_id"] +OLD_ENTITY_ID_IN_EVENT: Column = OLD_FORMAT_EVENT_DATA_JSON["entity_id"] +DEVICE_ID_IN_EVENT: Column = EVENT_DATA_JSON["device_id"] +OLD_STATE = aliased(States, name="old_state") + + +@overload +def process_timestamp(ts: None) -> None: + ... + + +@overload +def process_timestamp(ts: datetime) -> datetime: + ... + + +def process_timestamp(ts: datetime | None) -> datetime | None: + """Process a timestamp into datetime object.""" + if ts is None: + return None + if ts.tzinfo is None: + return ts.replace(tzinfo=dt_util.UTC) + + return dt_util.as_utc(ts) diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index 6362b83f78a..c35f0075844 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -1,7 +1,7 @@ """The tests the History component.""" from __future__ import annotations -# pylint: disable=protected-access,invalid-name +# pylint: disable=invalid-name from copy import copy from datetime import datetime, timedelta import json @@ -11,21 +11,29 @@ import pytest from sqlalchemy import text from homeassistant.components import recorder -from homeassistant.components.recorder import history +from homeassistant.components.recorder import get_instance, history from homeassistant.components.recorder.db_schema import ( Events, RecorderRuns, StateAttributes, States, ) -from homeassistant.components.recorder.models import LazyState, process_timestamp +from homeassistant.components.recorder.models import ( + LazyState, + LazyStatePreSchema31, + process_timestamp, +) from homeassistant.components.recorder.util import session_scope import homeassistant.core as ha from homeassistant.core import HomeAssistant, State from homeassistant.helpers.json import JSONEncoder import homeassistant.util.dt as dt_util -from .common import async_wait_recording_done, wait_recording_done +from .common import ( + async_recorder_block_till_done, + async_wait_recording_done, + wait_recording_done, +) from tests.common import SetupRecorderInstanceT, mock_state_change_event @@ -40,10 +48,14 @@ async def _async_get_states( """Get states from the database.""" def _get_states_with_session(): + if get_instance(hass).schema_version < 31: + klass = LazyStatePreSchema31 + else: + klass = LazyState with session_scope(hass=hass) as session: attr_cache = {} return [ - LazyState(row, attr_cache) + klass(row, attr_cache, None) for row in history._get_rows_with_session( hass, session, @@ -422,7 +434,8 @@ def test_get_significant_states_minimal_response(hass_recorder): assert states == hist -def test_get_significant_states_with_initial(hass_recorder): +@pytest.mark.parametrize("time_zone", ["Europe/Berlin", "US/Hawaii", "UTC"]) +def test_get_significant_states_with_initial(time_zone, hass_recorder): """Test that only significant states are returned. We should get back every thermostat change that @@ -430,6 +443,7 @@ def test_get_significant_states_with_initial(hass_recorder): media player (attribute changes are not significant and not returned). """ hass = hass_recorder() + hass.config.set_time_zone(time_zone) zero, four, states = record_states(hass) one = zero + timedelta(seconds=1) one_and_half = zero + timedelta(seconds=1.5) @@ -579,6 +593,27 @@ def test_get_significant_states_only(hass_recorder): assert states == hist[entity_id] +async def test_get_significant_states_only_minimal_response(recorder_mock, hass): + """Test significant states when significant_states_only is True.""" + now = dt_util.utcnow() + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "changed"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "again"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_wait_recording_done(hass) + + hist = history.get_significant_states( + hass, now, minimal_response=True, significant_changes_only=False + ) + assert len(hist["sensor.test"]) == 3 + + def record_states(hass) -> tuple[datetime, datetime, dict[str, list[State]]]: """Record some test states. @@ -884,7 +919,7 @@ async def test_get_full_significant_states_handles_empty_last_changed( != native_sensor_one_states[1].last_updated ) - def _fetch_db_states() -> list[State]: + def _fetch_db_states() -> list[States]: with session_scope(hass=hass) as session: states = list(session.query(States)) session.expunge_all() @@ -894,12 +929,20 @@ async def test_get_full_significant_states_handles_empty_last_changed( _fetch_db_states ) assert db_sensor_one_states[0].last_changed is None + assert db_sensor_one_states[0].last_changed_ts is None + assert ( - process_timestamp(db_sensor_one_states[1].last_changed) == state0.last_changed + process_timestamp( + dt_util.utc_from_timestamp(db_sensor_one_states[1].last_changed_ts) + ) + == state0.last_changed + ) + assert db_sensor_one_states[0].last_updated_ts is not None + assert db_sensor_one_states[1].last_updated_ts is not None + assert ( + db_sensor_one_states[0].last_updated_ts + != db_sensor_one_states[1].last_updated_ts ) - assert db_sensor_one_states[0].last_updated is not None - assert db_sensor_one_states[1].last_updated is not None - assert db_sensor_one_states[0].last_updated != db_sensor_one_states[1].last_updated def test_state_changes_during_period_multiple_entities_single_test(hass_recorder): @@ -929,3 +972,39 @@ def test_state_changes_during_period_multiple_entities_single_test(hass_recorder hist = history.state_changes_during_period(hass, start, end, None) for entity_id, value in test_entites.items(): hist[entity_id][0].state == value + + +@pytest.mark.freeze_time("2039-01-19 03:14:07.555555-00:00") +async def test_get_full_significant_states_past_year_2038( + async_setup_recorder_instance: SetupRecorderInstanceT, + hass: ha.HomeAssistant, +): + """Test we can store times past year 2038.""" + await async_setup_recorder_instance(hass, {}) + past_2038_time = dt_util.parse_datetime("2039-01-19 03:14:07.555555-00:00") + hass.states.async_set("sensor.one", "on", {"attr": "original"}) + state0 = hass.states.get("sensor.one") + await hass.async_block_till_done() + + hass.states.async_set("sensor.one", "on", {"attr": "new"}) + state1 = hass.states.get("sensor.one") + + await async_wait_recording_done(hass) + + def _get_entries(): + with session_scope(hass=hass) as session: + return history.get_full_significant_states_with_session( + hass, + session, + past_2038_time - timedelta(days=365), + past_2038_time + timedelta(days=365), + entity_ids=["sensor.one"], + significant_changes_only=False, + ) + + states = await recorder.get_instance(hass).async_add_executor_job(_get_entries) + sensor_one_states: list[State] = states["sensor.one"] + assert sensor_one_states[0] == state0 + assert sensor_one_states[1] == state1 + assert sensor_one_states[0].last_changed == past_2038_time + assert sensor_one_states[0].last_updated == past_2038_time diff --git a/tests/components/recorder/test_history_db_schema_30.py b/tests/components/recorder/test_history_db_schema_30.py new file mode 100644 index 00000000000..5e944ce454a --- /dev/null +++ b/tests/components/recorder/test_history_db_schema_30.py @@ -0,0 +1,624 @@ +"""The tests the History component.""" +from __future__ import annotations + +# pylint: disable=invalid-name +from copy import copy +from datetime import datetime, timedelta +import importlib +import json +import sys +from unittest.mock import patch, sentinel + +import pytest +from sqlalchemy import create_engine +from sqlalchemy.orm import Session + +from homeassistant.components import recorder +from homeassistant.components.recorder import core, history, statistics +from homeassistant.components.recorder.models import process_timestamp +from homeassistant.components.recorder.util import session_scope +from homeassistant.core import State +from homeassistant.helpers.json import JSONEncoder +import homeassistant.util.dt as dt_util + +from .common import wait_recording_done + +CREATE_ENGINE_TARGET = "homeassistant.components.recorder.core.create_engine" +SCHEMA_MODULE = "tests.components.recorder.db_schema_30" + + +def _create_engine_test(*args, **kwargs): + """Test version of create_engine that initializes with old schema. + + This simulates an existing db with the old schema. + """ + importlib.import_module(SCHEMA_MODULE) + old_db_schema = sys.modules[SCHEMA_MODULE] + engine = create_engine(*args, **kwargs) + old_db_schema.Base.metadata.create_all(engine) + with Session(engine) as session: + session.add( + recorder.db_schema.StatisticsRuns(start=statistics.get_start_time()) + ) + session.add( + recorder.db_schema.SchemaChanges( + schema_version=old_db_schema.SCHEMA_VERSION + ) + ) + session.commit() + return engine + + +@pytest.fixture(autouse=True) +def db_schema_30(): + """Fixture to initialize the db with the old schema.""" + importlib.import_module(SCHEMA_MODULE) + old_db_schema = sys.modules[SCHEMA_MODULE] + + with patch.object(recorder, "db_schema", old_db_schema), patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION + ), patch.object(core, "EventData", old_db_schema.EventData), patch.object( + core, "States", old_db_schema.States + ), patch.object( + core, "Events", old_db_schema.Events + ), patch.object( + core, "StateAttributes", old_db_schema.StateAttributes + ), patch( + CREATE_ENGINE_TARGET, new=_create_engine_test + ): + yield + + +def test_get_full_significant_states_with_session_entity_no_matches(hass_recorder): + """Test getting states at a specific point in time for entities that never have been recorded.""" + hass = hass_recorder() + now = dt_util.utcnow() + time_before_recorder_ran = now - timedelta(days=1000) + with session_scope(hass=hass) as session: + assert ( + history.get_full_significant_states_with_session( + hass, session, time_before_recorder_ran, now, entity_ids=["demo.id"] + ) + == {} + ) + assert ( + history.get_full_significant_states_with_session( + hass, + session, + time_before_recorder_ran, + now, + entity_ids=["demo.id", "demo.id2"], + ) + == {} + ) + + +def test_significant_states_with_session_entity_minimal_response_no_matches( + hass_recorder, +): + """Test getting states at a specific point in time for entities that never have been recorded.""" + hass = hass_recorder() + now = dt_util.utcnow() + time_before_recorder_ran = now - timedelta(days=1000) + with session_scope(hass=hass) as session: + assert ( + history.get_significant_states_with_session( + hass, + session, + time_before_recorder_ran, + now, + entity_ids=["demo.id"], + minimal_response=True, + ) + == {} + ) + assert ( + history.get_significant_states_with_session( + hass, + session, + time_before_recorder_ran, + now, + entity_ids=["demo.id", "demo.id2"], + minimal_response=True, + ) + == {} + ) + + +@pytest.mark.parametrize( + "attributes, no_attributes, limit", + [ + ({"attr": True}, False, 5000), + ({}, True, 5000), + ({"attr": True}, False, 3), + ({}, True, 3), + ], +) +def test_state_changes_during_period(hass_recorder, attributes, no_attributes, limit): + """Test state change during period.""" + hass = hass_recorder() + entity_id = "media_player.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state, attributes) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start + ): + set_state("idle") + set_state("YouTube") + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point + ): + states = [ + set_state("idle"), + set_state("Netflix"), + set_state("Plex"), + set_state("YouTube"), + ] + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=end + ): + set_state("Netflix") + set_state("Plex") + + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes, limit=limit + ) + + assert states[:limit] == hist[entity_id] + + +def test_state_changes_during_period_descending(hass_recorder): + """Test state change during period descending.""" + hass = hass_recorder() + entity_id = "media_player.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state, {"any": 1}) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + point2 = start + timedelta(seconds=1, microseconds=2) + point3 = start + timedelta(seconds=1, microseconds=3) + point4 = start + timedelta(seconds=1, microseconds=4) + end = point + timedelta(seconds=1) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start + ): + set_state("idle") + set_state("YouTube") + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point + ): + states = [set_state("idle")] + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point2 + ): + states.append(set_state("Netflix")) + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point3 + ): + states.append(set_state("Plex")) + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point4 + ): + states.append(set_state("YouTube")) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=end + ): + set_state("Netflix") + set_state("Plex") + + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes=False, descending=False + ) + assert states == hist[entity_id] + + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes=False, descending=True + ) + assert states == list(reversed(list(hist[entity_id]))) + + +def test_get_last_state_changes(hass_recorder): + """Test number of state changes.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + point2 = point + timedelta(minutes=1) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start + ): + set_state("1") + + states = [] + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point + ): + states.append(set_state("2")) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point2 + ): + states.append(set_state("3")) + + hist = history.get_last_state_changes(hass, 2, entity_id) + + assert states == hist[entity_id] + + +def test_ensure_state_can_be_copied(hass_recorder): + """Ensure a state can pass though copy(). + + The filter integration uses copy() on states + from history. + """ + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start + ): + set_state("1") + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=point + ): + set_state("2") + + hist = history.get_last_state_changes(hass, 2, entity_id) + + assert copy(hist[entity_id][0]) == hist[entity_id][0] + assert copy(hist[entity_id][1]) == hist[entity_id][1] + + +def test_get_significant_states(hass_recorder): + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + hist = history.get_significant_states(hass, zero, four) + assert states == hist + + +def test_get_significant_states_minimal_response(hass_recorder): + """Test that only significant states are returned. + + When minimal responses is set only the first and + last states return a complete state. + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + hist = history.get_significant_states(hass, zero, four, minimal_response=True) + entites_with_reducable_states = [ + "media_player.test", + "media_player.test3", + ] + + # All states for media_player.test state are reduced + # down to last_changed and state when minimal_response + # is set except for the first state. + # is set. We use JSONEncoder to make sure that are + # pre-encoded last_changed is always the same as what + # will happen with encoding a native state + for entity_id in entites_with_reducable_states: + entity_states = states[entity_id] + for state_idx in range(1, len(entity_states)): + input_state = entity_states[state_idx] + orig_last_changed = orig_last_changed = json.dumps( + process_timestamp(input_state.last_changed), + cls=JSONEncoder, + ).replace('"', "") + orig_state = input_state.state + entity_states[state_idx] = { + "last_changed": orig_last_changed, + "state": orig_state, + } + assert states == hist + + +def test_get_significant_states_with_initial(hass_recorder): + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + one = zero + timedelta(seconds=1) + one_and_half = zero + timedelta(seconds=1.5) + for entity_id in states: + if entity_id == "media_player.test": + states[entity_id] = states[entity_id][1:] + for state in states[entity_id]: + if state.last_changed == one: + state.last_changed = one_and_half + + hist = history.get_significant_states( + hass, + one_and_half, + four, + include_start_time_state=True, + ) + assert states == hist + + +def test_get_significant_states_without_initial(hass_recorder): + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + one = zero + timedelta(seconds=1) + one_and_half = zero + timedelta(seconds=1.5) + for entity_id in states: + states[entity_id] = list( + filter(lambda s: s.last_changed != one, states[entity_id]) + ) + del states["media_player.test2"] + + hist = history.get_significant_states( + hass, + one_and_half, + four, + include_start_time_state=False, + ) + assert states == hist + + +def test_get_significant_states_entity_id(hass_recorder): + """Test that only significant states are returned for one entity.""" + hass = hass_recorder() + zero, four, states = record_states(hass) + del states["media_player.test2"] + del states["media_player.test3"] + del states["thermostat.test"] + del states["thermostat.test2"] + del states["script.can_cancel_this_one"] + + hist = history.get_significant_states(hass, zero, four, ["media_player.test"]) + assert states == hist + + +def test_get_significant_states_multiple_entity_ids(hass_recorder): + """Test that only significant states are returned for one entity.""" + hass = hass_recorder() + zero, four, states = record_states(hass) + del states["media_player.test2"] + del states["media_player.test3"] + del states["thermostat.test2"] + del states["script.can_cancel_this_one"] + + hist = history.get_significant_states( + hass, + zero, + four, + ["media_player.test", "thermostat.test"], + ) + assert states == hist + + +def test_get_significant_states_are_ordered(hass_recorder): + """Test order of results from get_significant_states. + + When entity ids are given, the results should be returned with the data + in the same order. + """ + hass = hass_recorder() + zero, four, _states = record_states(hass) + entity_ids = ["media_player.test", "media_player.test2"] + hist = history.get_significant_states(hass, zero, four, entity_ids) + assert list(hist.keys()) == entity_ids + entity_ids = ["media_player.test2", "media_player.test"] + hist = history.get_significant_states(hass, zero, four, entity_ids) + assert list(hist.keys()) == entity_ids + + +def test_get_significant_states_only(hass_recorder): + """Test significant states when significant_states_only is set.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=4) + points = [] + for i in range(1, 4): + points.append(start + timedelta(minutes=i)) + + states = [] + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=start + ): + set_state("123", attributes={"attribute": 10.64}) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", + return_value=points[0], + ): + # Attributes are different, state not + states.append(set_state("123", attributes={"attribute": 21.42})) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", + return_value=points[1], + ): + # state is different, attributes not + states.append(set_state("32", attributes={"attribute": 21.42})) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", + return_value=points[2], + ): + # everything is different + states.append(set_state("412", attributes={"attribute": 54.23})) + + hist = history.get_significant_states(hass, start, significant_changes_only=True) + + assert len(hist[entity_id]) == 2 + assert states[0] not in hist[entity_id] + assert states[1] in hist[entity_id] + assert states[2] in hist[entity_id] + + hist = history.get_significant_states(hass, start, significant_changes_only=False) + + assert len(hist[entity_id]) == 3 + assert states == hist[entity_id] + + +def record_states(hass) -> tuple[datetime, datetime, dict[str, list[State]]]: + """Record some test states. + + We inject a bunch of state updates from media player, zone and + thermostat. + """ + mp = "media_player.test" + mp2 = "media_player.test2" + mp3 = "media_player.test3" + therm = "thermostat.test" + therm2 = "thermostat.test2" + zone = "zone.home" + script_c = "script.can_cancel_this_one" + + def set_state(entity_id, state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + zero = dt_util.utcnow() + one = zero + timedelta(seconds=1) + two = one + timedelta(seconds=1) + three = two + timedelta(seconds=1) + four = three + timedelta(seconds=1) + + states = {therm: [], therm2: [], mp: [], mp2: [], mp3: [], script_c: []} + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=one + ): + states[mp].append( + set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[mp].append( + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[mp2].append( + set_state(mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[mp3].append( + set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[therm].append( + set_state(therm, 20, attributes={"current_temperature": 19.5}) + ) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=two + ): + # This state will be skipped only different in time + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)}) + # This state will be skipped because domain is excluded + set_state(zone, "zoning") + states[script_c].append( + set_state(script_c, "off", attributes={"can_cancel": True}) + ) + states[therm].append( + set_state(therm, 21, attributes={"current_temperature": 19.8}) + ) + states[therm2].append( + set_state(therm2, 20, attributes={"current_temperature": 19}) + ) + + with patch( + "homeassistant.components.recorder.core.dt_util.utcnow", return_value=three + ): + states[mp].append( + set_state(mp, "Netflix", attributes={"media_title": str(sentinel.mt4)}) + ) + states[mp3].append( + set_state(mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)}) + ) + # Attributes changed even though state is the same + states[therm].append( + set_state(therm, 21, attributes={"current_temperature": 20}) + ) + + return zero, four, states + + +def test_state_changes_during_period_multiple_entities_single_test(hass_recorder): + """Test state change during period with multiple entities in the same test. + + This test ensures the sqlalchemy query cache does not + generate incorrect results. + """ + hass = hass_recorder() + start = dt_util.utcnow() + test_entites = {f"sensor.{i}": str(i) for i in range(30)} + for entity_id, value in test_entites.items(): + hass.states.set(entity_id, value) + + wait_recording_done(hass) + end = dt_util.utcnow() + + hist = history.state_changes_during_period(hass, start, end, None) + for entity_id, value in test_entites.items(): + hist[entity_id][0].state == value + + for entity_id, value in test_entites.items(): + hist = history.state_changes_during_period(hass, start, end, entity_id) + assert len(hist) == 1 + hist[entity_id][0].state == value + + hist = history.state_changes_during_period(hass, start, end, None) + for entity_id, value in test_entites.items(): + hist[entity_id][0].state == value diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index e13f1b873bd..c06865fb5a3 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1,7 +1,6 @@ """The tests for the Recorder component.""" from __future__ import annotations -# pylint: disable=protected-access import asyncio from datetime import datetime, timedelta import sqlite3 @@ -32,6 +31,7 @@ from homeassistant.components.recorder.const import ( EVENT_RECORDER_5MIN_STATISTICS_GENERATED, EVENT_RECORDER_HOURLY_STATISTICS_GENERATED, KEEPALIVE_TIME, + SupportedDialect, ) from homeassistant.components.recorder.db_schema import ( SCHEMA_VERSION, @@ -224,6 +224,42 @@ async def test_saving_state(recorder_mock, hass: HomeAssistant): assert state == _state_with_context(hass, entity_id) +@pytest.mark.parametrize( + "dialect_name, expected_attributes", + ( + (SupportedDialect.MYSQL, {"test_attr": 5, "test_attr_10": "silly\0stuff"}), + (SupportedDialect.POSTGRESQL, {"test_attr": 5, "test_attr_10": "silly"}), + (SupportedDialect.SQLITE, {"test_attr": 5, "test_attr_10": "silly\0stuff"}), + ), +) +async def test_saving_state_with_nul( + recorder_mock, hass: HomeAssistant, dialect_name, expected_attributes +): + """Test saving and restoring a state with nul in attributes.""" + entity_id = "test.recorder" + state = "restoring_from_db" + attributes = {"test_attr": 5, "test_attr_10": "silly\0stuff"} + + with patch( + "homeassistant.components.recorder.core.Recorder.dialect_name", dialect_name + ): + hass.states.async_set(entity_id, state, attributes) + await async_wait_recording_done(hass) + + with session_scope(hass=hass) as session: + db_states = [] + for db_state, db_state_attributes in session.query(States, StateAttributes): + db_states.append(db_state) + state = db_state.to_native() + state.attributes = db_state_attributes.to_native() + assert len(db_states) == 1 + assert db_states[0].event_id is None + + expected = _state_with_context(hass, entity_id) + expected.attributes = expected_attributes + assert state == expected + + async def test_saving_many_states( async_setup_recorder_instance: SetupRecorderInstanceT, hass: HomeAssistant ): @@ -505,7 +541,7 @@ def test_setup_without_migration(hass_recorder): assert recorder.get_instance(hass).schema_version == SCHEMA_VERSION -# pylint: disable=redefined-outer-name,invalid-name +# pylint: disable=invalid-name def test_saving_state_include_domains(hass_recorder): """Test saving and restoring a state.""" hass = hass_recorder({"include": {"domains": "test2"}}) diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 45268ae819b..d04d436f72c 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -1,5 +1,5 @@ """The tests for the Recorder component.""" -# pylint: disable=protected-access + import datetime import importlib import sqlite3 diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index 81469ab1dab..646b865477a 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -58,6 +58,61 @@ def test_from_event_to_db_state_attributes(): assert StateAttributes.from_event(event).to_native() == attrs +def test_repr(): + """Test converting event to db state repr.""" + attrs = {"this_attr": True} + fixed_time = datetime(2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC, microsecond=432432) + state = ha.State( + "sensor.temperature", + "18", + attrs, + last_changed=fixed_time, + last_updated=fixed_time, + ) + event = ha.Event( + EVENT_STATE_CHANGED, + {"entity_id": "sensor.temperature", "old_state": None, "new_state": state}, + context=state.context, + time_fired=fixed_time, + ) + assert "2016-07-09 11:00:00+00:00" in repr(States.from_event(event)) + assert "2016-07-09 11:00:00+00:00" in repr(Events.from_event(event)) + + +def test_states_repr_without_timestamp(): + """Test repr for a state without last_updated_ts.""" + fixed_time = datetime(2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC, microsecond=432432) + states = States( + entity_id="sensor.temp", + attributes=None, + context_id=None, + context_user_id=None, + context_parent_id=None, + origin_idx=None, + last_updated=fixed_time, + last_changed=fixed_time, + last_updated_ts=None, + last_changed_ts=None, + ) + assert "2016-07-09 11:00:00+00:00" in repr(states) + + +def test_events_repr_without_timestamp(): + """Test repr for an event without time_fired_ts.""" + fixed_time = datetime(2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC, microsecond=432432) + events = Events( + event_type="any", + event_data=None, + origin_idx=None, + time_fired=fixed_time, + time_fired_ts=None, + context_id=None, + context_user_id=None, + context_parent_id=None, + ) + assert "2016-07-09 11:00:00+00:00" in repr(events) + + def test_handling_broken_json_state_attributes(caplog): """Test we handle broken json in state attributes.""" state_attributes = StateAttributes( @@ -81,8 +136,8 @@ def test_from_event_to_delete_state(): assert db_state.entity_id == "sensor.temperature" assert db_state.state == "" - assert db_state.last_changed is None - assert db_state.last_updated == event.time_fired + assert db_state.last_changed_ts is None + assert db_state.last_updated_ts == event.time_fired.timestamp() def test_entity_ids(): @@ -251,7 +306,7 @@ async def test_lazy_state_handles_include_json(caplog): entity_id="sensor.invalid", shared_attrs="{INVALID_JSON}", ) - assert LazyState(row, {}).attributes == {} + assert LazyState(row, {}, None).attributes == {} assert "Error converting row to state attributes" in caplog.text @@ -262,7 +317,7 @@ async def test_lazy_state_prefers_shared_attrs_over_attrs(caplog): shared_attrs='{"shared":true}', attributes='{"shared":false}', ) - assert LazyState(row, {}).attributes == {"shared": True} + assert LazyState(row, {}, None).attributes == {"shared": True} async def test_lazy_state_handles_different_last_updated_and_last_changed(caplog): @@ -272,10 +327,10 @@ async def test_lazy_state_handles_different_last_updated_and_last_changed(caplog entity_id="sensor.valid", state="off", shared_attrs='{"shared":true}', - last_updated=now, - last_changed=now - timedelta(seconds=60), + last_updated_ts=now.timestamp(), + last_changed_ts=(now - timedelta(seconds=60)).timestamp(), ) - lstate = LazyState(row, {}) + lstate = LazyState(row, {}, None) assert lstate.as_dict() == { "attributes": {"shared": True}, "entity_id": "sensor.valid", @@ -283,8 +338,8 @@ async def test_lazy_state_handles_different_last_updated_and_last_changed(caplog "last_updated": "2021-06-12T03:04:01.000323+00:00", "state": "off", } - assert lstate.last_updated == row.last_updated - assert lstate.last_changed == row.last_changed + assert lstate.last_updated.timestamp() == row.last_updated_ts + assert lstate.last_changed.timestamp() == row.last_changed_ts assert lstate.as_dict() == { "attributes": {"shared": True}, "entity_id": "sensor.valid", @@ -301,10 +356,10 @@ async def test_lazy_state_handles_same_last_updated_and_last_changed(caplog): entity_id="sensor.valid", state="off", shared_attrs='{"shared":true}', - last_updated=now, - last_changed=now, + last_updated_ts=now.timestamp(), + last_changed_ts=now.timestamp(), ) - lstate = LazyState(row, {}) + lstate = LazyState(row, {}, None) assert lstate.as_dict() == { "attributes": {"shared": True}, "entity_id": "sensor.valid", @@ -312,8 +367,8 @@ async def test_lazy_state_handles_same_last_updated_and_last_changed(caplog): "last_updated": "2021-06-12T03:04:01.000323+00:00", "state": "off", } - assert lstate.last_updated == row.last_updated - assert lstate.last_changed == row.last_changed + assert lstate.last_updated.timestamp() == row.last_updated_ts + assert lstate.last_changed.timestamp() == row.last_changed_ts assert lstate.as_dict() == { "attributes": {"shared": True}, "entity_id": "sensor.valid", diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index f135ae8af43..a3b32fc7e37 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -460,7 +460,7 @@ async def test_purge_edge_case( event_type="EVENT_TEST_PURGE", event_data="{}", origin="LOCAL", - time_fired=timestamp, + time_fired_ts=dt_util.utc_to_timestamp(timestamp), ) ) session.add( @@ -468,8 +468,8 @@ async def test_purge_edge_case( entity_id="test.recorder2", state="purgeme", attributes="{}", - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), event_id=1001, attributes_id=1002, ) @@ -529,7 +529,7 @@ async def test_purge_cutoff_date( event_type="KEEP", event_data="{}", origin="LOCAL", - time_fired=timestamp_keep, + time_fired_ts=dt_util.utc_to_timestamp(timestamp_keep), ) ) session.add( @@ -537,8 +537,8 @@ async def test_purge_cutoff_date( entity_id="test.cutoff", state="keep", attributes="{}", - last_changed=timestamp_keep, - last_updated=timestamp_keep, + last_changed_ts=dt_util.utc_to_timestamp(timestamp_keep), + last_updated_ts=dt_util.utc_to_timestamp(timestamp_keep), event_id=1000, attributes_id=1000, ) @@ -557,7 +557,7 @@ async def test_purge_cutoff_date( event_type="PURGE", event_data="{}", origin="LOCAL", - time_fired=timestamp_purge, + time_fired_ts=dt_util.utc_to_timestamp(timestamp_purge), ) ) session.add( @@ -565,8 +565,8 @@ async def test_purge_cutoff_date( entity_id="test.cutoff", state="purge", attributes="{}", - last_changed=timestamp_purge, - last_updated=timestamp_purge, + last_changed_ts=dt_util.utc_to_timestamp(timestamp_purge), + last_updated_ts=dt_util.utc_to_timestamp(timestamp_purge), event_id=1000 + row, attributes_id=1000 + row, ) @@ -690,8 +690,8 @@ async def test_purge_filtered_states( entity_id="sensor.excluded", state="purgeme", attributes="{}", - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), ) ) # Add states and state_changed events that should be keeped @@ -716,8 +716,8 @@ async def test_purge_filtered_states( entity_id="sensor.linked_old_state_id", state="keep", attributes="{}", - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), old_state_id=1, state_attributes=state_attrs, ) @@ -726,8 +726,8 @@ async def test_purge_filtered_states( entity_id="sensor.linked_old_state_id", state="keep", attributes="{}", - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), old_state_id=2, state_attributes=state_attrs, ) @@ -735,8 +735,8 @@ async def test_purge_filtered_states( entity_id="sensor.linked_old_state_id", state="keep", attributes="{}", - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), old_state_id=62, # keep state_attributes=state_attrs, ) @@ -748,7 +748,7 @@ async def test_purge_filtered_states( event_type="EVENT_KEEP", event_data="{}", origin="LOCAL", - time_fired=timestamp, + time_fired_ts=dt_util.utc_to_timestamp(timestamp), ) ) @@ -920,8 +920,8 @@ async def test_purge_without_state_attributes_filtered_states_to_empty( entity_id="sensor.old_format", state=STATE_ON, attributes=json.dumps({"old": "not_using_state_attributes"}), - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), event_id=event_id, state_attributes=None, ) @@ -932,7 +932,7 @@ async def test_purge_without_state_attributes_filtered_states_to_empty( event_type=EVENT_STATE_CHANGED, event_data="{}", origin="LOCAL", - time_fired=timestamp, + time_fired_ts=dt_util.utc_to_timestamp(timestamp), ) ) session.add( @@ -941,7 +941,7 @@ async def test_purge_without_state_attributes_filtered_states_to_empty( event_type=EVENT_THEMES_UPDATED, event_data="{}", origin="LOCAL", - time_fired=timestamp, + time_fired_ts=dt_util.utc_to_timestamp(timestamp), ) ) @@ -993,7 +993,7 @@ async def test_purge_filtered_events( event_type="EVENT_PURGE", event_data="{}", origin="LOCAL", - time_fired=timestamp, + time_fired_ts=dt_util.utc_to_timestamp(timestamp), ) ) @@ -1093,7 +1093,7 @@ async def test_purge_filtered_events_state_changed( event_type="EVENT_KEEP", event_data="{}", origin="LOCAL", - time_fired=timestamp, + time_fired_ts=dt_util.utc_to_timestamp(timestamp), ) ) # Add states with linked old_state_ids that need to be handled @@ -1102,8 +1102,8 @@ async def test_purge_filtered_events_state_changed( entity_id="sensor.linked_old_state_id", state="keep", attributes="{}", - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), old_state_id=1, ) timestamp = dt_util.utcnow() - timedelta(days=4) @@ -1111,16 +1111,16 @@ async def test_purge_filtered_events_state_changed( entity_id="sensor.linked_old_state_id", state="keep", attributes="{}", - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), old_state_id=2, ) state_3 = States( entity_id="sensor.linked_old_state_id", state="keep", attributes="{}", - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), old_state_id=62, # keep ) session.add_all((state_1, state_2, state_3)) @@ -1355,7 +1355,7 @@ async def _add_test_events(hass: HomeAssistant, iterations: int = 1): event_type=event_type, event_data=json.dumps(event_data), origin="LOCAL", - time_fired=timestamp, + time_fired_ts=dt_util.utc_to_timestamp(timestamp), ) ) @@ -1392,7 +1392,7 @@ async def _add_events_with_event_data(hass: HomeAssistant, iterations: int = 1): Events( event_type=event_type, origin="LOCAL", - time_fired=timestamp, + time_fired_ts=dt_util.utc_to_timestamp(timestamp), event_data_rel=event_data, ) ) @@ -1494,8 +1494,8 @@ def _add_state_without_event_linkage( entity_id=entity_id, state=state, attributes=None, - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), event_id=None, state_attributes=state_attrs, ) @@ -1519,8 +1519,8 @@ def _add_state_and_state_changed_event( entity_id=entity_id, state=state, attributes=None, - last_changed=timestamp, - last_updated=timestamp, + last_changed_ts=dt_util.utc_to_timestamp(timestamp), + last_updated_ts=dt_util.utc_to_timestamp(timestamp), event_id=event_id, state_attributes=state_attrs, ) @@ -1531,7 +1531,7 @@ def _add_state_and_state_changed_event( event_type=EVENT_STATE_CHANGED, event_data="{}", origin="LOCAL", - time_fired=timestamp, + time_fired_ts=dt_util.utc_to_timestamp(timestamp), ) ) @@ -1600,8 +1600,8 @@ async def test_purge_can_mix_legacy_and_new_format( broken_state_no_time = States( event_id=None, entity_id="orphened.state", - last_updated=None, - last_changed=None, + last_updated_ts=None, + last_changed_ts=None, ) session.add(broken_state_no_time) start_id = 50000 diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index d3a1c8b7fe0..a131bee9c39 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -1,5 +1,5 @@ """The tests for sensor recorder platform.""" -# pylint: disable=protected-access,invalid-name +# pylint: disable=invalid-name from datetime import datetime, timedelta import importlib import sys @@ -30,7 +30,7 @@ from homeassistant.components.recorder.statistics import ( list_statistic_ids, ) from homeassistant.components.recorder.util import session_scope -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import UnitOfTemperature from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import recorder as recorder_helper @@ -1693,7 +1693,7 @@ def record_states(hass): sns1_attr = { "device_class": "temperature", "state_class": "measurement", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, } sns2_attr = { "device_class": "humidity", diff --git a/tests/components/recorder/test_statistics_v23_migration.py b/tests/components/recorder/test_statistics_v23_migration.py index fe6c95f7318..a1cf1aa73bb 100644 --- a/tests/components/recorder/test_statistics_v23_migration.py +++ b/tests/components/recorder/test_statistics_v23_migration.py @@ -3,7 +3,7 @@ The v23 schema used for these tests has been slightly modified to add the EventData table to allow the recorder to startup successfully. """ -# pylint: disable=protected-access,invalid-name +# pylint: disable=invalid-name import importlib import json import sys diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index ecdd729a163..3f5ba6d40ef 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -14,7 +14,7 @@ from sqlalchemy.sql.lambdas import StatementLambdaElement from homeassistant.components import recorder from homeassistant.components.recorder import history, util -from homeassistant.components.recorder.const import SQLITE_URL_PREFIX +from homeassistant.components.recorder.const import DOMAIN, SQLITE_URL_PREFIX from homeassistant.components.recorder.db_schema import RecorderRuns from homeassistant.components.recorder.models import UnsupportedDialect from homeassistant.components.recorder.util import ( @@ -25,6 +25,7 @@ from homeassistant.components.recorder.util import ( ) from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant +from homeassistant.helpers.issue_registry import async_get as async_get_issue_registry from homeassistant.util import dt as dt_util from .common import corrupt_db_file, run_information_with_session, wait_recording_done @@ -550,6 +551,118 @@ def test_warn_unsupported_dialect(caplog, dialect, message): assert message in caplog.text +@pytest.mark.parametrize( + "mysql_version,min_version", + [ + ( + "10.5.16-MariaDB", + "10.5.17", + ), + ( + "10.6.8-MariaDB", + "10.6.9", + ), + ( + "10.7.1-MariaDB", + "10.7.5", + ), + ( + "10.8.0-MariaDB", + "10.8.4", + ), + ], +) +async def test_issue_for_mariadb_with_MDEV_25020( + hass, caplog, mysql_version, min_version +): + """Test we create an issue for MariaDB versions affected. + + See https://jira.mariadb.org/browse/MDEV-25020. + """ + instance_mock = MagicMock() + instance_mock.hass = hass + execute_args = [] + close_mock = MagicMock() + + def execute_mock(statement): + nonlocal execute_args + execute_args.append(statement) + + def fetchall_mock(): + nonlocal execute_args + if execute_args[-1] == "SELECT VERSION()": + return [[mysql_version]] + return None + + def _make_cursor_mock(*_): + return MagicMock(execute=execute_mock, close=close_mock, fetchall=fetchall_mock) + + dbapi_connection = MagicMock(cursor=_make_cursor_mock) + + await hass.async_add_executor_job( + util.setup_connection_for_dialect, + instance_mock, + "mysql", + dbapi_connection, + True, + ) + await hass.async_block_till_done() + + registry = async_get_issue_registry(hass) + issue = registry.async_get_issue(DOMAIN, "maria_db_range_index_regression") + assert issue is not None + assert issue.translation_placeholders == {"min_version": min_version} + + +@pytest.mark.parametrize( + "mysql_version", + [ + "10.5.17-MariaDB", + "10.6.9-MariaDB", + "10.7.5-MariaDB", + "10.8.4-MariaDB", + "10.9.1-MariaDB", + ], +) +async def test_no_issue_for_mariadb_with_MDEV_25020(hass, caplog, mysql_version): + """Test we do not create an issue for MariaDB versions not affected. + + See https://jira.mariadb.org/browse/MDEV-25020. + """ + instance_mock = MagicMock() + instance_mock.hass = hass + execute_args = [] + close_mock = MagicMock() + + def execute_mock(statement): + nonlocal execute_args + execute_args.append(statement) + + def fetchall_mock(): + nonlocal execute_args + if execute_args[-1] == "SELECT VERSION()": + return [[mysql_version]] + return None + + def _make_cursor_mock(*_): + return MagicMock(execute=execute_mock, close=close_mock, fetchall=fetchall_mock) + + dbapi_connection = MagicMock(cursor=_make_cursor_mock) + + await hass.async_add_executor_job( + util.setup_connection_for_dialect, + instance_mock, + "mysql", + dbapi_connection, + True, + ) + await hass.async_block_till_done() + + registry = async_get_issue_registry(hass) + issue = registry.async_get_issue(DOMAIN, "maria_db_range_index_regression") + assert issue is None + + def test_basic_sanity_check(hass_recorder, recorder_db_url): """Test the basic sanity checks with a missing table.""" if recorder_db_url.startswith("mysql://"): diff --git a/tests/components/recorder/test_v32_migration.py b/tests/components/recorder/test_v32_migration.py new file mode 100644 index 00000000000..1585a3733c5 --- /dev/null +++ b/tests/components/recorder/test_v32_migration.py @@ -0,0 +1,135 @@ +"""The tests for recorder platform migrating data from v30.""" +# pylint: disable=invalid-name +from datetime import timedelta +import importlib +import sys +from unittest.mock import patch + +from sqlalchemy import create_engine +from sqlalchemy.orm import Session + +from homeassistant.components import recorder +from homeassistant.components.recorder import SQLITE_URL_PREFIX, core, statistics +from homeassistant.components.recorder.util import session_scope +from homeassistant.core import EVENT_STATE_CHANGED, Event, EventOrigin, State +from homeassistant.helpers import recorder as recorder_helper +from homeassistant.setup import setup_component +import homeassistant.util.dt as dt_util + +from .common import wait_recording_done + +from tests.common import get_test_home_assistant + +ORIG_TZ = dt_util.DEFAULT_TIME_ZONE + +CREATE_ENGINE_TARGET = "homeassistant.components.recorder.core.create_engine" +SCHEMA_MODULE = "tests.components.recorder.db_schema_30" + + +def _create_engine_test(*args, **kwargs): + """Test version of create_engine that initializes with old schema. + + This simulates an existing db with the old schema. + """ + importlib.import_module(SCHEMA_MODULE) + old_db_schema = sys.modules[SCHEMA_MODULE] + engine = create_engine(*args, **kwargs) + old_db_schema.Base.metadata.create_all(engine) + with Session(engine) as session: + session.add( + recorder.db_schema.StatisticsRuns(start=statistics.get_start_time()) + ) + session.add( + recorder.db_schema.SchemaChanges( + schema_version=old_db_schema.SCHEMA_VERSION + ) + ) + session.commit() + return engine + + +def test_migrate_times(caplog, tmpdir): + """Test we can migrate times.""" + test_db_file = tmpdir.mkdir("sqlite").join("test_run_info.db") + dburl = f"{SQLITE_URL_PREFIX}//{test_db_file}" + + importlib.import_module(SCHEMA_MODULE) + old_db_schema = sys.modules[SCHEMA_MODULE] + now = dt_util.utcnow() + one_second_past = now - timedelta(seconds=1) + now_timestamp = now.timestamp() + one_second_past_timestamp = one_second_past.timestamp() + + mock_state = State( + "sensor.test", + "old", + {"last_reset": now.isoformat()}, + last_changed=one_second_past, + last_updated=now, + ) + state_changed_event = Event( + EVENT_STATE_CHANGED, + { + "entity_id": "sensor.test", + "old_state": None, + "new_state": mock_state, + }, + EventOrigin.local, + time_fired=now, + ) + custom_event = Event( + "custom_event", + {"entity_id": "sensor.custom"}, + EventOrigin.local, + time_fired=now, + ) + + with patch.object(recorder, "db_schema", old_db_schema), patch.object( + recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION + ), patch.object(core, "EventData", old_db_schema.EventData), patch.object( + core, "States", old_db_schema.States + ), patch.object( + core, "Events", old_db_schema.Events + ), patch( + CREATE_ENGINE_TARGET, new=_create_engine_test + ): + hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + wait_recording_done(hass) + wait_recording_done(hass) + + with session_scope(hass=hass) as session: + session.add(old_db_schema.Events.from_event(custom_event)) + session.add(old_db_schema.States.from_event(state_changed_event)) + + hass.stop() + + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ + + # Test that the duplicates are removed during migration from schema 23 + hass = get_test_home_assistant() + recorder_helper.async_initialize_recorder(hass) + setup_component(hass, "recorder", {"recorder": {"db_url": dburl}}) + hass.start() + wait_recording_done(hass) + wait_recording_done(hass) + with session_scope(hass=hass) as session: + result = list( + session.query(recorder.db_schema.Events).where( + recorder.db_schema.Events.event_type == "custom_event" + ) + ) + assert len(result) == 1 + assert result[0].time_fired_ts == now_timestamp + result = list( + session.query(recorder.db_schema.States).where( + recorder.db_schema.States.entity_id == "sensor.test" + ) + ) + assert len(result) == 1 + assert result[0].last_changed_ts == one_second_past_timestamp + assert result[0].last_updated_ts == now_timestamp + + hass.stop() + dt_util.DEFAULT_TIME_ZONE = ORIG_TZ diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index fefc8dbdda1..935594d5a5c 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -1,5 +1,5 @@ """The tests for sensor recorder platform.""" -# pylint: disable=protected-access,invalid-name +# pylint: disable=invalid-name import datetime from datetime import timedelta from statistics import fmean @@ -1690,7 +1690,7 @@ async def test_clear_statistics(recorder_mock, hass, hass_ws_client): @pytest.mark.parametrize( "new_unit, new_unit_class, new_display_unit", - [("dogs", None, "dogs"), (None, None, None), ("W", "power", "kW")], + [("dogs", None, "dogs"), (None, "unitless", None), ("W", "power", "kW")], ) async def test_update_statistics_metadata( recorder_mock, hass, hass_ws_client, new_unit, new_unit_class, new_display_unit @@ -2033,7 +2033,7 @@ async def test_recorder_info(recorder_mock, hass, hass_ws_client): assert response["success"] assert response["result"] == { "backlog": 0, - "max_backlog": 40000, + "max_backlog": 65000, "migration_in_progress": False, "migration_is_live": False, "recording": True, @@ -2986,7 +2986,7 @@ async def test_adjust_sum_statistics_gas( ("m³", "m³", "volume", 1, ("ft³", "m³"), ("Wh", "kWh", "MWh", "cats", None)), ("ft³", "ft³", "volume", 1, ("ft³", "m³"), ("Wh", "kWh", "MWh", "cats", None)), ("dogs", "dogs", None, 1, ("dogs",), ("cats", None)), - (None, None, None, 1, (None,), ("cats",)), + (None, None, "unitless", 1, (None,), ("cats",)), ), ) async def test_adjust_sum_statistics_errors( diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index 96cdeed2493..5db47c5d589 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -24,18 +24,18 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_PASSWORD, CONF_USERNAME, - ENERGY_KILO_WATT_HOUR, - LENGTH_KILOMETERS, PERCENTAGE, - POWER_KILO_WATT, STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_UNKNOWN, - TEMP_CELSIUS, - TIME_MINUTES, - VOLUME_LITERS, Platform, + UnitOfEnergy, + UnitOfLength, + UnitOfPower, + UnitOfTemperature, + UnitOfTime, + UnitOfVolume, ) ATTR_DEFAULT_DISABLED = "default_disabled" @@ -133,7 +133,7 @@ MOCK_VEHICLES = { ATTR_STATE: "141", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_autonomy", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, @@ -141,7 +141,7 @@ MOCK_VEHICLES = { ATTR_STATE: "31", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_available_energy", - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY, @@ -164,7 +164,7 @@ MOCK_VEHICLES = { ATTR_STATE: "20", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM, @@ -189,7 +189,7 @@ MOCK_VEHICLES = { ATTR_STATE: "0.027", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging_power", - ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT, }, { ATTR_ENTITY_ID: "sensor.reg_number_charging_remaining_time", @@ -197,7 +197,7 @@ MOCK_VEHICLES = { ATTR_STATE: "145", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging_remaining_time", - ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTime.MINUTES, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.DISTANCE, @@ -206,7 +206,7 @@ MOCK_VEHICLES = { ATTR_STATE: "49114", ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, ATTR_UNIQUE_ID: "vf1aaaaa555777999_mileage", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, @@ -214,7 +214,7 @@ MOCK_VEHICLES = { ATTR_STATE: "8.0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_outside_temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_ENTITY_ID: "sensor.reg_number_hvac_soc_threshold", @@ -362,7 +362,7 @@ MOCK_VEHICLES = { ATTR_STATE: "128", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_autonomy", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, @@ -370,7 +370,7 @@ MOCK_VEHICLES = { ATTR_STATE: "0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_available_energy", - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY, @@ -393,7 +393,7 @@ MOCK_VEHICLES = { ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM, @@ -418,7 +418,7 @@ MOCK_VEHICLES = { ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging_power", - ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT, }, { ATTR_ENTITY_ID: "sensor.reg_number_charging_remaining_time", @@ -426,7 +426,7 @@ MOCK_VEHICLES = { ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_charging_remaining_time", - ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTime.MINUTES, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.DISTANCE, @@ -435,7 +435,7 @@ MOCK_VEHICLES = { ATTR_STATE: "49114", ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, ATTR_UNIQUE_ID: "vf1aaaaa555777999_mileage", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, @@ -443,7 +443,7 @@ MOCK_VEHICLES = { ATTR_STATE: STATE_UNKNOWN, ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777999_outside_temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_ENTITY_ID: "sensor.reg_number_hvac_soc_threshold", @@ -591,7 +591,7 @@ MOCK_VEHICLES = { ATTR_STATE: "141", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_autonomy", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, @@ -599,7 +599,7 @@ MOCK_VEHICLES = { ATTR_STATE: "31", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_available_energy", - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.BATTERY, @@ -622,7 +622,7 @@ MOCK_VEHICLES = { ATTR_STATE: "20", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM, @@ -647,7 +647,7 @@ MOCK_VEHICLES = { ATTR_STATE: "27.0", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_charging_power", - ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfPower.KILO_WATT, }, { ATTR_ENTITY_ID: "sensor.reg_number_charging_remaining_time", @@ -655,7 +655,7 @@ MOCK_VEHICLES = { ATTR_STATE: "145", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_charging_remaining_time", - ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTime.MINUTES, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.DISTANCE, @@ -664,7 +664,7 @@ MOCK_VEHICLES = { ATTR_STATE: "35", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_fuel_autonomy", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.VOLUME, @@ -673,7 +673,7 @@ MOCK_VEHICLES = { ATTR_STATE: "3", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_fuel_quantity", - ATTR_UNIT_OF_MEASUREMENT: VOLUME_LITERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfVolume.LITERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.DISTANCE, @@ -682,7 +682,7 @@ MOCK_VEHICLES = { ATTR_STATE: "5567", ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, ATTR_UNIQUE_ID: "vf1aaaaa555777123_mileage", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM, @@ -789,7 +789,7 @@ MOCK_VEHICLES = { ATTR_STATE: "35", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_fuel_autonomy", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.VOLUME, @@ -798,7 +798,7 @@ MOCK_VEHICLES = { ATTR_STATE: "3", ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_UNIQUE_ID: "vf1aaaaa555777123_fuel_quantity", - ATTR_UNIT_OF_MEASUREMENT: VOLUME_LITERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfVolume.LITERS, }, { ATTR_DEVICE_CLASS: SensorDeviceClass.DISTANCE, @@ -807,7 +807,7 @@ MOCK_VEHICLES = { ATTR_STATE: "5567", ATTR_STATE_CLASS: SensorStateClass.TOTAL_INCREASING, ATTR_UNIQUE_ID: "vf1aaaaa555777123_mileage", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, }, { ATTR_DEFAULT_DISABLED: True, diff --git a/tests/components/renault/test_binary_sensor.py b/tests/components/renault/test_binary_sensor.py index feef06746bd..c672d3fc06f 100644 --- a/tests/components/renault/test_binary_sensor.py +++ b/tests/components/renault/test_binary_sensor.py @@ -1,4 +1,5 @@ """Tests for Renault binary sensors.""" +from collections.abc import Generator from unittest.mock import patch import pytest @@ -21,7 +22,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", [Platform.BINARY_SENSOR]): yield @@ -30,7 +31,7 @@ def override_platforms(): @pytest.mark.usefixtures("fixtures_with_data") async def test_binary_sensors( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault binary sensors.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -50,7 +51,7 @@ async def test_binary_sensors( @pytest.mark.usefixtures("fixtures_with_no_data") async def test_binary_sensor_empty( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault binary sensors with empty data from Renault.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -69,7 +70,7 @@ async def test_binary_sensor_empty( @pytest.mark.usefixtures("fixtures_with_invalid_upstream_exception") async def test_binary_sensor_errors( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault binary sensors with temporary failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -90,7 +91,7 @@ async def test_binary_sensor_errors( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_binary_sensor_access_denied( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault binary sensors with access denied failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -108,7 +109,7 @@ async def test_binary_sensor_access_denied( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_binary_sensor_not_supported( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault binary sensors with not supported failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) diff --git a/tests/components/renault/test_button.py b/tests/components/renault/test_button.py index a7fdcb356cc..040c23a0836 100644 --- a/tests/components/renault/test_button.py +++ b/tests/components/renault/test_button.py @@ -1,4 +1,5 @@ """Tests for Renault sensors.""" +from collections.abc import Generator from unittest.mock import patch import pytest @@ -18,7 +19,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", [Platform.BUTTON]): yield @@ -27,7 +28,7 @@ def override_platforms(): @pytest.mark.usefixtures("fixtures_with_data") async def test_buttons( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers.""" entity_registry = mock_registry(hass) @@ -48,7 +49,7 @@ async def test_buttons( @pytest.mark.usefixtures("fixtures_with_no_data") async def test_button_empty( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers with empty data from Renault.""" entity_registry = mock_registry(hass) @@ -68,7 +69,7 @@ async def test_button_empty( @pytest.mark.usefixtures("fixtures_with_invalid_upstream_exception") async def test_button_errors( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers with temporary failure.""" entity_registry = mock_registry(hass) @@ -90,7 +91,7 @@ async def test_button_errors( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_button_access_denied( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers with access denied failure.""" entity_registry = mock_registry(hass) @@ -112,7 +113,7 @@ async def test_button_access_denied( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_button_not_supported( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers with not supported failure.""" entity_registry = mock_registry(hass) @@ -132,7 +133,9 @@ async def test_button_not_supported( @pytest.mark.usefixtures("fixtures_with_data") @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) -async def test_button_start_charge(hass: HomeAssistant, config_entry: ConfigEntry): +async def test_button_start_charge( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Test that button invokes renault_api with correct data.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -160,7 +163,7 @@ async def test_button_start_charge(hass: HomeAssistant, config_entry: ConfigEntr @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_button_start_air_conditioner( hass: HomeAssistant, config_entry: ConfigEntry -): +) -> None: """Test that button invokes renault_api with correct data.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/renault/test_device_tracker.py b/tests/components/renault/test_device_tracker.py index 6d1ace17754..0077b8d811d 100644 --- a/tests/components/renault/test_device_tracker.py +++ b/tests/components/renault/test_device_tracker.py @@ -1,4 +1,5 @@ """Tests for Renault sensors.""" +from collections.abc import Generator from unittest.mock import patch import pytest @@ -21,7 +22,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", [Platform.DEVICE_TRACKER]): yield @@ -30,7 +31,7 @@ def override_platforms(): @pytest.mark.usefixtures("fixtures_with_data") async def test_device_trackers( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers.""" entity_registry = mock_registry(hass) @@ -51,7 +52,7 @@ async def test_device_trackers( @pytest.mark.usefixtures("fixtures_with_no_data") async def test_device_tracker_empty( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers with empty data from Renault.""" entity_registry = mock_registry(hass) @@ -71,7 +72,7 @@ async def test_device_tracker_empty( @pytest.mark.usefixtures("fixtures_with_invalid_upstream_exception") async def test_device_tracker_errors( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers with temporary failure.""" entity_registry = mock_registry(hass) @@ -93,7 +94,7 @@ async def test_device_tracker_errors( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_device_tracker_access_denied( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers with access denied failure.""" entity_registry = mock_registry(hass) @@ -112,7 +113,7 @@ async def test_device_tracker_access_denied( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_device_tracker_not_supported( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault device trackers with not supported failure.""" entity_registry = mock_registry(hass) diff --git a/tests/components/renault/test_init.py b/tests/components/renault/test_init.py index 58da09ccd0b..8b4ed379db5 100644 --- a/tests/components/renault/test_init.py +++ b/tests/components/renault/test_init.py @@ -1,4 +1,5 @@ """Tests for Renault setup process.""" +from collections.abc import Generator from unittest.mock import patch import aiohttp @@ -11,7 +12,7 @@ from homeassistant.core import HomeAssistant @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", []): yield @@ -24,23 +25,25 @@ def override_vehicle_type(request) -> str: @pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicles") -async def test_setup_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): +async def test_setup_unload_entry( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Test entry setup and unload.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert config_entry.state is ConfigEntryState.LOADED - assert config_entry.entry_id in hass.data[DOMAIN] # Unload the entry and verify that the data has been removed await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.NOT_LOADED - assert config_entry.entry_id not in hass.data[DOMAIN] -async def test_setup_entry_bad_password(hass: HomeAssistant, config_entry: ConfigEntry): +async def test_setup_entry_bad_password( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Test entry setup and unload.""" # Create a mock entry so we don't have to go through config flow with patch( @@ -55,7 +58,9 @@ async def test_setup_entry_bad_password(hass: HomeAssistant, config_entry: Confi assert not hass.data.get(DOMAIN) -async def test_setup_entry_exception(hass: HomeAssistant, config_entry: ConfigEntry): +async def test_setup_entry_exception( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Test ConfigEntryNotReady when API raises an exception during entry setup.""" # In this case we are testing the condition where async_setup_entry raises # ConfigEntryNotReady. diff --git a/tests/components/renault/test_select.py b/tests/components/renault/test_select.py index 8ab9f116dba..228214fe4c0 100644 --- a/tests/components/renault/test_select.py +++ b/tests/components/renault/test_select.py @@ -1,4 +1,5 @@ """Tests for Renault selects.""" +from collections.abc import Generator from unittest.mock import patch import pytest @@ -27,7 +28,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", [Platform.SELECT]): yield @@ -36,7 +37,7 @@ def override_platforms(): @pytest.mark.usefixtures("fixtures_with_data") async def test_selects( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault selects.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -56,7 +57,7 @@ async def test_selects( @pytest.mark.usefixtures("fixtures_with_no_data") async def test_select_empty( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault selects with empty data from Renault.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -75,7 +76,7 @@ async def test_select_empty( @pytest.mark.usefixtures("fixtures_with_invalid_upstream_exception") async def test_select_errors( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault selects with temporary failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -96,7 +97,7 @@ async def test_select_errors( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_select_access_denied( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault selects with access denied failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -114,7 +115,7 @@ async def test_select_access_denied( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_select_not_supported( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault selects with access denied failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -130,7 +131,9 @@ async def test_select_not_supported( @pytest.mark.usefixtures("fixtures_with_data") @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) -async def test_select_charge_mode(hass: HomeAssistant, config_entry: ConfigEntry): +async def test_select_charge_mode( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Test that service invokes renault_api with correct data.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/renault/test_sensor.py b/tests/components/renault/test_sensor.py index c04bf8c0280..5e81791fdfa 100644 --- a/tests/components/renault/test_sensor.py +++ b/tests/components/renault/test_sensor.py @@ -1,4 +1,5 @@ """Tests for Renault sensors.""" +from collections.abc import Generator from types import MappingProxyType from unittest.mock import patch @@ -23,7 +24,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", [Platform.SENSOR]): yield @@ -45,7 +46,7 @@ def _check_and_enable_disabled_entities( @pytest.mark.usefixtures("fixtures_with_data") async def test_sensors( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault sensors.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -69,7 +70,7 @@ async def test_sensors( @pytest.mark.usefixtures("fixtures_with_no_data") async def test_sensor_empty( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault sensors with empty data from Renault.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -93,7 +94,7 @@ async def test_sensor_empty( @pytest.mark.usefixtures("fixtures_with_invalid_upstream_exception") async def test_sensor_errors( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault sensors with temporary failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -118,7 +119,7 @@ async def test_sensor_errors( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_sensor_access_denied( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault sensors with access denied failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) @@ -136,7 +137,7 @@ async def test_sensor_access_denied( @pytest.mark.parametrize("vehicle_type", ["zoe_40"], indirect=True) async def test_sensor_not_supported( hass: HomeAssistant, config_entry: ConfigEntry, vehicle_type: str -): +) -> None: """Test for Renault sensors with access denied failure.""" entity_registry = mock_registry(hass) device_registry = mock_device_registry(hass) diff --git a/tests/components/renault/test_services.py b/tests/components/renault/test_services.py index b45273c581b..d2c82a23d48 100644 --- a/tests/components/renault/test_services.py +++ b/tests/components/renault/test_services.py @@ -1,8 +1,10 @@ """Tests for Renault sensors.""" +from collections.abc import Generator from datetime import datetime from unittest.mock import patch import pytest +from renault_api.exceptions import RenaultException from renault_api.kamereon import schemas from renault_api.kamereon.models import ChargeSchedule @@ -15,7 +17,6 @@ from homeassistant.components.renault.services import ( SERVICE_AC_CANCEL, SERVICE_AC_START, SERVICE_CHARGE_SET_SCHEDULES, - SERVICE_CHARGE_START, SERVICES, ) from homeassistant.config_entries import ConfigEntry @@ -27,6 +28,7 @@ from homeassistant.const import ( ATTR_SW_VERSION, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from .const import MOCK_VEHICLES @@ -37,7 +39,7 @@ pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicle @pytest.fixture(autouse=True) -def override_platforms(): +def override_platforms() -> Generator[None, None, None]: """Override PLATFORMS.""" with patch("homeassistant.components.renault.PLATFORMS", []): yield @@ -57,7 +59,9 @@ def get_device_id(hass: HomeAssistant) -> str: return device.id -async def test_service_registration(hass: HomeAssistant, config_entry: ConfigEntry): +async def test_service_registration( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Test entry setup and unload.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -74,7 +78,9 @@ async def test_service_registration(hass: HomeAssistant, config_entry: ConfigEnt assert not hass.services.has_service(DOMAIN, service) -async def test_service_set_ac_cancel(hass: HomeAssistant, config_entry: ConfigEntry): +async def test_service_set_ac_cancel( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: """Test that service invokes renault_api with correct data.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -85,22 +91,19 @@ async def test_service_set_ac_cancel(hass: HomeAssistant, config_entry: ConfigEn with patch( "renault_api.renault_vehicle.RenaultVehicle.set_ac_stop", - return_value=( - schemas.KamereonVehicleHvacStartActionDataSchema.loads( - load_fixture("renault/action.set_ac_stop.json") - ) - ), + side_effect=RenaultException("Didn't work"), ) as mock_action: - await hass.services.async_call( - DOMAIN, SERVICE_AC_CANCEL, service_data=data, blocking=True - ) + with pytest.raises(HomeAssistantError, match="Didn't work"): + await hass.services.async_call( + DOMAIN, SERVICE_AC_CANCEL, service_data=data, blocking=True + ) assert len(mock_action.mock_calls) == 1 assert mock_action.mock_calls[0][1] == () async def test_service_set_ac_start_simple( hass: HomeAssistant, config_entry: ConfigEntry -): +) -> None: """Test that service invokes renault_api with correct data.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -128,7 +131,7 @@ async def test_service_set_ac_start_simple( async def test_service_set_ac_start_with_date( hass: HomeAssistant, config_entry: ConfigEntry -): +) -> None: """Test that service invokes renault_api with correct data.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -158,7 +161,7 @@ async def test_service_set_ac_start_with_date( async def test_service_set_charge_schedule( hass: HomeAssistant, config_entry: ConfigEntry -): +) -> None: """Test that service invokes renault_api with correct data.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -192,7 +195,7 @@ async def test_service_set_charge_schedule( async def test_service_set_charge_schedule_multi( hass: HomeAssistant, config_entry: ConfigEntry -): +) -> None: """Test that service invokes renault_api with correct data.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -237,36 +240,9 @@ async def test_service_set_charge_schedule_multi( assert mock_action.mock_calls[0][1] == (mock_call_data,) -async def test_service_set_charge_start( - hass: HomeAssistant, config_entry: ConfigEntry, caplog: pytest.LogCaptureFixture -): - """Test that service invokes renault_api with correct data.""" - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - data = { - ATTR_VEHICLE: get_device_id(hass), - } - - with patch( - "renault_api.renault_vehicle.RenaultVehicle.set_charge_start", - return_value=( - schemas.KamereonVehicleHvacStartActionDataSchema.loads( - load_fixture("renault/action.set_charge_start.json") - ) - ), - ) as mock_action: - await hass.services.async_call( - DOMAIN, SERVICE_CHARGE_START, service_data=data, blocking=True - ) - assert len(mock_action.mock_calls) == 1 - assert mock_action.mock_calls[0][1] == () - assert f"'{DOMAIN}.{SERVICE_CHARGE_START}' service is deprecated" in caplog.text - - async def test_service_invalid_device_id( hass: HomeAssistant, config_entry: ConfigEntry -): +) -> None: """Test that service fails with ValueError if device_id not found in registry.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -281,7 +257,7 @@ async def test_service_invalid_device_id( async def test_service_invalid_device_id2( hass: HomeAssistant, config_entry: ConfigEntry -): +) -> None: """Test that service fails with ValueError if device_id not found in vehicles.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/reolink/test_config_flow.py b/tests/components/reolink/test_config_flow.py index fc6672718b9..36c1862eaea 100644 --- a/tests/components/reolink/test_config_flow.py +++ b/tests/components/reolink/test_config_flow.py @@ -3,10 +3,12 @@ import json from unittest.mock import AsyncMock, Mock, patch import pytest -from reolink_aio.exceptions import ApiError, CredentialsInvalidError +from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError from homeassistant import config_entries, data_entry_flow +from homeassistant.components import dhcp from homeassistant.components.reolink import const +from homeassistant.components.reolink.config_flow import DEFAULT_PROTOCOL from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.helpers.device_registry import format_mac @@ -24,14 +26,14 @@ TEST_NVR_NAME = "test_reolink_name" TEST_USE_HTTPS = True -def get_mock_info(error=None, host_data_return=True, user_level="admin"): +def get_mock_info(error=None, user_level="admin"): """Return a mock gateway info instance.""" host_mock = Mock() if error is None: - host_mock.get_host_data = AsyncMock(return_value=host_data_return) + host_mock.get_host_data = AsyncMock(return_value=None) else: host_mock.get_host_data = AsyncMock(side_effect=error) - host_mock.unsubscribe_all = AsyncMock(return_value=True) + host_mock.unsubscribe = AsyncMock(return_value=True) host_mock.logout = AsyncMock(return_value=True) host_mock.mac_address = TEST_MAC host_mock.onvif_enabled = True @@ -85,7 +87,7 @@ async def test_config_flow_manual_success(hass): const.CONF_USE_HTTPS: TEST_USE_HTTPS, } assert result["options"] == { - const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL, + const.CONF_PROTOCOL: DEFAULT_PROTOCOL, } @@ -99,7 +101,7 @@ async def test_config_flow_errors(hass): assert result["step_id"] == "user" assert result["errors"] == {} - host_mock = get_mock_info(host_data_return=False) + host_mock = get_mock_info(error=ReolinkError("Test error")) with patch("homeassistant.components.reolink.host.Host", return_value=host_mock): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -195,7 +197,7 @@ async def test_config_flow_errors(hass): const.CONF_USE_HTTPS: TEST_USE_HTTPS, } assert result["options"] == { - const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL, + const.CONF_PROTOCOL: DEFAULT_PROTOCOL, } @@ -250,7 +252,7 @@ async def test_change_connection_settings(hass): const.CONF_USE_HTTPS: TEST_USE_HTTPS, }, options={ - const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL, + const.CONF_PROTOCOL: DEFAULT_PROTOCOL, }, title=TEST_NVR_NAME, ) @@ -293,7 +295,7 @@ async def test_reauth(hass): const.CONF_USE_HTTPS: TEST_USE_HTTPS, }, options={ - const.CONF_PROTOCOL: const.DEFAULT_PROTOCOL, + const.CONF_PROTOCOL: DEFAULT_PROTOCOL, }, title=TEST_NVR_NAME, ) @@ -313,7 +315,7 @@ async def test_reauth(hass): data=config_entry.data, ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "reauth_confirm" result = await hass.config_entries.flow.async_configure( @@ -321,21 +323,94 @@ async def test_reauth(hass): {}, ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} result = await hass.config_entries.flow.async_configure( result["flow_id"], { - CONF_HOST: TEST_HOST2, CONF_USERNAME: TEST_USERNAME2, CONF_PASSWORD: TEST_PASSWORD2, }, ) - assert result["type"] == "abort" + assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" - assert config_entry.data[CONF_HOST] == TEST_HOST2 + assert config_entry.data[CONF_HOST] == TEST_HOST assert config_entry.data[CONF_USERNAME] == TEST_USERNAME2 assert config_entry.data[CONF_PASSWORD] == TEST_PASSWORD2 + + +async def test_dhcp_flow(hass): + """Successful flow from DHCP discovery.""" + dhcp_data = dhcp.DhcpServiceInfo( + ip=TEST_HOST, + hostname="Reolink", + macaddress=TEST_MAC, + ) + + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + + assert result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_NVR_NAME + assert result["data"] == { + CONF_HOST: TEST_HOST, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_PORT: TEST_PORT, + const.CONF_USE_HTTPS: TEST_USE_HTTPS, + } + assert result["options"] == { + const.CONF_PROTOCOL: DEFAULT_PROTOCOL, + } + + +async def test_dhcp_abort_flow(hass): + """Test dhcp discovery aborts if already configured.""" + config_entry = MockConfigEntry( + domain=const.DOMAIN, + unique_id=format_mac(TEST_MAC), + data={ + CONF_HOST: TEST_HOST, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_PORT: TEST_PORT, + const.CONF_USE_HTTPS: TEST_USE_HTTPS, + }, + options={ + const.CONF_PROTOCOL: DEFAULT_PROTOCOL, + }, + title=TEST_NVR_NAME, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + dhcp_data = dhcp.DhcpServiceInfo( + ip=TEST_HOST, + hostname="Reolink", + macaddress=TEST_MAC, + ) + + result = await hass.config_entries.flow.async_init( + const.DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=dhcp_data + ) + + assert result["type"] is data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/repairs/test_init.py b/tests/components/repairs/test_init.py index 513eb4071bc..6d4ec300338 100644 --- a/tests/components/repairs/test_init.py +++ b/tests/components/repairs/test_init.py @@ -459,7 +459,7 @@ async def test_non_compliant_platform(hass: HomeAssistant, hass_ws_client) -> No """Test non-compliant platforms are not registered.""" hass.config.components.add("fake_integration") - hass.config.components.add("integration_without_diagnostics") + hass.config.components.add("integration_without_repairs") mock_platform( hass, "fake_integration.repairs", @@ -467,7 +467,7 @@ async def test_non_compliant_platform(hass: HomeAssistant, hass_ws_client) -> No ) mock_platform( hass, - "integration_without_diagnostics.repairs", + "integration_without_repairs.repairs", Mock(spec=[]), ) assert await async_setup_component(hass, DOMAIN, {}) diff --git a/tests/components/repairs/test_websocket_api.py b/tests/components/repairs/test_websocket_api.py index acbd7b879ab..206cedbe6a5 100644 --- a/tests/components/repairs/test_websocket_api.py +++ b/tests/components/repairs/test_websocket_api.py @@ -99,7 +99,7 @@ class MockFixFlow(RepairsFlow): ) -> data_entry_flow.FlowResult: """Handle a custom_step step of a fix flow.""" if user_input is not None: - return self.async_create_entry(title="", data={}) + return self.async_create_entry(data={}) return self.async_show_form(step_id="custom_step", data_schema=vol.Schema({})) @@ -334,7 +334,6 @@ async def test_fix_issue( "description_placeholders": None, "flow_id": flow_id, "handler": domain, - "title": "", "type": "create_entry", "version": 1, } diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index fbee3c2051a..79d59be3f44 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -11,9 +11,9 @@ from homeassistant import config as hass_config from homeassistant.components.rest.const import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, - DATA_MEGABYTES, SERVICE_RELOAD, STATE_UNAVAILABLE, + UnitOfInformation, ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -44,12 +44,12 @@ async def test_setup_with_endpoint_timeout_with_recovery(hass: HomeAssistant) -> "timeout": 30, "sensor": [ { - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "name": "sensor1", "value_template": "{{ value_json.sensor1 }}", }, { - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "name": "sensor2", "value_template": "{{ value_json.sensor2 }}", }, @@ -158,12 +158,12 @@ async def test_setup_minimum_resource_template(hass: HomeAssistant) -> None: "timeout": 30, "sensor": [ { - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "name": "sensor1", "value_template": "{{ value_json.sensor1 }}", }, { - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "name": "sensor2", "value_template": "{{ value_json.sensor2 }}", }, diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index db55df4ff16..5dcaed6985d 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -20,10 +20,10 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONTENT_TYPE_JSON, - DATA_MEGABYTES, SERVICE_RELOAD, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfInformation, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -182,7 +182,9 @@ async def test_setup_duplicate_resource_template(hass: HomeAssistant) -> None: @respx.mock async def test_setup_get(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" - respx.get("http://localhost").respond(status_code=HTTPStatus.OK, json={}) + respx.get("http://localhost").respond( + status_code=HTTPStatus.OK, json={"key": "123"} + ) assert await async_setup_component( hass, "sensor", @@ -193,7 +195,7 @@ async def test_setup_get(hass: HomeAssistant) -> None: "method": "GET", "value_template": "{{ value_json.key }}", "name": "foo", - "unit_of_measurement": TEMP_CELSIUS, + "unit_of_measurement": UnitOfTemperature.CELSIUS, "verify_ssl": "true", "timeout": 30, "authentication": "basic", @@ -210,7 +212,7 @@ async def test_setup_get(hass: HomeAssistant) -> None: await hass.async_block_till_done() assert len(hass.states.async_all("sensor")) == 1 - assert hass.states.get("sensor.foo").state == "" + assert hass.states.get("sensor.foo").state == "123" await hass.services.async_call( "homeassistant", SERVICE_UPDATE_ENTITY, @@ -219,8 +221,8 @@ async def test_setup_get(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() state = hass.states.get("sensor.foo") - assert state.state == "" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.state == "123" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE assert state.attributes[ATTR_STATE_CLASS] is SensorStateClass.MEASUREMENT @@ -325,7 +327,9 @@ async def test_setup_get_templated_headers_params(hass: HomeAssistant) -> None: @respx.mock async def test_setup_get_digest_auth(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" - respx.get("http://localhost").respond(status_code=HTTPStatus.OK, json={}) + respx.get("http://localhost").respond( + status_code=HTTPStatus.OK, json={"key": "123"} + ) assert await async_setup_component( hass, "sensor", @@ -336,7 +340,7 @@ async def test_setup_get_digest_auth(hass: HomeAssistant) -> None: "method": "GET", "value_template": "{{ value_json.key }}", "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, "authentication": "digest", @@ -354,7 +358,9 @@ async def test_setup_get_digest_auth(hass: HomeAssistant) -> None: @respx.mock async def test_setup_post(hass: HomeAssistant) -> None: """Test setup with valid configuration.""" - respx.post("http://localhost").respond(status_code=HTTPStatus.OK, json={}) + respx.post("http://localhost").respond( + status_code=HTTPStatus.OK, json={"key": "123"} + ) assert await async_setup_component( hass, "sensor", @@ -366,7 +372,7 @@ async def test_setup_post(hass: HomeAssistant) -> None: "value_template": "{{ value_json.key }}", "payload": '{ "device": "toaster"}', "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, "authentication": "basic", @@ -386,7 +392,7 @@ async def test_setup_get_xml(hass: HomeAssistant) -> None: respx.get("http://localhost").respond( status_code=HTTPStatus.OK, headers={"content-type": "text/xml"}, - content="abc", + content="123", ) assert await async_setup_component( hass, @@ -398,7 +404,7 @@ async def test_setup_get_xml(hass: HomeAssistant) -> None: "method": "GET", "value_template": "{{ value_json.dog }}", "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, } @@ -408,8 +414,8 @@ async def test_setup_get_xml(hass: HomeAssistant) -> None: assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") - assert state.state == "abc" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == DATA_MEGABYTES + assert state.state == "123" + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfInformation.MEGABYTES @respx.mock @@ -438,7 +444,7 @@ async def test_update_with_json_attrs(hass: HomeAssistant) -> None: respx.get("http://localhost").respond( status_code=HTTPStatus.OK, - json={"key": "some_json_value"}, + json={"key": "123", "other_key": "some_json_value"}, ) assert await async_setup_component( hass, @@ -449,9 +455,9 @@ async def test_update_with_json_attrs(hass: HomeAssistant) -> None: "resource": "http://localhost", "method": "GET", "value_template": "{{ value_json.key }}", - "json_attributes": ["key"], + "json_attributes": ["other_key"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, } @@ -461,8 +467,8 @@ async def test_update_with_json_attrs(hass: HomeAssistant) -> None: assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") - assert state.state == "some_json_value" - assert state.attributes["key"] == "some_json_value" + assert state.state == "123" + assert state.attributes["other_key"] == "some_json_value" @respx.mock @@ -483,7 +489,6 @@ async def test_update_with_no_template(hass: HomeAssistant) -> None: "method": "GET", "json_attributes": ["key"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, "verify_ssl": "true", "timeout": 30, "headers": {"Accept": "text/xml"}, @@ -519,7 +524,7 @@ async def test_update_with_json_attrs_no_data( "value_template": "{{ value_json.key }}", "json_attributes": ["key"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, "headers": {"Accept": "text/xml"}, @@ -556,7 +561,6 @@ async def test_update_with_json_attrs_not_dict( "value_template": "{{ value_json.key }}", "json_attributes": ["key"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, "verify_ssl": "true", "timeout": 30, "headers": {"Accept": "text/xml"}, @@ -568,7 +572,7 @@ async def test_update_with_json_attrs_not_dict( state = hass.states.get("sensor.foo") assert state.state == "" - assert state.attributes == {"unit_of_measurement": "MB", "friendly_name": "foo"} + assert state.attributes == {"friendly_name": "foo"} assert "not a dictionary or list" in caplog.text @@ -594,7 +598,7 @@ async def test_update_with_json_attrs_bad_JSON( "value_template": "{{ value_json.key }}", "json_attributes": ["key"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, "headers": {"Accept": "text/xml"}, @@ -618,7 +622,7 @@ async def test_update_with_json_attrs_with_json_attrs_path(hass: HomeAssistant) status_code=HTTPStatus.OK, json={ "toplevel": { - "master_value": "master", + "master_value": "123", "second_level": { "some_json_key": "some_json_value", "some_json_key2": "some_json_value2", @@ -638,7 +642,7 @@ async def test_update_with_json_attrs_with_json_attrs_path(hass: HomeAssistant) "json_attributes_path": "$.toplevel.second_level", "json_attributes": ["some_json_key", "some_json_key2"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, "headers": {"Accept": "text/xml"}, @@ -649,7 +653,7 @@ async def test_update_with_json_attrs_with_json_attrs_path(hass: HomeAssistant) assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") - assert state.state == "master" + assert state.state == "123" assert state.attributes["some_json_key"] == "some_json_value" assert state.attributes["some_json_key2"] == "some_json_value2" @@ -663,7 +667,7 @@ async def test_update_with_xml_convert_json_attrs_with_json_attrs_path( respx.get("http://localhost").respond( status_code=HTTPStatus.OK, headers={"content-type": "text/xml"}, - content="mastersome_json_valuesome_json_value2", + content="123some_json_valuesome_json_value2", ) assert await async_setup_component( hass, @@ -677,7 +681,7 @@ async def test_update_with_xml_convert_json_attrs_with_json_attrs_path( "json_attributes_path": "$.toplevel.second_level", "json_attributes": ["some_json_key", "some_json_key2"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, } @@ -687,7 +691,7 @@ async def test_update_with_xml_convert_json_attrs_with_json_attrs_path( assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") - assert state.state == "master" + assert state.state == "123" assert state.attributes["some_json_key"] == "some_json_value" assert state.attributes["some_json_key2"] == "some_json_value2" @@ -701,7 +705,7 @@ async def test_update_with_xml_convert_json_attrs_with_jsonattr_template( respx.get("http://localhost").respond( status_code=HTTPStatus.OK, headers={"content-type": "text/xml"}, - content='01255648alexander000bogus000000000upupupup000x0XF0x0XF 0', + content='01255648alexander000123000000000upupupup000x0XF0x0XF 0', ) assert await async_setup_component( hass, @@ -715,7 +719,7 @@ async def test_update_with_xml_convert_json_attrs_with_jsonattr_template( "json_attributes_path": "$.response", "json_attributes": ["led0", "led1", "temp0", "time0", "ver"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, } @@ -725,7 +729,7 @@ async def test_update_with_xml_convert_json_attrs_with_jsonattr_template( assert len(hass.states.async_all("sensor")) == 1 state = hass.states.get("sensor.foo") - assert state.state == "bogus" + assert state.state == "123" assert state.attributes["led0"] == "0" assert state.attributes["led1"] == "0" assert state.attributes["temp0"] == "0x0XF0x0XF" @@ -756,7 +760,7 @@ async def test_update_with_application_xml_convert_json_attrs_with_jsonattr_temp "json_attributes_path": "$.main", "json_attributes": ["dog", "cat"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, } @@ -793,7 +797,7 @@ async def test_update_with_xml_convert_bad_xml( "value_template": "{{ value_json.toplevel.master_value }}", "json_attributes": ["key"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, } @@ -830,7 +834,7 @@ async def test_update_with_failed_get( "value_template": "{{ value_json.toplevel.master_value }}", "json_attributes": ["key"], "name": "foo", - "unit_of_measurement": DATA_MEGABYTES, + "unit_of_measurement": UnitOfInformation.MEGABYTES, "verify_ssl": "true", "timeout": 30, } @@ -906,7 +910,7 @@ async def test_entity_config(hass: HomeAssistant) -> None: }, } - respx.get("http://localhost") % HTTPStatus.OK + respx.get("http://localhost").respond(status_code=HTTPStatus.OK, text="123") assert await async_setup_component(hass, DOMAIN, config) await hass.async_block_till_done() @@ -914,7 +918,7 @@ async def test_entity_config(hass: HomeAssistant) -> None: assert entity_registry.async_get("sensor.rest_sensor").unique_id == "very_unique" state = hass.states.get("sensor.rest_sensor") - assert state.state == "" + assert state.state == "123" assert state.attributes == { "device_class": "temperature", "entity_picture": "blabla.png", diff --git a/tests/components/rflink/test_sensor.py b/tests/components/rflink/test_sensor.py index 0202894a41c..c7b9d0ea19b 100644 --- a/tests/components/rflink/test_sensor.py +++ b/tests/components/rflink/test_sensor.py @@ -17,8 +17,8 @@ from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, - PRECIPITATION_MILLIMETERS, STATE_UNKNOWN, + UnitOfPrecipitationDepth, UnitOfTemperature, ) @@ -287,7 +287,10 @@ async def test_sensor_attributes(hass, monkeypatch): assert rain_state assert rain_state.attributes["device_class"] == SensorDeviceClass.PRECIPITATION assert rain_state.attributes["state_class"] == SensorStateClass.TOTAL_INCREASING - assert rain_state.attributes["unit_of_measurement"] == PRECIPITATION_MILLIMETERS + assert ( + rain_state.attributes["unit_of_measurement"] + == UnitOfPrecipitationDepth.MILLIMETERS + ) humidity_state = hass.states.get("sensor.humidity_device") assert humidity_state diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py index cd562037713..8ddd53e38d0 100644 --- a/tests/components/rfxtrx/test_sensor.py +++ b/tests/components/rfxtrx/test_sensor.py @@ -7,7 +7,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import State @@ -46,7 +46,7 @@ async def test_one_sensor(hass, rfxtrx): state.attributes.get("friendly_name") == "WT260,WT260H,WT440H,WT450,WT450H 05:02 Temperature" ) - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS @pytest.mark.parametrize( @@ -88,7 +88,7 @@ async def test_one_sensor_no_datatype(hass, rfxtrx): assert state assert state.state == "unknown" assert state.attributes.get("friendly_name") == f"{base_name} Temperature" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS state = hass.states.get(f"{base_id}_humidity") assert state @@ -141,7 +141,7 @@ async def test_several_sensors(hass, rfxtrx): state.attributes.get("friendly_name") == "WT260,WT260H,WT440H,WT450,WT450H 05:02 Temperature" ) - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS state = hass.states.get("sensor.wt260_wt260h_wt440h_wt450_wt450h_06_01_temperature") assert state @@ -150,7 +150,7 @@ async def test_several_sensors(hass, rfxtrx): state.attributes.get("friendly_name") == "WT260,WT260H,WT440H,WT450,WT450H 06:01 Temperature" ) - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS state = hass.states.get("sensor.wt260_wt260h_wt440h_wt450_wt450h_06_01_humidity") assert state @@ -191,7 +191,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): state = hass.states.get(f"{base_id}_temperature") assert state assert state.state == "18.4" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS state = hass.states.get(f"{base_id}_battery") assert state @@ -223,7 +223,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): state = hass.states.get(f"{base_id}_temperature") assert state assert state.state == "14.9" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS state = hass.states.get(f"{base_id}_battery") assert state @@ -255,7 +255,7 @@ async def test_discover_sensor(hass, rfxtrx_automatic): state = hass.states.get(f"{base_id}_temperature") assert state assert state.state == "17.9" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS state = hass.states.get(f"{base_id}_battery") assert state diff --git a/tests/components/ridwell/conftest.py b/tests/components/ridwell/conftest.py index c4ff094638b..31788bc5282 100644 --- a/tests/components/ridwell/conftest.py +++ b/tests/components/ridwell/conftest.py @@ -7,18 +7,21 @@ import pytest from homeassistant.components.ridwell.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -ACCOUNT_ID = "12345" +TEST_ACCOUNT_ID = "12345" +TEST_DASHBOARD_URL = "https://www.ridwell.com/users/12345/dashboard" +TEST_PASSWORD = "password" +TEST_USERNAME = "user@email.com" +TEST_USER_ID = "12345" @pytest.fixture(name="account") def account_fixture(): """Define a Ridwell account.""" return Mock( - account_id=ACCOUNT_ID, + account_id=TEST_ACCOUNT_ID, address={ "street1": "123 Main Street", "city": "New York", @@ -42,7 +45,9 @@ def client_fixture(account): """Define an aioridwell client.""" return Mock( async_authenticate=AsyncMock(), - async_get_accounts=AsyncMock(return_value={ACCOUNT_ID: account}), + async_get_accounts=AsyncMock(return_value={TEST_ACCOUNT_ID: account}), + get_dashboard_url=Mock(return_value=TEST_DASHBOARD_URL), + user_id=TEST_USER_ID, ) @@ -58,22 +63,27 @@ def config_entry_fixture(hass, config): def config_fixture(hass): """Define a config entry data fixture.""" return { - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, } -@pytest.fixture(name="setup_ridwell") -async def setup_ridwell_fixture(hass, client, config): - """Define a fixture to set up Ridwell.""" +@pytest.fixture(name="mock_aioridwell") +async def mock_aioridwell_fixture(hass, client, config): + """Define a fixture to patch aioridwell.""" with patch( "homeassistant.components.ridwell.config_flow.async_get_client", return_value=client, ), patch( - "homeassistant.components.ridwell.async_get_client", return_value=client - ), patch( - "homeassistant.components.ridwell.PLATFORMS", [] + "homeassistant.components.ridwell.coordinator.async_get_client", + return_value=client, ): - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() yield + + +@pytest.fixture(name="setup_config_entry") +async def setup_config_entry_fixture(hass, config_entry, mock_aioridwell): + """Define a fixture to set up ridwell.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield diff --git a/tests/components/ridwell/test_config_flow.py b/tests/components/ridwell/test_config_flow.py index a28660bb7a4..6ec34ba96a7 100644 --- a/tests/components/ridwell/test_config_flow.py +++ b/tests/components/ridwell/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Ridwell config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from aioridwell.errors import InvalidCredentialsError, RidwellError import pytest @@ -7,11 +7,51 @@ import pytest from homeassistant import config_entries from homeassistant.components.ridwell.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from .conftest import TEST_PASSWORD, TEST_USERNAME -async def test_duplicate_error(hass: HomeAssistant, config, config_entry): + +@pytest.mark.parametrize( + "get_client_response,errors", + [ + (AsyncMock(side_effect=InvalidCredentialsError), {"base": "invalid_auth"}), + (AsyncMock(side_effect=RidwellError), {"base": "unknown"}), + ], +) +async def test_create_entry(hass, config, errors, get_client_response, mock_aioridwell): + """Test creating an entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + + # Test errors that can arise: + with patch( + "homeassistant.components.ridwell.config_flow.async_get_client", + get_client_response, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == errors + + # Test that we can recover and finish the flow after errors occur: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_USERNAME + assert result["data"] == { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + } + + +async def test_duplicate_error(hass, config, setup_config_entry): """Test that errors are shown when duplicate entries are added.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config @@ -20,56 +60,15 @@ async def test_duplicate_error(hass: HomeAssistant, config, config_entry): assert result["reason"] == "already_configured" -@pytest.mark.parametrize( - "exc,error", - [ - (InvalidCredentialsError, "invalid_auth"), - (RidwellError, "unknown"), - ], -) -async def test_errors(hass: HomeAssistant, config, error, exc) -> None: - """Test that various exceptions show the correct error.""" - with patch( - "homeassistant.components.ridwell.config_flow.async_get_client", side_effect=exc - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config - ) - assert result["type"] == FlowResultType.FORM - assert result["errors"]["base"] == error - - -async def test_show_form_user(hass: HomeAssistant) -> None: - """Test showing the form to input credentials.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == FlowResultType.FORM - assert result["step_id"] == "user" - assert result["errors"] is None - - -async def test_step_reauth( - hass: HomeAssistant, config, config_entry, setup_ridwell -) -> None: +async def test_step_reauth(hass, config, config_entry, setup_config_entry) -> None: """Test a full reauth flow.""" result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_REAUTH}, - data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=config ) result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_PASSWORD: "password"}, + user_input={CONF_PASSWORD: "new_password"}, ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 - - -async def test_step_user(hass: HomeAssistant, config, setup_ridwell) -> None: - """Test that the full user step succeeds.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=config - ) - assert result["type"] == FlowResultType.CREATE_ENTRY diff --git a/tests/components/ridwell/test_diagnostics.py b/tests/components/ridwell/test_diagnostics.py index 96d1531ac84..1d18bb2f8f6 100644 --- a/tests/components/ridwell/test_diagnostics.py +++ b/tests/components/ridwell/test_diagnostics.py @@ -4,7 +4,7 @@ from homeassistant.components.diagnostics import REDACTED from tests.components.diagnostics import get_diagnostics_for_config_entry -async def test_entry_diagnostics(hass, config_entry, hass_client, setup_ridwell): +async def test_entry_diagnostics(hass, config_entry, hass_client, setup_config_entry): """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "entry": { diff --git a/tests/components/roku/test_remote.py b/tests/components/roku/test_remote.py index df36558e22e..5a0f00ab3b6 100644 --- a/tests/components/roku/test_remote.py +++ b/tests/components/roku/test_remote.py @@ -16,8 +16,6 @@ from tests.common import MockConfigEntry MAIN_ENTITY_ID = f"{REMOTE_DOMAIN}.my_roku_3" -# pylint: disable=redefined-outer-name - async def test_setup(hass: HomeAssistant, init_integration: MockConfigEntry) -> None: """Test setup with basic config.""" diff --git a/tests/components/rtsp_to_webrtc/test_config_flow.py b/tests/components/rtsp_to_webrtc/test_config_flow.py index 6386a942cc4..6101a045ffd 100644 --- a/tests/components/rtsp_to_webrtc/test_config_flow.py +++ b/tests/components/rtsp_to_webrtc/test_config_flow.py @@ -26,7 +26,6 @@ async def test_web_full_flow(hass: HomeAssistant) -> None: assert result.get("step_id") == "user" assert result.get("data_schema").schema.get("server_url") == str assert not result.get("errors") - assert "flow_id" in result with patch("rtsp_to_webrtc.client.Client.heartbeat"), patch( "homeassistant.components.rtsp_to_webrtc.async_setup_entry", return_value=True, @@ -63,7 +62,6 @@ async def test_invalid_url(hass: HomeAssistant) -> None: assert result.get("step_id") == "user" assert result.get("data_schema").schema.get("server_url") == str assert not result.get("errors") - assert "flow_id" in result result = await hass.config_entries.flow.async_configure( result["flow_id"], {"server_url": "not-a-url"} ) @@ -81,7 +79,6 @@ async def test_server_unreachable(hass: HomeAssistant) -> None: assert result.get("type") == "form" assert result.get("step_id") == "user" assert not result.get("errors") - assert "flow_id" in result with patch( "rtsp_to_webrtc.client.Client.heartbeat", side_effect=rtsp_to_webrtc.exceptions.ClientError(), @@ -102,7 +99,6 @@ async def test_server_failure(hass: HomeAssistant) -> None: assert result.get("type") == "form" assert result.get("step_id") == "user" assert not result.get("errors") - assert "flow_id" in result with patch( "rtsp_to_webrtc.client.Client.heartbeat", side_effect=rtsp_to_webrtc.exceptions.ResponseError(), @@ -214,7 +210,6 @@ async def test_hassio_discovery_server_failure(hass: HomeAssistant) -> None: assert result.get("type") == "form" assert result.get("step_id") == "hassio_confirm" assert not result.get("errors") - assert "flow_id" in result with patch( "rtsp_to_webrtc.client.Client.heartbeat", diff --git a/tests/components/ruuvi_gateway/__init__.py b/tests/components/ruuvi_gateway/__init__.py new file mode 100644 index 00000000000..219eb09f774 --- /dev/null +++ b/tests/components/ruuvi_gateway/__init__.py @@ -0,0 +1 @@ +"""Tests for the Ruuvi Gateway integration.""" diff --git a/tests/components/ruuvi_gateway/conftest.py b/tests/components/ruuvi_gateway/conftest.py new file mode 100644 index 00000000000..6a57ae00b1e --- /dev/null +++ b/tests/components/ruuvi_gateway/conftest.py @@ -0,0 +1,8 @@ +"""ruuvi_gateway session fixtures.""" + +import pytest + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" diff --git a/tests/components/ruuvi_gateway/consts.py b/tests/components/ruuvi_gateway/consts.py new file mode 100644 index 00000000000..bd544fb2098 --- /dev/null +++ b/tests/components/ruuvi_gateway/consts.py @@ -0,0 +1,12 @@ +"""Constants for ruuvi_gateway tests.""" +from __future__ import annotations + +ASYNC_SETUP_ENTRY = "homeassistant.components.ruuvi_gateway.async_setup_entry" +GET_GATEWAY_HISTORY_DATA = "aioruuvigateway.api.get_gateway_history_data" +EXPECTED_TITLE = "Ruuvi Gateway EE:FF" +BASE_DATA = { + "host": "1.1.1.1", + "token": "toktok", +} +GATEWAY_MAC = "AA:BB:CC:DD:EE:FF" +GATEWAY_MAC_LOWER = GATEWAY_MAC.lower() diff --git a/tests/components/ruuvi_gateway/test_config_flow.py b/tests/components/ruuvi_gateway/test_config_flow.py new file mode 100644 index 00000000000..4f7e1ae116e --- /dev/null +++ b/tests/components/ruuvi_gateway/test_config_flow.py @@ -0,0 +1,154 @@ +"""Test the Ruuvi Gateway config flow.""" +from unittest.mock import patch + +from aioruuvigateway.excs import CannotConnect, InvalidAuth +import pytest + +from homeassistant import config_entries +from homeassistant.components import dhcp +from homeassistant.components.ruuvi_gateway.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .consts import ( + BASE_DATA, + EXPECTED_TITLE, + GATEWAY_MAC_LOWER, + GET_GATEWAY_HISTORY_DATA, +) +from .utils import patch_gateway_ok, patch_setup_entry_ok + +DHCP_IP = "1.2.3.4" +DHCP_DATA = {**BASE_DATA, "host": DHCP_IP} + + +@pytest.mark.parametrize( + "init_data, init_context, entry", + [ + ( + None, + {"source": config_entries.SOURCE_USER}, + BASE_DATA, + ), + ( + dhcp.DhcpServiceInfo( + hostname="RuuviGateway1234", + ip=DHCP_IP, + macaddress="12:34:56:78:90:ab", + ), + {"source": config_entries.SOURCE_DHCP}, + DHCP_DATA, + ), + ], + ids=["user", "dhcp"], +) +async def test_ok_setup(hass: HomeAssistant, init_data, init_context, entry) -> None: + """Test we get the form.""" + init_result = await hass.config_entries.flow.async_init( + DOMAIN, + data=init_data, + context=init_context, + ) + assert init_result["type"] == FlowResultType.FORM + assert init_result["step_id"] == config_entries.SOURCE_USER + assert init_result["errors"] is None + + # Check that we can finalize setup + with patch_gateway_ok(), patch_setup_entry_ok() as mock_setup_entry: + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + entry, + ) + await hass.async_block_till_done() + assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["title"] == EXPECTED_TITLE + assert config_result["data"] == entry + assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + init_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch(GET_GATEWAY_HISTORY_DATA, side_effect=InvalidAuth): + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + BASE_DATA, + ) + + assert config_result["type"] == FlowResultType.FORM + assert config_result["errors"] == {"base": "invalid_auth"} + + # Check that we still can finalize setup + with patch_gateway_ok(), patch_setup_entry_ok() as mock_setup_entry: + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + BASE_DATA, + ) + await hass.async_block_till_done() + assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["title"] == EXPECTED_TITLE + assert config_result["data"] == BASE_DATA + assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + init_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch(GET_GATEWAY_HISTORY_DATA, side_effect=CannotConnect): + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + BASE_DATA, + ) + + assert config_result["type"] == FlowResultType.FORM + assert config_result["errors"] == {"base": "cannot_connect"} + + # Check that we still can finalize setup + with patch_gateway_ok(), patch_setup_entry_ok() as mock_setup_entry: + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + BASE_DATA, + ) + await hass.async_block_till_done() + assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["title"] == EXPECTED_TITLE + assert config_result["data"] == BASE_DATA + assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_unexpected(hass: HomeAssistant) -> None: + """Test we handle unexpected errors.""" + init_result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch(GET_GATEWAY_HISTORY_DATA, side_effect=MemoryError): + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + BASE_DATA, + ) + + assert config_result["type"] == FlowResultType.FORM + assert config_result["errors"] == {"base": "unknown"} + + # Check that we still can finalize setup + with patch_gateway_ok(), patch_setup_entry_ok() as mock_setup_entry: + config_result = await hass.config_entries.flow.async_configure( + init_result["flow_id"], + BASE_DATA, + ) + await hass.async_block_till_done() + assert config_result["type"] == FlowResultType.CREATE_ENTRY + assert config_result["title"] == EXPECTED_TITLE + assert config_result["data"] == BASE_DATA + assert config_result["context"]["unique_id"] == GATEWAY_MAC_LOWER + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/ruuvi_gateway/utils.py b/tests/components/ruuvi_gateway/utils.py new file mode 100644 index 00000000000..d3181ca8f5f --- /dev/null +++ b/tests/components/ruuvi_gateway/utils.py @@ -0,0 +1,30 @@ +"""Utilities for ruuvi_gateway tests.""" +from __future__ import annotations + +import time +from unittest.mock import _patch, patch + +from aioruuvigateway.models import HistoryResponse + +from tests.components.ruuvi_gateway.consts import ( + ASYNC_SETUP_ENTRY, + GATEWAY_MAC, + GET_GATEWAY_HISTORY_DATA, +) + + +def patch_gateway_ok() -> _patch: + """Patch gateway function to return valid data.""" + return patch( + GET_GATEWAY_HISTORY_DATA, + return_value=HistoryResponse( + timestamp=int(time.time()), + gw_mac=GATEWAY_MAC, + tags=[], + ), + ) + + +def patch_setup_entry_ok() -> _patch: + """Patch setup entry to return True.""" + return patch(ASYNC_SETUP_ENTRY, return_value=True) diff --git a/tests/components/rympro/__init__.py b/tests/components/rympro/__init__.py new file mode 100644 index 00000000000..6f06d9bdb20 --- /dev/null +++ b/tests/components/rympro/__init__.py @@ -0,0 +1 @@ +"""Tests for the Read Your Meter Pro integration.""" diff --git a/tests/components/rympro/test_config_flow.py b/tests/components/rympro/test_config_flow.py new file mode 100644 index 00000000000..ec0651fb881 --- /dev/null +++ b/tests/components/rympro/test_config_flow.py @@ -0,0 +1,202 @@ +"""Test the Read Your Meter Pro config flow.""" +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.rympro.config_flow import ( + CannotConnectError, + UnauthorizedError, +) +from homeassistant.components.rympro.const import DOMAIN +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TOKEN, CONF_UNIQUE_ID +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +TEST_DATA = { + CONF_EMAIL: "test-email", + CONF_PASSWORD: "test-password", + CONF_TOKEN: "test-token", + CONF_UNIQUE_ID: "test-account-number", +} + + +@pytest.fixture +def _config_entry(hass): + config_entry = MockConfigEntry( + domain=DOMAIN, + data=TEST_DATA, + unique_id=TEST_DATA[CONF_UNIQUE_ID], + ) + config_entry.add_to_hass(hass) + return config_entry + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.rympro.config_flow.RymPro.login", + return_value="test-token", + ), patch( + "homeassistant.components.rympro.config_flow.RymPro.account_info", + return_value={"accountNumber": TEST_DATA[CONF_UNIQUE_ID]}, + ), patch( + "homeassistant.components.rympro.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: TEST_DATA[CONF_EMAIL], + CONF_PASSWORD: TEST_DATA[CONF_PASSWORD], + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == TEST_DATA[CONF_EMAIL] + assert result2["data"] == TEST_DATA + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "exception, error", + [ + (UnauthorizedError, "invalid_auth"), + (CannotConnectError, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_login_error(hass, exception, error): + """Test we handle config flow errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch( + "homeassistant.components.rympro.config_flow.RymPro.login", + side_effect=exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: TEST_DATA[CONF_EMAIL], + CONF_PASSWORD: TEST_DATA[CONF_PASSWORD], + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": error} + + +async def test_form_already_exists(hass, _config_entry): + """Test that a flow with an existing account aborts.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.rympro.config_flow.RymPro.login", + return_value="test-token", + ), patch( + "homeassistant.components.rympro.config_flow.RymPro.account_info", + return_value={"accountNumber": TEST_DATA[CONF_UNIQUE_ID]}, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: TEST_DATA[CONF_EMAIL], + CONF_PASSWORD: TEST_DATA[CONF_PASSWORD], + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "already_configured" + + +async def test_form_reauth(hass, _config_entry): + """Test reauthentication.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": _config_entry.entry_id, + }, + data=_config_entry.data, + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.rympro.config_flow.RymPro.login", + return_value="test-token", + ), patch( + "homeassistant.components.rympro.config_flow.RymPro.account_info", + return_value={"accountNumber": TEST_DATA[CONF_UNIQUE_ID]}, + ), patch( + "homeassistant.components.rympro.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: TEST_DATA[CONF_EMAIL], + CONF_PASSWORD: "new_password", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + assert _config_entry.data[CONF_PASSWORD] == "new_password" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_reauth_with_new_account(hass, _config_entry): + """Test reauthentication with new account.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": _config_entry.entry_id, + }, + data=_config_entry.data, + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.rympro.config_flow.RymPro.login", + return_value="test-token", + ), patch( + "homeassistant.components.rympro.config_flow.RymPro.account_info", + return_value={"accountNumber": "new-account-number"}, + ), patch( + "homeassistant.components.rympro.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: TEST_DATA[CONF_EMAIL], + CONF_PASSWORD: TEST_DATA[CONF_PASSWORD], + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + assert _config_entry.data[CONF_UNIQUE_ID] == "new-account-number" + assert _config_entry.unique_id == "new-account-number" + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/sabnzbd/test_config_flow.py b/tests/components/sabnzbd/test_config_flow.py index 2c5e9e1ffc9..f48f788b348 100644 --- a/tests/components/sabnzbd/test_config_flow.py +++ b/tests/components/sabnzbd/test_config_flow.py @@ -10,7 +10,6 @@ from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_NAME, - CONF_PATH, CONF_PORT, CONF_SSL, CONF_URL, @@ -21,7 +20,6 @@ VALID_CONFIG = { CONF_NAME: "Sabnzbd", CONF_API_KEY: "edc3eee7330e4fdda04489e3fbc283d0", CONF_URL: "http://localhost:8080", - CONF_PATH: "", } VALID_CONFIG_OLD = { @@ -29,7 +27,6 @@ VALID_CONFIG_OLD = { CONF_API_KEY: "edc3eee7330e4fdda04489e3fbc283d0", CONF_HOST: "localhost", CONF_PORT: 8080, - CONF_PATH: "", CONF_SSL: False, } @@ -60,7 +57,6 @@ async def test_create_entry(hass): assert result2["data"] == { CONF_API_KEY: "edc3eee7330e4fdda04489e3fbc283d0", CONF_NAME: "Sabnzbd", - CONF_PATH: "", CONF_URL: "http://localhost:8080", } assert len(mock_setup_entry.mock_calls) == 1 @@ -99,5 +95,4 @@ async def test_import_flow(hass) -> None: assert result["data"][CONF_API_KEY] == "edc3eee7330e4fdda04489e3fbc283d0" assert result["data"][CONF_HOST] == "localhost" assert result["data"][CONF_PORT] == 8080 - assert result["data"][CONF_PATH] == "" assert result["data"][CONF_SSL] is False diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 30bb1052702..6679f1d786e 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for Samsung TV config flow.""" +from collections.abc import Generator import socket from unittest.mock import ANY, AsyncMock, Mock, call, patch @@ -60,7 +61,6 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component -from . import setup_samsungtv_entry from .const import ( MOCK_CONFIG_ENCRYPTED_WS, MOCK_ENTRYDATA_ENCRYPTED_WS, @@ -217,6 +217,15 @@ DEVICEINFO_WEBSOCKET_NO_SSL = { } +@pytest.fixture(autouse=True, name="mock_setup_entry") +def override_async_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.samsungtv.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + @pytest.mark.usefixtures("remote", "rest_api_failing") async def test_user_legacy(hass: HomeAssistant) -> None: """Test starting a flow by user.""" @@ -933,7 +942,9 @@ async def test_import_legacy(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remote", "remotews", "rest_api_failing") -async def test_import_legacy_without_name(hass: HomeAssistant) -> None: +async def test_import_legacy_without_name( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: """Test importing from yaml without a name.""" with patch( "homeassistant.components.samsungtv.bridge.SamsungTVEncryptedWSAsyncRemote.start_listening", @@ -951,10 +962,13 @@ async def test_import_legacy_without_name(hass: HomeAssistant) -> None: assert result["data"][CONF_MANUFACTURER] == "Samsung" assert result["result"].unique_id is None + mock_setup_entry.assert_called_once() entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].data[CONF_METHOD] == METHOD_LEGACY - assert entries[0].data[CONF_PORT] == LEGACY_PORT + # METHOD / PORT failed during import + # They will get checked/set on setup + assert CONF_METHOD not in entries[0].data + assert CONF_PORT not in entries[0].data @pytest.mark.usefixtures("remotews", "rest_api") @@ -1382,7 +1396,7 @@ async def test_update_old_entry(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_added_from_dhcp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing mac and unique id added.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None) @@ -1390,10 +1404,7 @@ async def test_update_missing_mac_unique_id_added_from_dhcp( with patch( "homeassistant.components.samsungtv.async_setup", return_value=True, - ) as mock_setup, patch( - "homeassistant.components.samsungtv.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, @@ -1411,7 +1422,7 @@ async def test_update_missing_mac_unique_id_added_from_dhcp( @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_added_from_zeroconf( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing mac and unique id added.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None) @@ -1419,10 +1430,7 @@ async def test_update_missing_mac_unique_id_added_from_zeroconf( with patch( "homeassistant.components.samsungtv.async_setup", return_value=True, - ) as mock_setup, patch( - "homeassistant.components.samsungtv.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, @@ -1438,7 +1446,9 @@ async def test_update_missing_mac_unique_id_added_from_zeroconf( @pytest.mark.usefixtures("remote", "rest_api_failing") -async def test_update_missing_model_added_from_ssdp(hass: HomeAssistant) -> None: +async def test_update_missing_model_added_from_ssdp( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: """Test missing model added via ssdp on legacy models.""" entry = MockConfigEntry( domain=DOMAIN, @@ -1449,10 +1459,7 @@ async def test_update_missing_model_added_from_ssdp(hass: HomeAssistant) -> None with patch( "homeassistant.components.samsungtv.async_setup", return_value=True, - ) as mock_setup, patch( - "homeassistant.components.samsungtv.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -1469,7 +1476,7 @@ async def test_update_missing_model_added_from_ssdp(hass: HomeAssistant) -> None @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_ssdp_location_added_from_ssdp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing mac, ssdp_location, and unique id added via ssdp.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None) @@ -1477,10 +1484,7 @@ async def test_update_missing_mac_unique_id_ssdp_location_added_from_ssdp( with patch( "homeassistant.components.samsungtv.async_setup", return_value=True, - ) as mock_setup, patch( - "homeassistant.components.samsungtv.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -1525,7 +1529,7 @@ async def test_update_zeroconf_discovery_preserved_unique_id( @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_added_ssdp_location_updated_from_ssdp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing mac and unique id with outdated ssdp_location with the wrong st added via ssdp.""" entry = MockConfigEntry( @@ -1540,10 +1544,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_updated_from_ssd with patch( "homeassistant.components.samsungtv.async_setup", return_value=True, - ) as mock_setup, patch( - "homeassistant.components.samsungtv.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -1565,7 +1566,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_updated_from_ssd @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_updated_from_ssdp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing mac and unique id with outdated ssdp_location with the correct st added via ssdp.""" entry = MockConfigEntry( @@ -1580,10 +1581,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_upd with patch( "homeassistant.components.samsungtv.async_setup", return_value=True, - ) as mock_setup, patch( - "homeassistant.components.samsungtv.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -1606,7 +1604,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_rendering_st_upd @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_missing_mac_unique_id_added_ssdp_location_main_tv_agent_st_updated_from_ssdp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing mac and unique id with outdated ssdp_location with the correct st added via ssdp.""" entry = MockConfigEntry( @@ -1622,10 +1620,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_main_tv_agent_st with patch( "homeassistant.components.samsungtv.async_setup", return_value=True, - ) as mock_setup, patch( - "homeassistant.components.samsungtv.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -1652,7 +1647,7 @@ async def test_update_missing_mac_unique_id_added_ssdp_location_main_tv_agent_st @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_ssdp_location_rendering_st_updated_from_ssdp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test with outdated ssdp_location with the correct st added via ssdp.""" entry = MockConfigEntry( @@ -1664,10 +1659,7 @@ async def test_update_ssdp_location_rendering_st_updated_from_ssdp( with patch( "homeassistant.components.samsungtv.async_setup", return_value=True, - ) as mock_setup, patch( - "homeassistant.components.samsungtv.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -1690,7 +1682,7 @@ async def test_update_ssdp_location_rendering_st_updated_from_ssdp( @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_main_tv_ssdp_location_rendering_st_updated_from_ssdp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test with outdated ssdp_location with the correct st added via ssdp.""" entry = MockConfigEntry( @@ -1702,10 +1694,7 @@ async def test_update_main_tv_ssdp_location_rendering_st_updated_from_ssdp( with patch( "homeassistant.components.samsungtv.async_setup", return_value=True, - ) as mock_setup, patch( - "homeassistant.components.samsungtv.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -1728,7 +1717,7 @@ async def test_update_main_tv_ssdp_location_rendering_st_updated_from_ssdp( @pytest.mark.usefixtures("remotews", "rest_api") async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing mac and unique id added.""" entry = MockConfigEntry( @@ -1740,10 +1729,7 @@ async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( with patch( "homeassistant.components.samsungtv.async_setup", return_value=True, - ) as mock_setup, patch( - "homeassistant.components.samsungtv.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, @@ -1759,7 +1745,9 @@ async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( @pytest.mark.usefixtures("remote") -async def test_update_legacy_missing_mac_from_dhcp(hass: HomeAssistant) -> None: +async def test_update_legacy_missing_mac_from_dhcp( + hass: HomeAssistant, mock_setup_entry: AsyncMock +) -> None: """Test missing mac added.""" entry = MockConfigEntry( domain=DOMAIN, @@ -1770,10 +1758,7 @@ async def test_update_legacy_missing_mac_from_dhcp(hass: HomeAssistant) -> None: with patch( "homeassistant.components.samsungtv.async_setup", return_value=True, - ) as mock_setup, patch( - "homeassistant.components.samsungtv.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, @@ -1792,7 +1777,7 @@ async def test_update_legacy_missing_mac_from_dhcp(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remote") async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( - hass: HomeAssistant, rest_api: Mock + hass: HomeAssistant, rest_api: Mock, mock_setup_entry: AsyncMock ) -> None: """Test missing mac added when there is no unique id.""" rest_api.rest_device_info.side_effect = HttpApiError @@ -1810,10 +1795,7 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( ), patch( "homeassistant.components.samsungtv.async_setup", return_value=True, - ) as mock_setup, patch( - "homeassistant.components.samsungtv.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, @@ -1832,7 +1814,7 @@ async def test_update_legacy_missing_mac_from_dhcp_no_unique_id( @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_ssdp_location_unique_id_added_from_ssdp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing ssdp_location, and unique id added via ssdp.""" entry = MockConfigEntry( @@ -1844,10 +1826,7 @@ async def test_update_ssdp_location_unique_id_added_from_ssdp( with patch( "homeassistant.components.samsungtv.async_setup", return_value=True, - ) as mock_setup, patch( - "homeassistant.components.samsungtv.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -1867,7 +1846,7 @@ async def test_update_ssdp_location_unique_id_added_from_ssdp( @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_ssdp_location_unique_id_added_from_ssdp_with_rendering_control_st( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test missing ssdp_location, and unique id added via ssdp with rendering control st.""" entry = MockConfigEntry( @@ -1879,10 +1858,7 @@ async def test_update_ssdp_location_unique_id_added_from_ssdp_with_rendering_con with patch( "homeassistant.components.samsungtv.async_setup", return_value=True, - ) as mock_setup, patch( - "homeassistant.components.samsungtv.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -2018,15 +1994,17 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: del encrypted_entry_data[CONF_TOKEN] del encrypted_entry_data[CONF_SESSION_ID] - entry = await setup_samsungtv_entry(hass, encrypted_entry_data) - assert entry.state == config_entries.ConfigEntryState.SETUP_ERROR - flows_in_progress = [ - flow - for flow in hass.config_entries.flow.async_progress() - if flow["context"]["source"] == "reauth" - ] - assert len(flows_in_progress) == 1 - result = flows_in_progress[0] + entry = MockConfigEntry(domain=DOMAIN, data=encrypted_entry_data) + entry.add_to_hass(hass) + assert entry.state == config_entries.ConfigEntryState.NOT_LOADED + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, + data=entry.data, + ) + assert result["type"] == "form" + assert result["errors"] == {} with patch( "homeassistant.components.samsungtv.config_flow.SamsungTVEncryptedWSAsyncAuthenticator", @@ -2085,7 +2063,7 @@ async def test_form_reauth_encrypted(hass: HomeAssistant) -> None: @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test updating the wrong udn from ssdp via upnp udn match.""" entry = MockConfigEntry( @@ -2097,10 +2075,7 @@ async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp( with patch( "homeassistant.components.samsungtv.async_setup", return_value=True, - ) as mock_setup, patch( - "homeassistant.components.samsungtv.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -2118,7 +2093,7 @@ async def test_update_incorrect_udn_matching_upnp_udn_unique_id_added_from_ssdp( @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test updating the wrong udn from ssdp via mac match.""" entry = MockConfigEntry( @@ -2130,10 +2105,7 @@ async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp( with patch( "homeassistant.components.samsungtv.async_setup", return_value=True, - ) as mock_setup, patch( - "homeassistant.components.samsungtv.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, @@ -2151,7 +2123,7 @@ async def test_update_incorrect_udn_matching_mac_unique_id_added_from_ssdp( @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_update_incorrect_udn_matching_mac_from_dhcp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test that DHCP updates the wrong udn from ssdp via mac match.""" entry = MockConfigEntry( @@ -2164,10 +2136,7 @@ async def test_update_incorrect_udn_matching_mac_from_dhcp( with patch( "homeassistant.components.samsungtv.async_setup", return_value=True, - ) as mock_setup, patch( - "homeassistant.components.samsungtv.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, @@ -2185,7 +2154,7 @@ async def test_update_incorrect_udn_matching_mac_from_dhcp( @pytest.mark.usefixtures("remotews", "rest_api", "remoteencws_failing") async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp( - hass: HomeAssistant, + hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test that DHCP does not update the wrong udn from ssdp via host match.""" entry = MockConfigEntry( @@ -2198,10 +2167,7 @@ async def test_no_update_incorrect_udn_not_matching_mac_from_dhcp( with patch( "homeassistant.components.samsungtv.async_setup", return_value=True, - ) as mock_setup, patch( - "homeassistant.components.samsungtv.async_setup_entry", - return_value=True, - ) as mock_setup_entry: + ) as mock_setup: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_DHCP}, diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 3b4e33ebf3e..710d421c97c 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -5,10 +5,14 @@ import pytest from homeassistant.components.media_player import DOMAIN, SUPPORT_TURN_ON from homeassistant.components.samsungtv.const import ( + CONF_MANUFACTURER, CONF_ON_ACTION, + CONF_SESSION_ID, CONF_SSDP_MAIN_TV_AGENT_LOCATION, CONF_SSDP_RENDERING_CONTROL_LOCATION, DOMAIN as SAMSUNGTV_DOMAIN, + LEGACY_PORT, + METHOD_LEGACY, METHOD_WEBSOCKET, UPNP_SVC_MAIN_TV_AGENT, UPNP_SVC_RENDERING_CONTROL, @@ -22,12 +26,16 @@ from homeassistant.const import ( CONF_MAC, CONF_METHOD, CONF_NAME, + CONF_PORT, + CONF_TOKEN, SERVICE_VOLUME_UP, ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +from . import setup_samsungtv_entry from .const import ( + MOCK_ENTRYDATA_ENCRYPTED_WS, MOCK_ENTRYDATA_WS, MOCK_SSDP_DATA_MAIN_TV_AGENT_ST, MOCK_SSDP_DATA_RENDERING_CONTROL_ST, @@ -193,3 +201,33 @@ async def test_setup_updates_from_ssdp(hass: HomeAssistant) -> None: entry.data[CONF_SSDP_RENDERING_CONTROL_LOCATION] == "https://fake_host:12345/test" ) + + +@pytest.mark.usefixtures("remoteencws", "rest_api") +async def test_reauth_triggered_encrypted(hass: HomeAssistant) -> None: + """Test reauth flow is triggered for encrypted TVs.""" + encrypted_entry_data = {**MOCK_ENTRYDATA_ENCRYPTED_WS} + del encrypted_entry_data[CONF_TOKEN] + del encrypted_entry_data[CONF_SESSION_ID] + + entry = await setup_samsungtv_entry(hass, encrypted_entry_data) + assert entry.state == ConfigEntryState.SETUP_ERROR + flows_in_progress = [ + flow + for flow in hass.config_entries.flow.async_progress() + if flow["context"]["source"] == "reauth" + ] + assert len(flows_in_progress) == 1 + + +@pytest.mark.usefixtures("remote", "remotews", "rest_api_failing") +async def test_update_imported_legacy_without_method(hass: HomeAssistant) -> None: + """Test updating an imported legacy entry without a method.""" + await setup_samsungtv_entry( + hass, {CONF_HOST: "fake_host", CONF_MANUFACTURER: "Samsung"} + ) + + entries = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) + assert len(entries) == 1 + assert entries[0].data[CONF_METHOD] == METHOD_LEGACY + assert entries[0].data[CONF_PORT] == LEGACY_PORT diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index e9b66049e61..f32603af7cc 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -18,7 +18,7 @@ from homeassistant.const import ( CONF_UNIT_OF_MEASUREMENT, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -172,7 +172,7 @@ async def test_scrape_uom_and_classes(hass: HomeAssistant) -> None: 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_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS assert state.attributes[CONF_DEVICE_CLASS] == SensorDeviceClass.TEMPERATURE assert state.attributes[CONF_STATE_CLASS] == SensorStateClass.MEASUREMENT diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 7fbb1f6bfe4..0c42b46059f 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -1,5 +1,5 @@ """The tests for the Script component.""" -# pylint: disable=protected-access + import asyncio from datetime import timedelta from unittest.mock import Mock, patch diff --git a/tests/components/season/test_config_flow.py b/tests/components/season/test_config_flow.py index 9a64bcd140a..2cf9e46b666 100644 --- a/tests/components/season/test_config_flow.py +++ b/tests/components/season/test_config_flow.py @@ -27,7 +27,6 @@ async def test_full_user_flow( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/sensibo/conftest.py b/tests/components/sensibo/conftest.py index 48c9317a5cb..b2798224b14 100644 --- a/tests/components/sensibo/conftest.py +++ b/tests/components/sensibo/conftest.py @@ -58,7 +58,7 @@ async def get_data_from_library( client = SensiboClient("123467890", aioclient_mock.create_session(hass.loop)) with patch("pysensibo.SensiboClient.async_get_devices", return_value=load_json): output = await client.async_get_devices_data() - await client._session.close() # pylint: disable=protected-access + await client._session.close() return output diff --git a/tests/components/sensibo/test_climate.py b/tests/components/sensibo/test_climate.py index 224248c2d16..e75e4844c53 100644 --- a/tests/components/sensibo/test_climate.py +++ b/tests/components/sensibo/test_climate.py @@ -99,8 +99,8 @@ async def test_climate( "fan_modes": ["quiet", "low", "medium"], "swing_modes": [ "stopped", - "fixedTop", - "fixedMiddleTop", + "fixedtop", + "fixedmiddletop", ], "current_temperature": 21.2, "temperature": 25, @@ -203,13 +203,13 @@ async def test_climate_swing( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_SWING_MODE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_SWING_MODE: "fixedTop"}, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_SWING_MODE: "fixedtop"}, blocking=True, ) await hass.async_block_till_done() state2 = hass.states.get("climate.hallway") - assert state2.attributes["swing_mode"] == "fixedTop" + assert state2.attributes["swing_mode"] == "fixedtop" monkeypatch.setattr( get_data.parsed["ABC999111"], @@ -240,13 +240,13 @@ async def test_climate_swing( await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_SWING_MODE, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_SWING_MODE: "fixedTop"}, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_SWING_MODE: "fixedtop"}, blocking=True, ) await hass.async_block_till_done() state3 = hass.states.get("climate.hallway") - assert state3.attributes["swing_mode"] == "fixedTop" + assert state3.attributes["swing_mode"] == "fixedtop" async def test_climate_temperatures( diff --git a/tests/components/sensibo/test_select.py b/tests/components/sensibo/test_select.py index 0fdf67fd5c2..acde7be41ef 100644 --- a/tests/components/sensibo/test_select.py +++ b/tests/components/sensibo/test_select.py @@ -34,7 +34,7 @@ async def test_select( assert state1.state == "stopped" monkeypatch.setattr( - get_data.parsed["ABC999111"], "horizontal_swing_mode", "fixedLeft" + get_data.parsed["ABC999111"], "horizontal_swing_mode", "fixedleft" ) with patch( @@ -48,7 +48,7 @@ async def test_select( await hass.async_block_till_done() state1 = hass.states.get("select.hallway_horizontal_swing") - assert state1.state == "fixedLeft" + assert state1.state == "fixedleft" async def test_select_set_option( @@ -95,7 +95,7 @@ async def test_select_set_option( await hass.services.async_call( SELECT_DOMAIN, SERVICE_SELECT_OPTION, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_OPTION: "fixedLeft"}, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_OPTION: "fixedleft"}, blocking=True, ) await hass.async_block_till_done() @@ -136,7 +136,7 @@ async def test_select_set_option( await hass.services.async_call( SELECT_DOMAIN, SERVICE_SELECT_OPTION, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_OPTION: "fixedLeft"}, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_OPTION: "fixedleft"}, blocking=True, ) await hass.async_block_till_done() @@ -154,10 +154,10 @@ async def test_select_set_option( await hass.services.async_call( SELECT_DOMAIN, SERVICE_SELECT_OPTION, - {ATTR_ENTITY_ID: state1.entity_id, ATTR_OPTION: "fixedLeft"}, + {ATTR_ENTITY_ID: state1.entity_id, ATTR_OPTION: "fixedleft"}, blocking=True, ) await hass.async_block_till_done() state2 = hass.states.get("select.hallway_horizontal_swing") - assert state2.state == "fixedLeft" + assert state2.state == "fixedleft" diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index d0027a6a07c..89501cf37df 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -1,38 +1,31 @@ """The test for sensor entity.""" +from __future__ import annotations + from datetime import date, datetime, timezone from decimal import Decimal +from typing import Any import pytest from pytest import approx from homeassistant.components.number import NumberDeviceClass -from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass +from homeassistant.components.sensor import ( + DEVICE_CLASS_UNITS, + SensorDeviceClass, + SensorStateClass, +) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, - LENGTH_CENTIMETERS, - LENGTH_INCHES, - LENGTH_KILOMETERS, - LENGTH_METERS, - LENGTH_MILES, - LENGTH_YARD, - MASS_GRAMS, - MASS_OUNCES, - PRESSURE_HPA, - PRESSURE_INHG, - PRESSURE_KPA, - PRESSURE_MMHG, - SPEED_INCHES_PER_HOUR, - SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, - SPEED_MILLIMETERS_PER_DAY, + PERCENTAGE, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - VOLUME_CUBIC_FEET, - VOLUME_CUBIC_METERS, - VOLUME_FLUID_OUNCE, - VOLUME_LITERS, + UnitOfEnergy, + UnitOfLength, + UnitOfMass, + UnitOfPressure, + UnitOfSpeed, UnitOfTemperature, + UnitOfVolume, + UnitOfVolumetricFlux, ) from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er @@ -47,10 +40,34 @@ from tests.common import mock_restore_cache_with_extra_data @pytest.mark.parametrize( "unit_system,native_unit,state_unit,native_value,state_value", [ - (US_CUSTOMARY_SYSTEM, TEMP_FAHRENHEIT, TEMP_FAHRENHEIT, 100, 100), - (US_CUSTOMARY_SYSTEM, TEMP_CELSIUS, TEMP_FAHRENHEIT, 38, 100), - (METRIC_SYSTEM, TEMP_FAHRENHEIT, TEMP_CELSIUS, 100, 38), - (METRIC_SYSTEM, TEMP_CELSIUS, TEMP_CELSIUS, 38, 38), + ( + US_CUSTOMARY_SYSTEM, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.FAHRENHEIT, + 100, + "100", + ), + ( + US_CUSTOMARY_SYSTEM, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, + 38, + "100", + ), + ( + METRIC_SYSTEM, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.CELSIUS, + 100, + "38", + ), + ( + METRIC_SYSTEM, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.CELSIUS, + 38, + "38", + ), ], ) async def test_temperature_conversion( @@ -78,7 +95,7 @@ async def test_temperature_conversion( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert float(state.state) == approx(float(state_value)) + assert state.state == state_value assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit @@ -92,7 +109,7 @@ async def test_temperature_conversion_wrong_device_class( platform.ENTITIES["0"] = platform.MockSensor( name="Test", native_value="0.0", - native_unit_of_measurement=TEMP_FAHRENHEIT, + native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=device_class, ) @@ -103,7 +120,7 @@ async def test_temperature_conversion_wrong_device_class( # Check temperature is not converted state = hass.states.get(entity0.entity_id) assert state.state == "0.0" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_FAHRENHEIT + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.FAHRENHEIT @pytest.mark.parametrize("state_class", ("measurement", "total_increasing")) @@ -229,25 +246,25 @@ async def test_reject_timezoneless_datetime_str( RESTORE_DATA = { - "str": {"native_unit_of_measurement": "°F", "native_value": "abc123"}, + "str": {"native_unit_of_measurement": None, "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_unit_of_measurement": None, "native_value": { "__type": "", "isoformat": date(2020, 2, 8).isoformat(), }, }, "datetime": { - "native_unit_of_measurement": "°F", + "native_unit_of_measurement": None, "native_value": { "__type": "", "isoformat": datetime(2020, 2, 8, 15, tzinfo=timezone.utc).isoformat(), }, }, "Decimal": { - "native_unit_of_measurement": "°F", + "native_unit_of_measurement": "kWh", "native_value": { "__type": "", "decimal_str": "123.4", @@ -265,19 +282,38 @@ RESTORE_DATA = { # None | str | int | float | date | datetime | Decimal: @pytest.mark.parametrize( - "native_value, native_value_type, expected_extra_data, device_class", + "native_value, native_value_type, expected_extra_data, device_class, uom", [ - ("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), + ("abc123", str, RESTORE_DATA["str"], None, None), + ( + 123, + int, + RESTORE_DATA["int"], + SensorDeviceClass.TEMPERATURE, + UnitOfTemperature.FAHRENHEIT, + ), + ( + 123.0, + float, + RESTORE_DATA["float"], + SensorDeviceClass.TEMPERATURE, + UnitOfTemperature.FAHRENHEIT, + ), + (date(2020, 2, 8), dict, RESTORE_DATA["date"], SensorDeviceClass.DATE, None), ( datetime(2020, 2, 8, 15, tzinfo=timezone.utc), dict, RESTORE_DATA["datetime"], SensorDeviceClass.TIMESTAMP, + None, + ), + ( + Decimal("123.4"), + dict, + RESTORE_DATA["Decimal"], + SensorDeviceClass.ENERGY, + UnitOfEnergy.KILO_WATT_HOUR, ), - (Decimal("123.4"), dict, RESTORE_DATA["Decimal"], SensorDeviceClass.ENERGY), ], ) async def test_restore_sensor_save_state( @@ -288,6 +324,7 @@ async def test_restore_sensor_save_state( native_value_type, expected_extra_data, device_class, + uom, ): """Test RestoreSensor.""" platform = getattr(hass.components, "test.sensor") @@ -295,7 +332,7 @@ async def test_restore_sensor_save_state( platform.ENTITIES["0"] = platform.MockRestoreSensor( name="Test", native_value=native_value, - native_unit_of_measurement=TEMP_FAHRENHEIT, + native_unit_of_measurement=uom, device_class=device_class, ) @@ -317,23 +354,23 @@ async def test_restore_sensor_save_state( @pytest.mark.parametrize( "native_value, native_value_type, extra_data, device_class, uom", [ - ("abc123", str, RESTORE_DATA["str"], None, "°F"), + ("abc123", str, RESTORE_DATA["str"], None, None), (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"), + (date(2020, 2, 8), date, RESTORE_DATA["date"], SensorDeviceClass.DATE, None), ( datetime(2020, 2, 8, 15, tzinfo=timezone.utc), datetime, RESTORE_DATA["datetime"], SensorDeviceClass.TIMESTAMP, - "°F", + None, ), ( Decimal("123.4"), Decimal, RESTORE_DATA["Decimal"], SensorDeviceClass.ENERGY, - "°F", + "kWh", ), (None, type(None), None, None, None), (None, type(None), {}, None, None), @@ -380,57 +417,65 @@ async def test_restore_sensor_restore_state( @pytest.mark.parametrize( - "device_class,native_unit,custom_unit,state_unit,native_value,custom_value", + "device_class, native_unit, custom_unit, state_unit, native_value, custom_state", [ # Smaller to larger unit, InHg is ~33x larger than hPa -> 1 more decimal ( SensorDeviceClass.PRESSURE, - PRESSURE_HPA, - PRESSURE_INHG, - PRESSURE_INHG, + UnitOfPressure.HPA, + UnitOfPressure.INHG, + UnitOfPressure.INHG, 1000.0, - 29.53, + "29.53", ), ( SensorDeviceClass.PRESSURE, - PRESSURE_KPA, - PRESSURE_HPA, - PRESSURE_HPA, + UnitOfPressure.KPA, + UnitOfPressure.HPA, + UnitOfPressure.HPA, 1.234, - 12.34, + "12.340", + ), + ( + SensorDeviceClass.ATMOSPHERIC_PRESSURE, + UnitOfPressure.HPA, + UnitOfPressure.MMHG, + UnitOfPressure.MMHG, + 1000, + "750", ), ( SensorDeviceClass.PRESSURE, - PRESSURE_HPA, - PRESSURE_MMHG, - PRESSURE_MMHG, + UnitOfPressure.HPA, + UnitOfPressure.MMHG, + UnitOfPressure.MMHG, 1000, - 750, + "750", ), # Not a supported pressure unit ( SensorDeviceClass.PRESSURE, - PRESSURE_HPA, + UnitOfPressure.HPA, "peer_pressure", - PRESSURE_HPA, - 1000, + UnitOfPressure.HPA, 1000, + "1000", ), ( SensorDeviceClass.TEMPERATURE, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TEMP_FAHRENHEIT, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.FAHRENHEIT, 37.5, - 99.5, + "99.5", ), ( SensorDeviceClass.TEMPERATURE, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, - TEMP_CELSIUS, + UnitOfTemperature.FAHRENHEIT, + UnitOfTemperature.CELSIUS, + UnitOfTemperature.CELSIUS, 100, - 38.0, + "38", ), ], ) @@ -442,7 +487,7 @@ async def test_custom_unit( custom_unit, state_unit, native_value, - custom_value, + custom_state, ): """Test custom unit.""" entity_registry = er.async_get(hass) @@ -468,145 +513,407 @@ async def test_custom_unit( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert float(state.state) == approx(float(custom_value)) + assert state.state == custom_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit @pytest.mark.parametrize( - "native_unit,custom_unit,state_unit,native_value,custom_value,device_class", + "device_class,native_unit,custom_unit,native_value,native_precision,default_state,custom_state", + [ + ( + SensorDeviceClass.ATMOSPHERIC_PRESSURE, + UnitOfPressure.HPA, + UnitOfPressure.INHG, + 1000.0, + 2, + "1000.00", # Native precision is 2 + "29.530", # One digit of precision added when converting + ), + ( + SensorDeviceClass.ATMOSPHERIC_PRESSURE, + UnitOfPressure.INHG, + UnitOfPressure.HPA, + 29.9211, + 3, + "29.921", # Native precision is 3 + "1013.24", # One digit of precision removed when converting + ), + ( + SensorDeviceClass.ATMOSPHERIC_PRESSURE, + UnitOfPressure.INHG, + UnitOfPressure.HPA, + -0.0001, + 3, + "0.000", # Native precision is 3 + "0.00", # One digit of precision removed when converting + ), + ], +) +async def test_native_precision_scaling( + hass, + enable_custom_integrations, + device_class, + native_unit, + custom_unit, + native_value, + native_precision, + default_state, + custom_state, +): + """Test native precision is influenced by unit conversion.""" + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get_or_create("sensor", "test", "very_unique") + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", + native_value=str(native_value), + native_precision=native_precision, + native_unit_of_measurement=native_unit, + device_class=device_class, + unique_id="very_unique", + ) + + 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 == default_state + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit + + entity_registry.async_update_entity_options( + entry.entity_id, "sensor", {"unit_of_measurement": custom_unit} + ) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.state == custom_state + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit + + +@pytest.mark.parametrize( + "device_class,native_unit,custom_precision,native_value,default_state,custom_state", + [ + ( + SensorDeviceClass.ATMOSPHERIC_PRESSURE, + UnitOfPressure.HPA, + 4, + 1000.0, + "1000.000", + "1000.0000", + ), + ( + SensorDeviceClass.DISTANCE, + UnitOfLength.KILOMETERS, + 1, + -0.04, + "-0.040", + "0.0", # Make sure minus is dropped + ), + ], +) +async def test_custom_precision_native_precision( + hass, + enable_custom_integrations, + device_class, + native_unit, + custom_precision, + native_value, + default_state, + custom_state, +): + """Test custom precision.""" + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get_or_create("sensor", "test", "very_unique") + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", + native_value=str(native_value), + native_precision=3, + native_unit_of_measurement=native_unit, + device_class=device_class, + unique_id="very_unique", + ) + + 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 == default_state + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit + + entity_registry.async_update_entity_options( + entry.entity_id, "sensor", {"precision": custom_precision} + ) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.state == custom_state + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit + + +@pytest.mark.parametrize( + "device_class,native_unit,custom_precision,native_value,custom_state", + [ + ( + SensorDeviceClass.ATMOSPHERIC_PRESSURE, + UnitOfPressure.HPA, + 4, + 1000.0, + "1000.0000", + ), + ], +) +async def test_custom_precision_no_native_precision( + hass, + enable_custom_integrations, + device_class, + native_unit, + custom_precision, + native_value, + custom_state, +): + """Test custom precision.""" + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get_or_create("sensor", "test", "very_unique") + entity_registry.async_update_entity_options( + entry.entity_id, "sensor", {"precision": custom_precision} + ) + await hass.async_block_till_done() + + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", + native_value=str(native_value), + native_unit_of_measurement=native_unit, + device_class=device_class, + unique_id="very_unique", + ) + + 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 == custom_state + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit + + +@pytest.mark.parametrize( + "native_unit, custom_unit, state_unit, native_value, native_state, custom_state, device_class", [ # Distance ( - LENGTH_KILOMETERS, - LENGTH_MILES, - LENGTH_MILES, + UnitOfLength.KILOMETERS, + UnitOfLength.MILES, + UnitOfLength.MILES, 1000, - 621, + "1000", + "621", SensorDeviceClass.DISTANCE, ), ( - LENGTH_CENTIMETERS, - LENGTH_INCHES, - LENGTH_INCHES, + UnitOfLength.CENTIMETERS, + UnitOfLength.INCHES, + UnitOfLength.INCHES, 7.24, - 2.85, + "7.24", + "2.85", SensorDeviceClass.DISTANCE, ), ( - LENGTH_KILOMETERS, + UnitOfLength.KILOMETERS, "peer_distance", - LENGTH_KILOMETERS, - 1000, + UnitOfLength.KILOMETERS, 1000, + "1000", + "1000", SensorDeviceClass.DISTANCE, ), + # Energy + ( + UnitOfEnergy.KILO_WATT_HOUR, + UnitOfEnergy.MEGA_WATT_HOUR, + UnitOfEnergy.MEGA_WATT_HOUR, + 1000, + "1000", + "1.000", + SensorDeviceClass.ENERGY, + ), + ( + UnitOfEnergy.GIGA_JOULE, + UnitOfEnergy.MEGA_WATT_HOUR, + UnitOfEnergy.MEGA_WATT_HOUR, + 1000, + "1000", + "278", + SensorDeviceClass.ENERGY, + ), + ( + UnitOfEnergy.KILO_WATT_HOUR, + "BTU", + UnitOfEnergy.KILO_WATT_HOUR, + 1000, + "1000", + "1000", + SensorDeviceClass.ENERGY, + ), + # Power factor + ( + None, + PERCENTAGE, + PERCENTAGE, + 1.0, + "1.0", + "100.0", + SensorDeviceClass.POWER_FACTOR, + ), + ( + PERCENTAGE, + None, + None, + 100, + "100", + "1.00", + SensorDeviceClass.POWER_FACTOR, + ), + ( + "Cos φ", + None, + "Cos φ", + 1.0, + "1.0", + "1.0", + SensorDeviceClass.POWER_FACTOR, + ), + # Pressure # Smaller to larger unit, InHg is ~33x larger than hPa -> 1 more decimal ( - PRESSURE_HPA, - PRESSURE_INHG, - PRESSURE_INHG, + UnitOfPressure.HPA, + UnitOfPressure.INHG, + UnitOfPressure.INHG, 1000.0, - 29.53, + "1000.0", + "29.53", SensorDeviceClass.PRESSURE, ), ( - PRESSURE_KPA, - PRESSURE_HPA, - PRESSURE_HPA, + UnitOfPressure.KPA, + UnitOfPressure.HPA, + UnitOfPressure.HPA, 1.234, - 12.34, + "1.234", + "12.340", SensorDeviceClass.PRESSURE, ), ( - PRESSURE_HPA, - PRESSURE_MMHG, - PRESSURE_MMHG, + UnitOfPressure.HPA, + UnitOfPressure.MMHG, + UnitOfPressure.MMHG, 1000, - 750, + "1000", + "750", SensorDeviceClass.PRESSURE, ), # Not a supported pressure unit ( - PRESSURE_HPA, + UnitOfPressure.HPA, "peer_pressure", - PRESSURE_HPA, - 1000, + UnitOfPressure.HPA, 1000, + "1000", + "1000", SensorDeviceClass.PRESSURE, ), # Speed ( - SPEED_KILOMETERS_PER_HOUR, - SPEED_MILES_PER_HOUR, - SPEED_MILES_PER_HOUR, + UnitOfSpeed.KILOMETERS_PER_HOUR, + UnitOfSpeed.MILES_PER_HOUR, + UnitOfSpeed.MILES_PER_HOUR, 100, - 62, + "100", + "62", SensorDeviceClass.SPEED, ), ( - SPEED_MILLIMETERS_PER_DAY, - SPEED_INCHES_PER_HOUR, - SPEED_INCHES_PER_HOUR, + UnitOfVolumetricFlux.MILLIMETERS_PER_DAY, + UnitOfVolumetricFlux.INCHES_PER_HOUR, + UnitOfVolumetricFlux.INCHES_PER_HOUR, 78, - 0.13, + "78", + "0.13", SensorDeviceClass.SPEED, ), ( - SPEED_KILOMETERS_PER_HOUR, + UnitOfSpeed.KILOMETERS_PER_HOUR, "peer_distance", - SPEED_KILOMETERS_PER_HOUR, - 100, + UnitOfSpeed.KILOMETERS_PER_HOUR, 100, + "100", + "100", SensorDeviceClass.SPEED, ), # Volume ( - VOLUME_CUBIC_METERS, - VOLUME_CUBIC_FEET, - VOLUME_CUBIC_FEET, + UnitOfVolume.CUBIC_METERS, + UnitOfVolume.CUBIC_FEET, + UnitOfVolume.CUBIC_FEET, 100, - 3531, + "100", + "3531", SensorDeviceClass.VOLUME, ), ( - VOLUME_LITERS, - VOLUME_FLUID_OUNCE, - VOLUME_FLUID_OUNCE, + UnitOfVolume.LITERS, + UnitOfVolume.FLUID_OUNCES, + UnitOfVolume.FLUID_OUNCES, 2.3, - 77.8, + "2.3", + "77.8", SensorDeviceClass.VOLUME, ), ( - VOLUME_CUBIC_METERS, + UnitOfVolume.CUBIC_METERS, "peer_distance", - VOLUME_CUBIC_METERS, - 100, + UnitOfVolume.CUBIC_METERS, 100, + "100", + "100", SensorDeviceClass.VOLUME, ), # Weight ( - MASS_GRAMS, - MASS_OUNCES, - MASS_OUNCES, + UnitOfMass.GRAMS, + UnitOfMass.OUNCES, + UnitOfMass.OUNCES, 100, - 3.5, + "100", + "3.5", SensorDeviceClass.WEIGHT, ), ( - MASS_OUNCES, - MASS_GRAMS, - MASS_GRAMS, + UnitOfMass.OUNCES, + UnitOfMass.GRAMS, + UnitOfMass.GRAMS, 78, - 2211, + "78", + "2211", SensorDeviceClass.WEIGHT, ), ( - MASS_GRAMS, + UnitOfMass.GRAMS, "peer_distance", - MASS_GRAMS, - 100, + UnitOfMass.GRAMS, 100, + "100", + "100", SensorDeviceClass.WEIGHT, ), ], @@ -618,7 +925,8 @@ async def test_custom_unit_change( custom_unit, state_unit, native_value, - custom_value, + native_state, + custom_state, device_class, ): """Test custom unit changes are picked up.""" @@ -638,8 +946,8 @@ async def test_custom_unit_change( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert float(state.state) == approx(float(native_value)) - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit + assert state.state == native_state + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == native_unit entity_registry.async_update_entity_options( "sensor.test", "sensor", {"unit_of_measurement": custom_unit} @@ -647,8 +955,8 @@ async def test_custom_unit_change( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert float(state.state) == approx(float(custom_value)) - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == state_unit + assert state.state == custom_state + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == state_unit entity_registry.async_update_entity_options( "sensor.test", "sensor", {"unit_of_measurement": native_unit} @@ -656,31 +964,32 @@ async def test_custom_unit_change( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert float(state.state) == approx(float(native_value)) - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit + assert state.state == native_state + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == native_unit entity_registry.async_update_entity_options("sensor.test", "sensor", None) await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert float(state.state) == approx(float(native_value)) - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit + assert state.state == native_state + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == native_unit @pytest.mark.parametrize( - "unit_system, native_unit, automatic_unit, suggested_unit, custom_unit, native_value, automatic_value, suggested_value, custom_value, device_class", + "unit_system, native_unit, automatic_unit, suggested_unit, custom_unit, native_value, native_state, automatic_state, suggested_state, custom_state, device_class", [ # Distance ( US_CUSTOMARY_SYSTEM, - LENGTH_KILOMETERS, - LENGTH_MILES, - LENGTH_METERS, - LENGTH_YARD, + UnitOfLength.KILOMETERS, + UnitOfLength.MILES, + UnitOfLength.METERS, + UnitOfLength.YARDS, 1000, - 621, - 1000000, - 1093613, + "1000", + "621", + "1000000", + "1093613", SensorDeviceClass.DISTANCE, ), ], @@ -694,9 +1003,10 @@ async def test_unit_conversion_priority( suggested_unit, custom_unit, native_value, - automatic_value, - suggested_value, - custom_value, + native_state, + automatic_state, + suggested_state, + custom_state, device_class, ): """Test priority of unit conversion.""" @@ -748,7 +1058,7 @@ async def test_unit_conversion_priority( # Registered entity -> Follow automatic unit conversion state = hass.states.get(entity0.entity_id) - assert float(state.state) == approx(float(automatic_value)) + assert state.state == automatic_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == automatic_unit # Assert the automatic unit conversion is stored in the registry entry = entity_registry.async_get(entity0.entity_id) @@ -758,12 +1068,12 @@ async def test_unit_conversion_priority( # Unregistered entity -> Follow native unit state = hass.states.get(entity1.entity_id) - assert float(state.state) == approx(float(native_value)) + assert state.state == native_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == native_unit # Registered entity with suggested unit state = hass.states.get(entity2.entity_id) - assert float(state.state) == approx(float(suggested_value)) + assert state.state == suggested_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit # Assert the suggested unit is stored in the registry entry = entity_registry.async_get(entity2.entity_id) @@ -773,7 +1083,7 @@ async def test_unit_conversion_priority( # Unregistered entity with suggested unit state = hass.states.get(entity3.entity_id) - assert float(state.state) == approx(float(suggested_value)) + assert state.state == suggested_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == suggested_unit # Set a custom unit, this should have priority over the automatic unit conversion @@ -783,7 +1093,7 @@ async def test_unit_conversion_priority( await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - assert float(state.state) == approx(float(custom_value)) + assert state.state == custom_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit entity_registry.async_update_entity_options( @@ -792,7 +1102,7 @@ async def test_unit_conversion_priority( await hass.async_block_till_done() state = hass.states.get(entity2.entity_id) - assert float(state.state) == approx(float(custom_value)) + assert state.state == custom_state assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == custom_unit @@ -802,9 +1112,9 @@ async def test_unit_conversion_priority( # Distance ( US_CUSTOMARY_SYSTEM, - LENGTH_KILOMETERS, - LENGTH_YARD, - LENGTH_METERS, + UnitOfLength.KILOMETERS, + UnitOfLength.YARDS, + UnitOfLength.METERS, 1000, 1093613, SensorDeviceClass.DISTANCE, @@ -883,16 +1193,16 @@ async def test_unit_conversion_priority_suggested_unit_change( # Distance ( US_CUSTOMARY_SYSTEM, - LENGTH_KILOMETERS, - LENGTH_MILES, + UnitOfLength.KILOMETERS, + UnitOfLength.MILES, 1000, - 621, + 621.0, SensorDeviceClass.DISTANCE, ), ( US_CUSTOMARY_SYSTEM, - LENGTH_METERS, - LENGTH_MILES, + UnitOfLength.METERS, + UnitOfLength.MILES, 1000000, 621.371, SensorDeviceClass.DISTANCE, @@ -1018,40 +1328,6 @@ async def test_invalid_enumeration_entity_without_device_class( ) in caplog.text -@pytest.mark.parametrize( - "device_class", - ( - SensorDeviceClass.DATE, - SensorDeviceClass.ENUM, - SensorDeviceClass.TIMESTAMP, - ), -) -async def test_non_numeric_device_class_with_state_class( - hass: HomeAssistant, - caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, - device_class: SensorDeviceClass, -): - """Test error on numeric entities that provide an state class.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( - name="Test", - native_value=None, - device_class=device_class, - state_class=SensorStateClass.MEASUREMENT, - options=["option1", "option2"], - ) - - assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) - await hass.async_block_till_done() - - assert ( - "Sensor sensor.test has a state class and thus indicating it has a numeric " - f"value; however, it has the non-numeric device class: {device_class}" - ) in caplog.text - - @pytest.mark.parametrize( "device_class", ( @@ -1148,11 +1424,220 @@ async def test_device_classes_with_invalid_unit_of_measurement( device_class=device_class, native_unit_of_measurement="INVALID!", ) - + units = [ + str(unit) if unit else "no unit of measurement" + for unit in DEVICE_CLASS_UNITS.get(device_class, set()) + ] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() assert ( "is using native unit of measurement 'INVALID!' which is not a valid " - f"unit for the device class ('{device_class}') it is using" + f"unit for the device class ('{device_class}') it is using; " + f"expected one of {units}" + ) in caplog.text + + +@pytest.mark.parametrize( + "device_class,state_class,unit", + [ + (SensorDeviceClass.AQI, None, None), + (None, SensorStateClass.MEASUREMENT, None), + (None, None, UnitOfTemperature.CELSIUS), + ], +) +@pytest.mark.parametrize( + "native_value,expected", + [ + ("abc", "abc"), + ("13.7.1", "13.7.1"), + (datetime(2012, 11, 10, 7, 35, 1), "2012-11-10 07:35:01"), + (date(2012, 11, 10), "2012-11-10"), + ], +) +async def test_non_numeric_validation_warn( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, + native_value: Any, + expected: str, + device_class: SensorDeviceClass | None, + state_class: SensorStateClass | None, + unit: str | None, +) -> None: + """Test error on expected numeric entities.""" + 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, + native_unit_of_measurement=unit, + state_class=state_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 == expected + + assert ( + "thus indicating it has a numeric value; " + f"however, it has the non-numeric value: {native_value}" + ) in caplog.text + + +@pytest.mark.parametrize( + "device_class,state_class,unit,precision", ((None, None, None, 1),) +) +@pytest.mark.parametrize( + "native_value,expected", + [ + ("abc", "abc"), + ("13.7.1", "13.7.1"), + (datetime(2012, 11, 10, 7, 35, 1), "2012-11-10 07:35:01"), + (date(2012, 11, 10), "2012-11-10"), + ], +) +async def test_non_numeric_validation_raise( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, + native_value: Any, + expected: str, + device_class: SensorDeviceClass | None, + state_class: SensorStateClass | None, + unit: str | None, + precision, +) -> None: + """Test error on expected numeric entities.""" + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", + device_class=device_class, + native_precision=precision, + native_unit_of_measurement=unit, + native_value=native_value, + state_class=state_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 is None + + assert ("Error adding entities for domain sensor with platform test") in caplog.text + + +@pytest.mark.parametrize( + "device_class,state_class,unit", + [ + (SensorDeviceClass.AQI, None, None), + (None, SensorStateClass.MEASUREMENT, None), + (None, None, UnitOfTemperature.CELSIUS), + ], +) +@pytest.mark.parametrize( + "native_value,expected", + [ + (13, "13"), + (17.50, "17.5"), + (Decimal(18.50), "18.5"), + ("19.70", "19.70"), + (None, STATE_UNKNOWN), + ], +) +async def test_numeric_validation( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, + native_value: Any, + expected: str, + device_class: SensorDeviceClass | None, + state_class: SensorStateClass | None, + unit: str | None, +) -> None: + """Test does not error on expected numeric entities.""" + 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, + native_unit_of_measurement=unit, + state_class=state_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 == expected + + assert ( + "thus indicating it has a numeric value; " + f"however, it has the non-numeric value: {native_value}" + ) not in caplog.text + + +async def test_numeric_validation_ignores_custom_device_class( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, +) -> None: + """Test does not error on expected numeric entities.""" + native_value = "Three elephants" + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", + native_value=native_value, + device_class="custom__deviceclass", + ) + 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 == "Three elephants" + + assert ( + "thus indicating it has a numeric value; " + f"however, it has the non-numeric value: {native_value}" + ) not in caplog.text + + +@pytest.mark.parametrize( + "device_class", + list(SensorDeviceClass), +) +async def test_device_classes_with_invalid_state_class( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, + device_class: SensorDeviceClass, +): + """Test error when unit of measurement is not valid for used device class.""" + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", + native_value=None, + state_class="INVALID!", + device_class=device_class, + ) + + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + assert ( + "is using state class 'INVALID!' which is impossible considering device " + f"class ('{device_class}') it is using" ) in caplog.text diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index a252a85b8d7..506216df9d4 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -1,5 +1,5 @@ """The tests for sensor recorder platform.""" -# pylint: disable=protected-access,invalid-name +# pylint: disable=invalid-name from datetime import datetime, timedelta import math from statistics import mean @@ -94,13 +94,13 @@ def set_time_zone(): @pytest.mark.parametrize( "device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max", [ - (None, "%", "%", "%", None, 13.050847, -10, 30), - ("battery", "%", "%", "%", None, 13.050847, -10, 30), - ("battery", None, None, None, None, 13.050847, -10, 30), + (None, "%", "%", "%", "unitless", 13.050847, -10, 30), + ("battery", "%", "%", "%", "unitless", 13.050847, -10, 30), + ("battery", None, None, None, "unitless", 13.050847, -10, 30), ("distance", "m", "m", "m", "distance", 13.050847, -10, 30), ("distance", "mi", "mi", "mi", "distance", 13.050847, -10, 30), - ("humidity", "%", "%", "%", None, 13.050847, -10, 30), - ("humidity", None, None, None, None, 13.050847, -10, 30), + ("humidity", "%", "%", "%", "unitless", 13.050847, -10, 30), + ("humidity", None, None, None, "unitless", 13.050847, -10, 30), ("pressure", "Pa", "Pa", "Pa", "pressure", 13.050847, -10, 30), ("pressure", "hPa", "hPa", "hPa", "pressure", 13.050847, -10, 30), ("pressure", "mbar", "mbar", "mbar", "pressure", 13.050847, -10, 30), @@ -178,7 +178,7 @@ def test_compile_hourly_statistics( @pytest.mark.parametrize( "device_class, state_unit, display_unit, statistics_unit, unit_class", [ - (None, "%", "%", "%", None), + (None, "%", "%", "%", "unitless"), ], ) def test_compile_hourly_statistics_purged_state_changes( @@ -317,7 +317,7 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes) "source": "recorder", "statistic_id": "sensor.test3", "statistics_unit_of_measurement": None, - "unit_class": None, + "unit_class": "unitless", }, { "statistic_id": "sensor.test6", @@ -1775,8 +1775,8 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog): @pytest.mark.parametrize( "state_class, device_class, state_unit, display_unit, statistics_unit, unit_class, statistic_type", [ - ("measurement", "battery", "%", "%", "%", None, "mean"), - ("measurement", "battery", None, None, None, None, "mean"), + ("measurement", "battery", "%", "%", "%", "unitless", "mean"), + ("measurement", "battery", None, None, None, "unitless", "mean"), ("measurement", "distance", "m", "m", "m", "distance", "mean"), ("measurement", "distance", "mi", "mi", "mi", "distance", "mean"), ("total", "distance", "m", "m", "m", "distance", "sum"), @@ -1785,8 +1785,8 @@ def test_compile_hourly_statistics_fails(hass_recorder, caplog): ("total", "energy", "kWh", "kWh", "kWh", "energy", "sum"), ("measurement", "energy", "Wh", "Wh", "Wh", "energy", "mean"), ("measurement", "energy", "kWh", "kWh", "kWh", "energy", "mean"), - ("measurement", "humidity", "%", "%", "%", None, "mean"), - ("measurement", "humidity", None, None, None, None, "mean"), + ("measurement", "humidity", "%", "%", "%", "unitless", "mean"), + ("measurement", "humidity", None, None, None, "unitless", "mean"), ("total", "monetary", "USD", "USD", "USD", None, "sum"), ("total", "monetary", "None", "None", "None", None, "sum"), ("total", "gas", "m³", "m³", "m³", "volume", "sum"), @@ -1898,10 +1898,10 @@ def test_list_statistic_ids_unsupported(hass_recorder, caplog, _attributes): @pytest.mark.parametrize( "device_class, state_unit, state_unit2, unit_class, mean, min, max", [ - (None, None, "cats", None, 13.050847, -10, 30), - (None, "%", "cats", None, 13.050847, -10, 30), - ("battery", "%", "cats", None, 13.050847, -10, 30), - ("battery", None, "cats", None, 13.050847, -10, 30), + (None, None, "cats", "unitless", 13.050847, -10, 30), + (None, "%", "cats", "unitless", 13.050847, -10, 30), + ("battery", "%", "cats", "unitless", 13.050847, -10, 30), + ("battery", None, "cats", "unitless", 13.050847, -10, 30), (None, "kW", "Wh", "power", 13.050847, -10, 30), # Can't downgrade from ft³ to ft3 or from m³ to m3 (None, "ft³", "ft3", "volume", 13.050847, -10, 30), @@ -1919,7 +1919,10 @@ def test_compile_hourly_statistics_changing_units_1( min, max, ): - """Test compiling hourly statistics where units change from one hour to the next.""" + """Test compiling hourly statistics where units change from one hour to the next. + + This tests the case where the recorder can not convert between the units. + """ zero = dt_util.utcnow() hass = hass_recorder() setup_component(hass, "sensor", {}) @@ -2014,10 +2017,7 @@ def test_compile_hourly_statistics_changing_units_1( @pytest.mark.parametrize( "device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max", [ - (None, None, None, None, None, 13.050847, -10, 30), - (None, "%", "%", "%", None, 13.050847, -10, 30), - ("battery", "%", "%", "%", None, 13.050847, -10, 30), - ("battery", None, None, None, None, 13.050847, -10, 30), + (None, "dogs", "dogs", "dogs", None, 13.050847, -10, 30), ], ) def test_compile_hourly_statistics_changing_units_2( @@ -2032,7 +2032,11 @@ def test_compile_hourly_statistics_changing_units_2( min, max, ): - """Test compiling hourly statistics where units change during an hour.""" + """Test compiling hourly statistics where units change during an hour. + + This tests the behaviour when the sensor units are note supported by any unit + converter. + """ zero = dt_util.utcnow() hass = hass_recorder() setup_component(hass, "sensor", {}) @@ -2077,10 +2081,7 @@ def test_compile_hourly_statistics_changing_units_2( @pytest.mark.parametrize( "device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max", [ - (None, None, None, None, None, 13.050847, -10, 30), - (None, "%", "%", "%", None, 13.050847, -10, 30), - ("battery", "%", "%", "%", None, 13.050847, -10, 30), - ("battery", None, None, None, None, 13.050847, -10, 30), + (None, "dogs", "dogs", "dogs", None, 13.050847, -10, 30), ], ) def test_compile_hourly_statistics_changing_units_3( @@ -2095,7 +2096,11 @@ def test_compile_hourly_statistics_changing_units_3( min, max, ): - """Test compiling hourly statistics where units change from one hour to the next.""" + """Test compiling hourly statistics where units change from one hour to the next. + + This tests the behaviour when the sensor units are note supported by any unit + converter. + """ zero = dt_util.utcnow() hass = hass_recorder() setup_component(hass, "sensor", {}) @@ -2187,6 +2192,132 @@ def test_compile_hourly_statistics_changing_units_3( assert "Error while processing event StatisticsTask" not in caplog.text +@pytest.mark.parametrize( + "state_unit_1, state_unit_2, unit_class, mean, min, max, factor", + [ + (None, "%", "unitless", 13.050847, -10, 30, 100), + ("%", None, "unitless", 13.050847, -10, 30, 0.01), + ("W", "kW", "power", 13.050847, -10, 30, 0.001), + ("kW", "W", "power", 13.050847, -10, 30, 1000), + ], +) +def test_compile_hourly_statistics_convert_units_1( + hass_recorder, + caplog, + state_unit_1, + state_unit_2, + unit_class, + mean, + min, + max, + factor, +): + """Test compiling hourly statistics where units change from one hour to the next. + + This tests the case where the recorder can convert between the units. + """ + zero = dt_util.utcnow() + hass = hass_recorder() + setup_component(hass, "sensor", {}) + wait_recording_done(hass) # Wait for the sensor recorder platform to be added + attributes = { + "device_class": None, + "state_class": "measurement", + "unit_of_measurement": state_unit_1, + } + four, states = record_states(hass, zero, "sensor.test1", attributes) + four, _states = record_states( + hass, zero + timedelta(minutes=5), "sensor.test1", attributes, seq=[0, 1, None] + ) + states["sensor.test1"] += _states["sensor.test1"] + + do_adhoc_statistics(hass, start=zero) + wait_recording_done(hass) + assert "does not match the unit of already compiled" not in caplog.text + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + { + "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit_1, + "has_mean": True, + "has_sum": False, + "name": None, + "source": "recorder", + "statistics_unit_of_measurement": state_unit_1, + "unit_class": unit_class, + }, + ] + stats = statistics_during_period(hass, zero, period="5minute") + assert stats == { + "sensor.test1": [ + { + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), + "mean": approx(mean), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + attributes["unit_of_measurement"] = state_unit_2 + four, _states = record_states( + hass, zero + timedelta(minutes=10), "sensor.test1", attributes + ) + states["sensor.test1"] += _states["sensor.test1"] + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) + wait_recording_done(hass) + assert "The unit of sensor.test1 is changing" not in caplog.text + assert ( + f"matches the unit of already compiled statistics ({state_unit_1})" + not in caplog.text + ) + statistic_ids = list_statistic_ids(hass) + assert statistic_ids == [ + { + "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit_2, + "has_mean": True, + "has_sum": False, + "name": None, + "source": "recorder", + "statistics_unit_of_measurement": state_unit_1, + "unit_class": unit_class, + }, + ] + stats = statistics_during_period(hass, zero, period="5minute") + assert stats == { + "sensor.test1": [ + { + "start": process_timestamp(zero), + "end": process_timestamp(zero + timedelta(minutes=5)), + "mean": approx(mean * factor), + "min": approx(min * factor), + "max": approx(max * factor), + "last_reset": None, + "state": None, + "sum": None, + }, + { + "start": process_timestamp(zero + timedelta(minutes=10)), + "end": process_timestamp(zero + timedelta(minutes=15)), + "mean": approx(mean), + "min": approx(min), + "max": approx(max), + "last_reset": None, + "state": None, + "sum": None, + }, + ] + } + assert "Error while processing event StatisticsTask" not in caplog.text + + @pytest.mark.parametrize( "device_class, state_unit, state_unit2, unit_class, unit_class2, mean, mean2, min, max", [ @@ -2400,7 +2531,10 @@ def test_compile_hourly_statistics_changing_device_class_1( min, max, ): - """Test compiling hourly statistics where device class changes from one hour to the next.""" + """Test compiling hourly statistics where device class changes from one hour to the next. + + Device class is ignored, meaning changing device class should not influence the statistics. + """ zero = dt_util.utcnow() hass = hass_recorder() setup_component(hass, "sensor", {}) @@ -2586,7 +2720,10 @@ def test_compile_hourly_statistics_changing_device_class_2( min, max, ): - """Test compiling hourly statistics where device class changes from one hour to the next.""" + """Test compiling hourly statistics where device class changes from one hour to the next. + + Device class is ignored, meaning changing device class should not influence the statistics. + """ zero = dt_util.utcnow() hass = hass_recorder() setup_component(hass, "sensor", {}) @@ -2692,10 +2829,10 @@ def test_compile_hourly_statistics_changing_device_class_2( @pytest.mark.parametrize( "device_class, state_unit, display_unit, statistics_unit, unit_class, mean, min, max", [ - (None, None, None, None, None, 13.050847, -10, 30), + (None, None, None, None, "unitless", 13.050847, -10, 30), ], ) -def test_compile_hourly_statistics_changing_statistics( +def test_compile_hourly_statistics_changing_state_class( hass_recorder, caplog, device_class, @@ -2707,7 +2844,7 @@ def test_compile_hourly_statistics_changing_statistics( min, max, ): - """Test compiling hourly statistics where units change during an hour.""" + """Test compiling hourly statistics where state class changes.""" period0 = dt_util.utcnow() period0_end = period1 = period0 + timedelta(minutes=5) period1_end = period0 + timedelta(minutes=10) @@ -2737,7 +2874,7 @@ def test_compile_hourly_statistics_changing_statistics( "name": None, "source": "recorder", "statistics_unit_of_measurement": None, - "unit_class": None, + "unit_class": unit_class, }, ] metadata = get_metadata(hass, statistic_ids=("sensor.test1",)) @@ -2773,7 +2910,7 @@ def test_compile_hourly_statistics_changing_statistics( "name": None, "source": "recorder", "statistics_unit_of_measurement": None, - "unit_class": None, + "unit_class": unit_class, }, ] metadata = get_metadata(hass, statistic_ids=("sensor.test1",)) @@ -2965,7 +3102,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): "name": None, "source": "recorder", "statistics_unit_of_measurement": "%", - "unit_class": None, + "unit_class": "unitless", }, { "statistic_id": "sensor.test2", @@ -2975,7 +3112,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): "name": None, "source": "recorder", "statistics_unit_of_measurement": "%", - "unit_class": None, + "unit_class": "unitless", }, { "statistic_id": "sensor.test3", @@ -2985,7 +3122,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): "name": None, "source": "recorder", "statistics_unit_of_measurement": "%", - "unit_class": None, + "unit_class": "unitless", }, { "statistic_id": "sensor.test4", @@ -3496,6 +3633,13 @@ async def test_validate_statistics_unit_ignore_device_class( "bar", "Pa, bar, cbar, hPa, inHg, kPa, mbar, mmHg, psi", ), + ( + METRIC_SYSTEM, + BATTERY_SENSOR_ATTRIBUTES, + "%", + None, + "%, ", + ), ], ) async def test_validate_statistics_unit_change_no_device_class( @@ -3851,8 +3995,8 @@ async def test_validate_statistics_sensor_removed( @pytest.mark.parametrize( "attributes, unit1, unit2", [ - (BATTERY_SENSOR_ATTRIBUTES, "%", "dogs"), - (NONE_SENSOR_ATTRIBUTES, None, "dogs"), + (BATTERY_SENSOR_ATTRIBUTES, "cats", "dogs"), + (NONE_SENSOR_ATTRIBUTES, "cats", "dogs"), ], ) async def test_validate_statistics_unit_change_no_conversion( diff --git a/tests/components/sensor/test_significant_change.py b/tests/components/sensor/test_significant_change.py index bfa01d6eb08..bac3d05b02e 100644 --- a/tests/components/sensor/test_significant_change.py +++ b/tests/components/sensor/test_significant_change.py @@ -5,8 +5,7 @@ from homeassistant.components.sensor import SensorDeviceClass, significant_chang from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfTemperature, ) AQI_ATTRS = { @@ -23,12 +22,12 @@ HUMIDITY_ATTRS = { TEMP_CELSIUS_ATTRS = { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, } TEMP_FREEDOM_ATTRS = { ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, - ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT, } diff --git a/tests/components/sensor/test_websocket_api.py b/tests/components/sensor/test_websocket_api.py new file mode 100644 index 00000000000..c2187d5fe7c --- /dev/null +++ b/tests/components/sensor/test_websocket_api.py @@ -0,0 +1,53 @@ +"""Test the sensor websocket API.""" +from pytest_unordered import unordered + +from homeassistant.components.sensor.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def test_device_class_units(hass: HomeAssistant, hass_ws_client) -> None: + """Test we can get supported units.""" + assert await async_setup_component(hass, DOMAIN, {}) + + client = await hass_ws_client(hass) + + # Device class with units which sensor allows customizing & converting + await client.send_json( + { + "id": 1, + "type": "sensor/device_class_convertible_units", + "device_class": "speed", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == { + "units": unordered( + ["km/h", "kn", "mph", "in/h", "in/d", "ft/s", "mm/d", "mm/h", "m/s"] + ) + } + + # Device class with units which sensor doesn't allow customizing & converting + await client.send_json( + { + "id": 2, + "type": "sensor/device_class_convertible_units", + "device_class": "power", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"units": []} + + # Unknown device class + await client.send_json( + { + "id": 3, + "type": "sensor/device_class_convertible_units", + "device_class": "kebabsås", + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"units": unordered([])} diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py index 984c486c69e..34ed1d2c4b5 100644 --- a/tests/components/sentry/test_config_flow.py +++ b/tests/components/sentry/test_config_flow.py @@ -30,7 +30,6 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: ) assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} - assert "flow_id" in result with patch("homeassistant.components.sentry.config_flow.Dsn"), patch( "homeassistant.components.sentry.async_setup_entry", @@ -67,7 +66,6 @@ async def test_user_flow_bad_dsn(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert "flow_id" in result with patch( "homeassistant.components.sentry.config_flow.Dsn", @@ -87,7 +85,6 @@ async def test_user_flow_unknown_exception(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert "flow_id" in result with patch( "homeassistant.components.sentry.config_flow.Dsn", @@ -118,7 +115,6 @@ async def test_options_flow(hass: HomeAssistant) -> None: assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" - assert "flow_id" in result result = await hass.config_entries.options.async_configure( result["flow_id"], diff --git a/tests/components/sfr_box/__init__.py b/tests/components/sfr_box/__init__.py new file mode 100644 index 00000000000..90ab88c7c9c --- /dev/null +++ b/tests/components/sfr_box/__init__.py @@ -0,0 +1,53 @@ +"""Tests for the SFR Box integration.""" +from __future__ import annotations + +from types import MappingProxyType + +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_IDENTIFIERS, + ATTR_MODEL, + ATTR_NAME, + ATTR_STATE, + ATTR_SW_VERSION, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceRegistry +from homeassistant.helpers.entity_registry import EntityRegistry + +from .const import ATTR_UNIQUE_ID, FIXED_ATTRIBUTES + + +def check_device_registry( + device_registry: DeviceRegistry, expected_device: MappingProxyType +) -> None: + """Ensure that the expected_device is correctly registered.""" + assert len(device_registry.devices) == 1 + registry_entry = device_registry.async_get_device(expected_device[ATTR_IDENTIFIERS]) + assert registry_entry is not None + assert registry_entry.identifiers == expected_device[ATTR_IDENTIFIERS] + assert registry_entry.name == expected_device[ATTR_NAME] + assert registry_entry.model == expected_device[ATTR_MODEL] + assert registry_entry.sw_version == expected_device[ATTR_SW_VERSION] + + +def check_entities( + hass: HomeAssistant, + entity_registry: EntityRegistry, + expected_entities: MappingProxyType, +) -> None: + """Ensure that the expected_entities are correct.""" + for expected_entity in expected_entities: + entity_id = expected_entity[ATTR_ENTITY_ID] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity[ATTR_UNIQUE_ID] + state = hass.states.get(entity_id) + assert state, f"Expected valid state for {entity_id}, got {state}" + assert ( + state.state == expected_entity[ATTR_STATE] + ), f"Expected state {expected_entity[ATTR_STATE]}, got {state.state} for {entity_id}" + for attr in FIXED_ATTRIBUTES: + assert state.attributes.get(attr) == expected_entity.get( + attr + ), f"Expected attribute {attr} == {expected_entity.get(attr)}, got {state.attributes.get(attr)} for {entity_id}" diff --git a/tests/components/sfr_box/conftest.py b/tests/components/sfr_box/conftest.py new file mode 100644 index 00000000000..922becf9bde --- /dev/null +++ b/tests/components/sfr_box/conftest.py @@ -0,0 +1,70 @@ +"""Provide common SFR Box fixtures.""" +from collections.abc import Generator +import json +from unittest.mock import patch + +import pytest +from sfrbox_api.models import DslInfo, SystemInfo + +from homeassistant.components.sfr_box.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER, ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture(name="config_entry") +def get_config_entry(hass: HomeAssistant) -> ConfigEntry: + """Create and register mock config entry.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={CONF_HOST: "192.168.0.1"}, + unique_id="e4:5d:51:00:11:22", + options={}, + entry_id="123456", + ) + config_entry.add_to_hass(hass) + return config_entry + + +@pytest.fixture(name="config_entry_with_auth") +def get_config_entry_with_auth(hass: HomeAssistant) -> ConfigEntry: + """Create and register mock config entry.""" + config_entry_with_auth = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data={ + CONF_HOST: "192.168.0.1", + CONF_USERNAME: "admin", + CONF_PASSWORD: "password", + }, + unique_id="e4:5d:51:00:11:23", + options={}, + entry_id="1234567", + ) + config_entry_with_auth.add_to_hass(hass) + return config_entry_with_auth + + +@pytest.fixture() +def system_get_info() -> Generator[SystemInfo, None, None]: + """Fixture for SFRBox.system_get_info.""" + system_info = SystemInfo(**json.loads(load_fixture("system_getInfo.json", DOMAIN))) + with patch( + "homeassistant.components.sfr_box.coordinator.SFRBox.system_get_info", + return_value=system_info, + ): + yield system_info + + +@pytest.fixture() +def dsl_get_info() -> Generator[DslInfo, None, None]: + """Fixture for SFRBox.dsl_get_info.""" + dsl_info = DslInfo(**json.loads(load_fixture("dsl_getInfo.json", DOMAIN))) + with patch( + "homeassistant.components.sfr_box.coordinator.SFRBox.dsl_get_info", + return_value=dsl_info, + ): + yield dsl_info diff --git a/tests/components/sfr_box/const.py b/tests/components/sfr_box/const.py new file mode 100644 index 00000000000..8b7513aaf8c --- /dev/null +++ b/tests/components/sfr_box/const.py @@ -0,0 +1,191 @@ +"""Constants for SFR Box tests.""" +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.components.button import ButtonDeviceClass +from homeassistant.components.sensor import ( + ATTR_OPTIONS, + ATTR_STATE_CLASS, + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.components.sfr_box.const import DOMAIN +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_IDENTIFIERS, + ATTR_MODEL, + ATTR_NAME, + ATTR_STATE, + ATTR_SW_VERSION, + ATTR_UNIT_OF_MEASUREMENT, + SIGNAL_STRENGTH_DECIBELS, + STATE_ON, + STATE_UNKNOWN, + Platform, + UnitOfDataRate, + UnitOfElectricPotential, + UnitOfTemperature, +) + +ATTR_DEFAULT_DISABLED = "default_disabled" +ATTR_UNIQUE_ID = "unique_id" +FIXED_ATTRIBUTES = ( + ATTR_DEVICE_CLASS, + ATTR_OPTIONS, + ATTR_STATE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, +) + +EXPECTED_ENTITIES = { + "expected_device": { + ATTR_IDENTIFIERS: {(DOMAIN, "e4:5d:51:00:11:22")}, + ATTR_MODEL: "NB6VAC-FXC-r0", + ATTR_NAME: "SFR Box", + ATTR_SW_VERSION: "NB6VAC-MAIN-R4.0.44k", + }, + Platform.BINARY_SENSOR: [ + { + ATTR_DEVICE_CLASS: BinarySensorDeviceClass.CONNECTIVITY, + ATTR_ENTITY_ID: "binary_sensor.sfr_box_status", + ATTR_STATE: STATE_ON, + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_status", + }, + ], + Platform.BUTTON: [ + { + ATTR_DEVICE_CLASS: ButtonDeviceClass.RESTART, + ATTR_ENTITY_ID: "button.sfr_box_reboot", + ATTR_STATE: STATE_UNKNOWN, + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_system_reboot", + }, + ], + Platform.SENSOR: [ + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM, + ATTR_ENTITY_ID: "sensor.sfr_box_network_infrastructure", + ATTR_OPTIONS: ["adsl", "ftth", "gprs", "unknown"], + ATTR_STATE: "adsl", + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_system_net_infra", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, + ATTR_ENTITY_ID: "sensor.sfr_box_temperature", + ATTR_STATE: "27.56", + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_system_temperature", + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.VOLTAGE, + ATTR_ENTITY_ID: "sensor.sfr_box_voltage", + ATTR_STATE: "12251", + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_system_alimvoltage", + ATTR_UNIT_OF_MEASUREMENT: UnitOfElectricPotential.MILLIVOLT, + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_ID: "sensor.sfr_box_line_mode", + ATTR_STATE: "ADSL2+", + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_linemode", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_ID: "sensor.sfr_box_counter", + ATTR_STATE: "16", + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_counter", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_ENTITY_ID: "sensor.sfr_box_crc", + ATTR_STATE: "0", + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_crc", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.SIGNAL_STRENGTH, + ATTR_ENTITY_ID: "sensor.sfr_box_noise_down", + ATTR_STATE: "5.8", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_noise_down", + ATTR_UNIT_OF_MEASUREMENT: SIGNAL_STRENGTH_DECIBELS, + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.SIGNAL_STRENGTH, + ATTR_ENTITY_ID: "sensor.sfr_box_noise_up", + ATTR_STATE: "6.0", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_noise_up", + ATTR_UNIT_OF_MEASUREMENT: SIGNAL_STRENGTH_DECIBELS, + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.SIGNAL_STRENGTH, + ATTR_ENTITY_ID: "sensor.sfr_box_attenuation_down", + ATTR_STATE: "28.5", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_attenuation_down", + ATTR_UNIT_OF_MEASUREMENT: SIGNAL_STRENGTH_DECIBELS, + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.SIGNAL_STRENGTH, + ATTR_ENTITY_ID: "sensor.sfr_box_attenuation_up", + ATTR_STATE: "20.8", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_attenuation_up", + ATTR_UNIT_OF_MEASUREMENT: SIGNAL_STRENGTH_DECIBELS, + }, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.DATA_RATE, + ATTR_ENTITY_ID: "sensor.sfr_box_rate_down", + ATTR_STATE: "5549", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_rate_down", + ATTR_UNIT_OF_MEASUREMENT: UnitOfDataRate.KILOBITS_PER_SECOND, + }, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.DATA_RATE, + ATTR_ENTITY_ID: "sensor.sfr_box_rate_up", + ATTR_STATE: "187", + ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_rate_up", + ATTR_UNIT_OF_MEASUREMENT: UnitOfDataRate.KILOBITS_PER_SECOND, + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM, + ATTR_ENTITY_ID: "sensor.sfr_box_line_status", + ATTR_OPTIONS: [ + "no_defect", + "of_frame", + "loss_of_signal", + "loss_of_power", + "loss_of_signal_quality", + "unknown", + ], + ATTR_STATE: "no_defect", + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_line_status", + }, + { + ATTR_DEFAULT_DISABLED: True, + ATTR_DEVICE_CLASS: SensorDeviceClass.ENUM, + ATTR_ENTITY_ID: "sensor.sfr_box_training", + ATTR_OPTIONS: [ + "idle", + "g_994_training", + "g_992_started", + "g_922_channel_analysis", + "g_992_message_exchange", + "g_993_started", + "g_993_channel_analysis", + "g_993_message_exchange", + "showtime", + "unknown", + ], + ATTR_STATE: "showtime", + ATTR_UNIQUE_ID: "e4:5d:51:00:11:22_dsl_training", + }, + ], +} diff --git a/tests/components/sfr_box/fixtures/dsl_getInfo.json b/tests/components/sfr_box/fixtures/dsl_getInfo.json new file mode 100644 index 00000000000..be64186a97b --- /dev/null +++ b/tests/components/sfr_box/fixtures/dsl_getInfo.json @@ -0,0 +1,15 @@ +{ + "linemode": "ADSL2+", + "uptime": 450796, + "counter": 16, + "crc": 0, + "status": "up", + "noise_down": 5.8, + "noise_up": 6.0, + "attenuation_down": 28.5, + "attenuation_up": 20.8, + "rate_down": 5549, + "rate_up": 187, + "line_status": "No Defect", + "training": "Showtime" +} diff --git a/tests/components/sfr_box/fixtures/system_getInfo.json b/tests/components/sfr_box/fixtures/system_getInfo.json new file mode 100644 index 00000000000..ba8a424934b --- /dev/null +++ b/tests/components/sfr_box/fixtures/system_getInfo.json @@ -0,0 +1,17 @@ +{ + "product_id": "NB6VAC-FXC-r0", + "mac_addr": "e4:5d:51:00:11:22", + "net_mode": "router", + "net_infra": "adsl", + "uptime": 2353575, + "version_mainfirmware": "NB6VAC-MAIN-R4.0.44k", + "version_rescuefirmware": "NB6VAC-MAIN-R4.0.44k", + "version_bootloader": "NB6VAC-BOOTLOADER-R4.0.8", + "version_dsldriver": "NB6VAC-XDSL-A2pv6F039p", + "current_datetime": "202212282233", + "refclient": "", + "idur": "RP3P85K", + "alimvoltage": 12251, + "temperature": 27560, + "serial_number": "XU1001001001001001" +} diff --git a/tests/components/sfr_box/test_binary_sensor.py b/tests/components/sfr_box/test_binary_sensor.py new file mode 100644 index 00000000000..7cc60c4c537 --- /dev/null +++ b/tests/components/sfr_box/test_binary_sensor.py @@ -0,0 +1,39 @@ +"""Test the SFR Box sensors.""" +from collections.abc import Generator +from unittest.mock import patch + +import pytest + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from . import check_device_registry, check_entities +from .const import EXPECTED_ENTITIES + +from tests.common import mock_device_registry, mock_registry + +pytestmark = pytest.mark.usefixtures("system_get_info", "dsl_get_info") + + +@pytest.fixture(autouse=True) +def override_platforms() -> Generator[None, None, None]: + """Override PLATFORMS.""" + with patch("homeassistant.components.sfr_box.PLATFORMS", [Platform.BINARY_SENSOR]): + yield + + +async def test_binary_sensors(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Test for SFR Box binary sensors.""" + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + check_device_registry(device_registry, EXPECTED_ENTITIES["expected_device"]) + + expected_entities = EXPECTED_ENTITIES[Platform.BINARY_SENSOR] + assert len(entity_registry.entities) == len(expected_entities) + + check_entities(hass, entity_registry, expected_entities) diff --git a/tests/components/sfr_box/test_button.py b/tests/components/sfr_box/test_button.py new file mode 100644 index 00000000000..9872e39d3c4 --- /dev/null +++ b/tests/components/sfr_box/test_button.py @@ -0,0 +1,71 @@ +"""Test the SFR Box buttons.""" +from collections.abc import Generator +from unittest.mock import patch + +import pytest +from sfrbox_api.exceptions import SFRBoxError + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError + +from . import check_device_registry, check_entities +from .const import EXPECTED_ENTITIES + +from tests.common import mock_device_registry, mock_registry + +pytestmark = pytest.mark.usefixtures("system_get_info", "dsl_get_info") + + +@pytest.fixture(autouse=True) +def override_platforms() -> Generator[None, None, None]: + """Override PLATFORMS_WITH_AUTH.""" + with patch( + "homeassistant.components.sfr_box.PLATFORMS_WITH_AUTH", [Platform.BUTTON] + ), patch("homeassistant.components.sfr_box.coordinator.SFRBox.authenticate"): + yield + + +async def test_buttons( + hass: HomeAssistant, config_entry_with_auth: ConfigEntry +) -> None: + """Test for SFR Box buttons.""" + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + await hass.config_entries.async_setup(config_entry_with_auth.entry_id) + await hass.async_block_till_done() + + check_device_registry(device_registry, EXPECTED_ENTITIES["expected_device"]) + + expected_entities = EXPECTED_ENTITIES[Platform.BUTTON] + assert len(entity_registry.entities) == len(expected_entities) + + check_entities(hass, entity_registry, expected_entities) + + # Reboot success + service_data = {ATTR_ENTITY_ID: "button.sfr_box_reboot"} + with patch( + "homeassistant.components.sfr_box.button.SFRBox.system_reboot" + ) as mock_action: + await hass.services.async_call( + BUTTON_DOMAIN, SERVICE_PRESS, service_data=service_data, blocking=True + ) + + assert len(mock_action.mock_calls) == 1 + assert mock_action.mock_calls[0][1] == () + + # Reboot failed + service_data = {ATTR_ENTITY_ID: "button.sfr_box_reboot"} + with patch( + "homeassistant.components.sfr_box.button.SFRBox.system_reboot", + side_effect=SFRBoxError, + ) as mock_action, pytest.raises(HomeAssistantError): + await hass.services.async_call( + BUTTON_DOMAIN, SERVICE_PRESS, service_data=service_data, blocking=True + ) + + assert len(mock_action.mock_calls) == 1 + assert mock_action.mock_calls[0][1] == () diff --git a/tests/components/sfr_box/test_config_flow.py b/tests/components/sfr_box/test_config_flow.py new file mode 100644 index 00000000000..3b15907e36b --- /dev/null +++ b/tests/components/sfr_box/test_config_flow.py @@ -0,0 +1,252 @@ +"""Test the SFR Box config flow.""" +from collections.abc import Generator +import json +from unittest.mock import AsyncMock, patch + +import pytest +from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError +from sfrbox_api.models import SystemInfo + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.sfr_box.const import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from tests.common import load_fixture + + +@pytest.fixture(autouse=True, name="mock_setup_entry") +def override_async_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.sfr_box.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +async def test_config_flow_skip_auth(hass: HomeAssistant, mock_setup_entry: AsyncMock): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.sfr_box.config_flow.SFRBox.system_get_info", + side_effect=SFRBoxError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.1", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {"base": "cannot_connect"} + + with patch( + "homeassistant.components.sfr_box.config_flow.SFRBox.system_get_info", + return_value=SystemInfo( + **json.loads(load_fixture("system_getInfo.json", DOMAIN)) + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.1", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["step_id"] == "choose_auth" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": "skip_auth"}, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "SFR Box" + assert result["data"] == {CONF_HOST: "192.168.0.1"} + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_config_flow_with_auth(hass: HomeAssistant, mock_setup_entry: AsyncMock): + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.sfr_box.config_flow.SFRBox.system_get_info", + return_value=SystemInfo( + **json.loads(load_fixture("system_getInfo.json", DOMAIN)) + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.1", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.MENU + assert result["step_id"] == "choose_auth" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"next_step_id": "auth"}, + ) + + with patch( + "homeassistant.components.sfr_box.config_flow.SFRBox.authenticate", + side_effect=SFRBoxAuthenticationError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "admin", + CONF_PASSWORD: "invalid", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + with patch("homeassistant.components.sfr_box.config_flow.SFRBox.authenticate"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "admin", + CONF_PASSWORD: "valid", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == "SFR Box" + assert result["data"] == { + CONF_HOST: "192.168.0.1", + CONF_USERNAME: "admin", + CONF_PASSWORD: "valid", + } + + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.usefixtures("config_entry") +async def test_config_flow_duplicate_host( + hass: HomeAssistant, mock_setup_entry: AsyncMock +): + """Test abort if unique_id configured.""" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {} + + system_info = SystemInfo(**json.loads(load_fixture("system_getInfo.json", DOMAIN))) + # Ensure mac doesn't match existing mock entry + system_info.mac_addr = "aa:bb:cc:dd:ee:ff" + with patch( + "homeassistant.components.sfr_box.config_flow.SFRBox.system_get_info", + return_value=system_info, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.1", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 0 + + +@pytest.mark.usefixtures("config_entry") +async def test_config_flow_duplicate_mac( + hass: HomeAssistant, mock_setup_entry: AsyncMock +): + """Test abort if unique_id configured.""" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] == {} + + system_info = SystemInfo(**json.loads(load_fixture("system_getInfo.json", DOMAIN))) + with patch( + "homeassistant.components.sfr_box.config_flow.SFRBox.system_get_info", + return_value=system_info, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "192.168.0.2", + }, + ) + + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" + await hass.async_block_till_done() + + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_reauth(hass: HomeAssistant, config_entry_with_auth: ConfigEntry) -> None: + """Test the start of the config flow.""" + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": config_entry_with_auth.entry_id, + "unique_id": config_entry_with_auth.unique_id, + }, + data=config_entry_with_auth.data, + ) + + assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("errors") == {} + + # Failed credentials + with patch( + "homeassistant.components.sfr_box.config_flow.SFRBox.authenticate", + side_effect=SFRBoxAuthenticationError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "admin", + CONF_PASSWORD: "invalid", + }, + ) + + assert result.get("type") == data_entry_flow.FlowResultType.FORM + assert result.get("errors") == {"base": "invalid_auth"} + + # Valid credentials + with patch("homeassistant.components.sfr_box.config_flow.SFRBox.authenticate"): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_USERNAME: "admin", + CONF_PASSWORD: "new_password", + }, + ) + + assert result.get("type") == data_entry_flow.FlowResultType.ABORT + assert result.get("reason") == "reauth_successful" diff --git a/tests/components/sfr_box/test_diagnostics.py b/tests/components/sfr_box/test_diagnostics.py new file mode 100644 index 00000000000..08b65ac9315 --- /dev/null +++ b/tests/components/sfr_box/test_diagnostics.py @@ -0,0 +1,69 @@ +"""Test the SFR Box diagnostics.""" +from collections.abc import Generator +from unittest.mock import patch + +import pytest + +from homeassistant.components.diagnostics import REDACTED +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from tests.components.diagnostics import get_diagnostics_for_config_entry + +pytestmark = pytest.mark.usefixtures("system_get_info", "dsl_get_info") + + +@pytest.fixture(autouse=True) +def override_platforms() -> Generator[None, None, None]: + """Override PLATFORMS.""" + with patch("homeassistant.components.sfr_box.PLATFORMS", []): + yield + + +async def test_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry, hass_client +) -> None: + """Test config entry diagnostics.""" + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { + "entry": { + "data": {"host": "192.168.0.1"}, + "title": "Mock Title", + }, + "data": { + "dsl": { + "attenuation_down": 28.5, + "attenuation_up": 20.8, + "counter": 16, + "crc": 0, + "line_status": "No Defect", + "linemode": "ADSL2+", + "noise_down": 5.8, + "noise_up": 6.0, + "rate_down": 5549, + "rate_up": 187, + "status": "up", + "training": "Showtime", + "uptime": 450796, + }, + "system": { + "alimvoltage": 12251, + "current_datetime": "202212282233", + "idur": "RP3P85K", + "mac_addr": REDACTED, + "net_infra": "adsl", + "net_mode": "router", + "product_id": "NB6VAC-FXC-r0", + "refclient": "", + "serial_number": REDACTED, + "temperature": 27560, + "uptime": 2353575, + "version_bootloader": "NB6VAC-BOOTLOADER-R4.0.8", + "version_dsldriver": "NB6VAC-XDSL-A2pv6F039p", + "version_mainfirmware": "NB6VAC-MAIN-R4.0.44k", + "version_rescuefirmware": "NB6VAC-MAIN-R4.0.44k", + }, + }, + } diff --git a/tests/components/sfr_box/test_init.py b/tests/components/sfr_box/test_init.py new file mode 100644 index 00000000000..3a740753b21 --- /dev/null +++ b/tests/components/sfr_box/test_init.py @@ -0,0 +1,82 @@ +"""Test the SFR Box setup process.""" +from collections.abc import Generator +from unittest.mock import patch + +import pytest +from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError + +from homeassistant.components.sfr_box.const import DOMAIN +from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.core import HomeAssistant + + +@pytest.fixture(autouse=True) +def override_platforms() -> Generator[None, None, None]: + """Override PLATFORMS.""" + with patch("homeassistant.components.sfr_box.PLATFORMS", []): + yield + + +@pytest.mark.usefixtures("system_get_info", "dsl_get_info") +async def test_setup_unload_entry( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: + """Test entry setup and unload.""" + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.LOADED + + # Unload the entry and verify that the data has been removed + await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state is ConfigEntryState.NOT_LOADED + + +async def test_setup_entry_exception( + hass: HomeAssistant, config_entry: ConfigEntry +) -> None: + """Test ConfigEntryNotReady when API raises an exception during entry setup.""" + with patch( + "homeassistant.components.sfr_box.coordinator.SFRBox.system_get_info", + side_effect=SFRBoxError, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.SETUP_RETRY + assert not hass.data.get(DOMAIN) + + +async def test_setup_entry_auth_exception( + hass: HomeAssistant, config_entry_with_auth: ConfigEntry +) -> None: + """Test ConfigEntryNotReady when API raises an exception during authentication.""" + with patch( + "homeassistant.components.sfr_box.coordinator.SFRBox.authenticate", + side_effect=SFRBoxError, + ): + await hass.config_entries.async_setup(config_entry_with_auth.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry_with_auth.state is ConfigEntryState.SETUP_RETRY + assert not hass.data.get(DOMAIN) + + +async def test_setup_entry_invalid_auth( + hass: HomeAssistant, config_entry_with_auth: ConfigEntry +) -> None: + """Test ConfigEntryAuthFailed when API raises an exception during authentication.""" + with patch( + "homeassistant.components.sfr_box.coordinator.SFRBox.authenticate", + side_effect=SFRBoxAuthenticationError, + ): + await hass.config_entries.async_setup(config_entry_with_auth.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert config_entry_with_auth.state is ConfigEntryState.SETUP_ERROR + assert not hass.data.get(DOMAIN) diff --git a/tests/components/sfr_box/test_sensor.py b/tests/components/sfr_box/test_sensor.py new file mode 100644 index 00000000000..2f6bc9bac84 --- /dev/null +++ b/tests/components/sfr_box/test_sensor.py @@ -0,0 +1,59 @@ +"""Test the SFR Box sensors.""" +from collections.abc import Generator +from types import MappingProxyType +from unittest.mock import patch + +import pytest + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import EntityRegistry, RegistryEntryDisabler + +from . import check_device_registry, check_entities +from .const import ATTR_DEFAULT_DISABLED, EXPECTED_ENTITIES + +from tests.common import mock_device_registry, mock_registry + +pytestmark = pytest.mark.usefixtures("system_get_info", "dsl_get_info") + + +@pytest.fixture(autouse=True) +def override_platforms() -> Generator[None, None, None]: + """Override PLATFORMS.""" + with patch("homeassistant.components.sfr_box.PLATFORMS", [Platform.SENSOR]): + yield + + +def _check_and_enable_disabled_entities( + entity_registry: EntityRegistry, expected_entities: MappingProxyType +) -> None: + """Ensure that the expected_entities are correctly disabled.""" + for expected_entity in expected_entities: + if expected_entity.get(ATTR_DEFAULT_DISABLED): + entity_id = expected_entity[ATTR_ENTITY_ID] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry, f"Registry entry not found for {entity_id}" + assert registry_entry.disabled + assert registry_entry.disabled_by is RegistryEntryDisabler.INTEGRATION + entity_registry.async_update_entity(entity_id, **{"disabled_by": None}) + + +async def test_sensors(hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Test for SFR Box sensors.""" + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + check_device_registry(device_registry, EXPECTED_ENTITIES["expected_device"]) + + expected_entities = EXPECTED_ENTITIES[Platform.SENSOR] + assert len(entity_registry.entities) == len(expected_entities) + + _check_and_enable_disabled_entities(entity_registry, expected_entities) + await hass.config_entries.async_reload(config_entry.entry_id) + await hass.async_block_till_done() + + check_entities(hass, entity_registry, expected_entities) diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index 36e9944e394..672e81a881f 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -83,7 +83,7 @@ class MockAyla(AylaApi): """Get the list of devices.""" shark = MockShark(self, SHARK_DEVICE_DICT) shark.properties_full = deepcopy(SHARK_PROPERTIES_DICT) - shark._update_metadata(SHARK_METADATA_DICT) # pylint: disable=protected-access + shark._update_metadata(SHARK_METADATA_DICT) return [shark] async def async_request(self, http_method: str, url: str, **kwargs): diff --git a/tests/components/shelly/__init__.py b/tests/components/shelly/__init__.py index b58b147a9a3..346eed45baf 100644 --- a/tests/components/shelly/__init__.py +++ b/tests/components/shelly/__init__.py @@ -18,7 +18,7 @@ from homeassistant.components.shelly.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac from homeassistant.helpers.entity_registry import async_get from homeassistant.util import dt @@ -118,10 +118,5 @@ def register_device(device_reg, config_entry: ConfigEntry): """Register Shelly device.""" device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={ - ( - device_registry.CONNECTION_NETWORK_MAC, - device_registry.format_mac(MOCK_MAC), - ) - }, + connections={(CONNECTION_NETWORK_MAC, format_mac(MOCK_MAC))}, ) diff --git a/tests/components/shelly/test_climate.py b/tests/components/shelly/test_climate.py index 0d43ae118cf..527aa44c892 100644 --- a/tests/components/shelly/test_climate.py +++ b/tests/components/shelly/test_climate.py @@ -25,7 +25,7 @@ from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM from . import init_integration, register_device, register_entity -from tests.common import mock_restore_cache +from tests.common import mock_restore_cache, mock_restore_cache_with_extra_data SENSOR_BLOCK_ID = 3 DEVICE_BLOCK_ID = 4 @@ -191,19 +191,25 @@ async def test_block_restored_climate(hass, mock_block_device, device_reg, monke "sensor_0", entry, ) - mock_restore_cache(hass, [State(entity_id, HVACMode.HEAT)]) + attrs = {"current_temperature": 20.5, "temperature": 4.0} + extra_data = {"last_target_temp": 22.0} + mock_restore_cache_with_extra_data( + hass, ((State(entity_id, HVACMode.OFF, attributes=attrs), extra_data),) + ) monkeypatch.setattr(mock_block_device, "initialized", False) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == HVACMode.HEAT + assert hass.states.get(entity_id).state == HVACMode.OFF + assert hass.states.get(entity_id).attributes.get("temperature") == 4.0 # Partial update, should not change state mock_block_device.mock_update() await hass.async_block_till_done() - assert hass.states.get(entity_id).state == HVACMode.HEAT + assert hass.states.get(entity_id).state == HVACMode.OFF + assert hass.states.get(entity_id).attributes.get("temperature") == 4.0 # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) @@ -211,6 +217,24 @@ async def test_block_restored_climate(hass, mock_block_device, device_reg, monke await hass.async_block_till_done() assert hass.states.get(entity_id).state == HVACMode.OFF + assert hass.states.get(entity_id).attributes.get("temperature") == 4.0 + + # Test set hvac mode heat, target temp should be set to last target temp (22) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + mock_block_device.http_request.assert_called_once_with( + "get", "thermostat/0", {"target_t_enabled": 1, "target_t": 22.0} + ) + + monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "targetTemp", 22.0) + mock_block_device.mock_update() + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.HEAT + assert hass.states.get(entity_id).attributes.get("temperature") == 22.0 async def test_block_restored_climate_us_customery( @@ -229,36 +253,56 @@ async def test_block_restored_climate_us_customery( "sensor_0", entry, ) - attrs = {"current_temperature": 67, "temperature": 68} - mock_restore_cache(hass, [State(entity_id, HVACMode.HEAT, attributes=attrs)]) + attrs = {"current_temperature": 67, "temperature": 39} + extra_data = {"last_target_temp": 10.0} + mock_restore_cache_with_extra_data( + hass, ((State(entity_id, HVACMode.OFF, attributes=attrs), extra_data),) + ) monkeypatch.setattr(mock_block_device, "initialized", False) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == HVACMode.HEAT - assert hass.states.get(entity_id).attributes.get("temperature") == 68 + assert hass.states.get(entity_id).state == HVACMode.OFF + assert hass.states.get(entity_id).attributes.get("temperature") == 39 assert hass.states.get(entity_id).attributes.get("current_temperature") == 67 # Partial update, should not change state mock_block_device.mock_update() await hass.async_block_till_done() - assert hass.states.get(entity_id).state == HVACMode.HEAT - assert hass.states.get(entity_id).attributes.get("temperature") == 68 + assert hass.states.get(entity_id).state == HVACMode.OFF + assert hass.states.get(entity_id).attributes.get("temperature") == 39 assert hass.states.get(entity_id).attributes.get("current_temperature") == 67 # Make device online monkeypatch.setattr(mock_block_device, "initialized", True) - monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "targetTemp", 19.7) + monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "targetTemp", 4.0) monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "temp", 18.2) mock_block_device.mock_update() await hass.async_block_till_done() - assert hass.states.get(entity_id).state == HVACMode.HEAT - assert hass.states.get(entity_id).attributes.get("temperature") == 67 + assert hass.states.get(entity_id).state == HVACMode.OFF + assert hass.states.get(entity_id).attributes.get("temperature") == 39 assert hass.states.get(entity_id).attributes.get("current_temperature") == 65 + # Test set hvac mode heat, target temp should be set to last target temp (10.0/50) + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, + blocking=True, + ) + mock_block_device.http_request.assert_called_once_with( + "get", "thermostat/0", {"target_t_enabled": 1, "target_t": 10.0} + ) + + monkeypatch.setattr(mock_block_device.blocks[SENSOR_BLOCK_ID], "targetTemp", 10.0) + mock_block_device.mock_update() + state = hass.states.get(ENTITY_ID) + assert state.state == HVACMode.HEAT + assert hass.states.get(entity_id).attributes.get("temperature") == 50 + async def test_block_restored_climate_unavailable( hass, mock_block_device, device_reg, monkeypatch diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index fbaf2d98ba2..7a8ffc87e52 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -66,7 +66,7 @@ async def test_form(hass, gen): assert result["errors"] == {} with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False, "gen": gen}, ), patch( "aioshelly.block_device.BlockDevice.create", @@ -123,7 +123,7 @@ async def test_title_without_name(hass): settings["device"] = settings["device"].copy() settings["device"]["hostname"] = "shelly1pm-12345" with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, ), patch( "aioshelly.block_device.BlockDevice.create", @@ -174,7 +174,7 @@ async def test_form_auth(hass, test_data): assert result["errors"] == {} with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True, "gen": gen}, ): result2 = await hass.config_entries.flow.async_configure( @@ -237,7 +237,7 @@ async def test_form_errors_get_info(hass, error): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("aioshelly.common.get_info", side_effect=exc): + with patch("homeassistant.components.shelly.config_flow.get_info", side_effect=exc): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.1.1.1"}, @@ -253,7 +253,7 @@ async def test_form_missing_model_key(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "auth": False, "gen": "2"}, ), patch( "aioshelly.rpc_device.RpcDevice.create", @@ -283,7 +283,7 @@ async def test_form_missing_model_key_auth_enabled(hass): assert result["errors"] == {} with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "auth": True, "gen": 2}, ): result2 = await hass.config_entries.flow.async_configure( @@ -316,7 +316,7 @@ async def test_form_missing_model_key_zeroconf(hass, caplog): """Test we handle missing Shelly model key via zeroconf.""" with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "auth": False, "gen": 2}, ), patch( "aioshelly.rpc_device.RpcDevice.create", @@ -355,7 +355,8 @@ async def test_form_errors_test_connection(hass, error): ) with patch( - "aioshelly.common.get_info", return_value={"mac": "test-mac", "auth": False} + "homeassistant.components.shelly.config_flow.get_info", + return_value={"mac": "test-mac", "auth": False}, ), patch( "aioshelly.block_device.BlockDevice.create", new=AsyncMock(side_effect=exc) ): @@ -381,7 +382,7 @@ async def test_form_already_configured(hass): ) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, ): result2 = await hass.config_entries.flow.async_configure( @@ -416,7 +417,7 @@ async def test_user_setup_ignored_device(hass): settings["fw"] = "20201124-092534/v1.9.0@57ac4ad8" with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, ), patch( "aioshelly.block_device.BlockDevice.create", @@ -452,7 +453,10 @@ async def test_form_firmware_unsupported(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch("aioshelly.common.get_info", side_effect=FirmwareUnsupported): + with patch( + "homeassistant.components.shelly.config_flow.get_info", + side_effect=FirmwareUnsupported, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.1.1.1"}, @@ -478,7 +482,7 @@ async def test_form_auth_errors_test_connection_gen1(hass, error): ) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "auth": True}, ): result2 = await hass.config_entries.flow.async_configure( @@ -514,7 +518,7 @@ async def test_form_auth_errors_test_connection_gen2(hass, error): ) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "auth": True, "gen": 2}, ): result2 = await hass.config_entries.flow.async_configure( @@ -543,7 +547,9 @@ async def test_form_auth_errors_test_connection_gen2(hass, error): async def test_zeroconf(hass, gen, get_info): """Test we get the form.""" - with patch("aioshelly.common.get_info", return_value=get_info), patch( + with patch( + "homeassistant.components.shelly.config_flow.get_info", return_value=get_info + ), patch( "aioshelly.block_device.BlockDevice.create", new=AsyncMock(return_value=Mock(model="SHSW-1", settings=MOCK_SETTINGS)), ), patch( @@ -598,7 +604,7 @@ async def test_zeroconf_sleeping_device(hass): """Test sleeping device configuration via zeroconf.""" with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={ "mac": "test-mac", "type": "SHSW-1", @@ -662,7 +668,7 @@ async def test_zeroconf_sleeping_device(hass): async def test_zeroconf_sleeping_device_error(hass): """Test sleeping device configuration via zeroconf with error.""" with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={ "mac": "test-mac", "type": "SHSW-1", @@ -691,7 +697,7 @@ async def test_zeroconf_already_configured(hass): entry.add_to_hass(hass) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, ): result = await hass.config_entries.flow.async_init( @@ -718,7 +724,7 @@ async def test_zeroconf_ignored(hass): entry.add_to_hass(hass) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, ): result = await hass.config_entries.flow.async_init( @@ -739,7 +745,7 @@ async def test_zeroconf_with_wifi_ap_ip(hass): entry.add_to_hass(hass) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, ): result = await hass.config_entries.flow.async_init( @@ -756,7 +762,10 @@ async def test_zeroconf_with_wifi_ap_ip(hass): async def test_zeroconf_firmware_unsupported(hass): """Test we abort if device firmware is unsupported.""" - with patch("aioshelly.common.get_info", side_effect=FirmwareUnsupported): + with patch( + "homeassistant.components.shelly.config_flow.get_info", + side_effect=FirmwareUnsupported, + ): result = await hass.config_entries.flow.async_init( DOMAIN, data=DISCOVERY_INFO, @@ -769,7 +778,10 @@ async def test_zeroconf_firmware_unsupported(hass): async def test_zeroconf_cannot_connect(hass): """Test we get the form.""" - with patch("aioshelly.common.get_info", side_effect=DeviceConnectionError): + with patch( + "homeassistant.components.shelly.config_flow.get_info", + side_effect=DeviceConnectionError, + ): result = await hass.config_entries.flow.async_init( DOMAIN, data=DISCOVERY_INFO, @@ -783,7 +795,7 @@ async def test_zeroconf_require_auth(hass): """Test zeroconf if auth is required.""" with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True}, ): result = await hass.config_entries.flow.async_init( @@ -844,7 +856,7 @@ async def test_reauth_successful(hass, test_data): entry.add_to_hass(hass) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True, "gen": gen}, ), patch( "aioshelly.block_device.BlockDevice.create", @@ -898,7 +910,7 @@ async def test_reauth_unsuccessful(hass, test_data): entry.add_to_hass(hass) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": True, "gen": gen}, ), patch( "aioshelly.block_device.BlockDevice.create", @@ -937,7 +949,7 @@ async def test_reauth_get_info_error(hass, error): entry.add_to_hass(hass) with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", side_effect=error, ): result = await hass.config_entries.flow.async_init( @@ -1140,7 +1152,7 @@ async def test_zeroconf_already_configured_triggers_refresh_mac_in_name( assert len(mock_rpc_device.initialize.mock_calls) == 1 with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "", "type": "SHSW-1", "auth": False}, ): result = await hass.config_entries.flow.async_init( @@ -1172,7 +1184,7 @@ async def test_zeroconf_already_configured_triggers_refresh( assert len(mock_rpc_device.initialize.mock_calls) == 1 with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "AABBCCDDEEFF", "type": "SHSW-1", "auth": False}, ): result = await hass.config_entries.flow.async_init( @@ -1207,7 +1219,7 @@ async def test_zeroconf_sleeping_device_not_triggers_refresh( assert "online, resuming setup" in caplog.text with patch( - "aioshelly.common.get_info", + "homeassistant.components.shelly.config_flow.get_info", return_value={"mac": "AABBCCDDEEFF", "type": "SHSW-1", "auth": False}, ): result = await hass.config_entries.flow.async_init( diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py index 13ccf3f843e..31e54545b28 100644 --- a/tests/components/shelly/test_device_trigger.py +++ b/tests/components/shelly/test_device_trigger.py @@ -14,8 +14,8 @@ from homeassistant.components.shelly.const import ( EVENT_SHELLY_CLICK, ) from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE -from homeassistant.helpers import device_registry from homeassistant.helpers.device_registry import ( + CONNECTION_NETWORK_MAC, async_entries_for_config_entry, async_get as async_get_dev_reg, ) @@ -151,7 +151,7 @@ async def test_get_triggers_for_invalid_device_id(hass, device_reg, mock_block_d config_entry.add_to_hass(hass) invalid_device = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + connections={(CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) with pytest.raises(InvalidDeviceAutomationConfig): diff --git a/tests/components/shelly/test_init.py b/tests/components/shelly/test_init.py index 3675186b9ba..707e8d5cfb1 100644 --- a/tests/components/shelly/test_init.py +++ b/tests/components/shelly/test_init.py @@ -13,7 +13,7 @@ from homeassistant.components.shelly.const import ( ) from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import STATE_ON, STATE_UNAVAILABLE -from homeassistant.helpers import device_registry +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac from homeassistant.setup import async_setup_component from . import MOCK_MAC, init_integration @@ -43,12 +43,7 @@ async def test_shared_device_mac( config_entry.add_to_hass(hass) device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={ - ( - device_registry.CONNECTION_NETWORK_MAC, - device_registry.format_mac(MOCK_MAC), - ) - }, + connections={(CONNECTION_NETWORK_MAC, format_mac(MOCK_MAC))}, ) await init_integration(hass, gen, sleep_period=1000) assert "will resume when device is online" in caplog.text @@ -117,12 +112,7 @@ async def test_sleeping_block_device_online( config_entry.add_to_hass(hass) device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={ - ( - device_registry.CONNECTION_NETWORK_MAC, - device_registry.format_mac(MOCK_MAC), - ) - }, + connections={(CONNECTION_NETWORK_MAC, format_mac(MOCK_MAC))}, ) entry = await init_integration(hass, 1, sleep_period=entry_sleep) diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index 6dcd903219f..d62682df5a9 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -180,6 +180,11 @@ async def test_rpc_sensor(hass, mock_rpc_device, monkeypatch) -> None: assert hass.states.get(entity_id).state == "88.2" + mutate_rpc_device_status(monkeypatch, mock_rpc_device, "cover:0", "apower", None) + mock_rpc_device.mock_update() + + assert hass.states.get(entity_id).state == STATE_UNKNOWN + async def test_rpc_sensor_error(hass, mock_rpc_device, monkeypatch): """Test RPC sensor unavailable on sensor error.""" diff --git a/tests/components/sma/test_sensor.py b/tests/components/sma/test_sensor.py index 9e4149b0720..fc175b286ed 100644 --- a/tests/components/sma/test_sensor.py +++ b/tests/components/sma/test_sensor.py @@ -1,9 +1,9 @@ """Test the sma sensor platform.""" -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, POWER_WATT +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfPower async def test_sensors(hass, init_integration): """Test states of the sensors.""" state = hass.states.get("sensor.sma_device_grid_power") assert state - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == POWER_WATT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 4111d21b25b..1e3b084877a 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -124,7 +124,7 @@ async def test_set_cover_position(hass, device_factory): assert state.attributes[ATTR_BATTERY_LEVEL] == 95 assert state.attributes[ATTR_CURRENT_POSITION] == 10 # Ensure API called - # pylint: disable=protected-access + assert device._api.post_device_command.call_count == 1 # type: ignore @@ -147,7 +147,7 @@ async def test_set_cover_position_unsupported(hass, device_factory): assert ATTR_CURRENT_POSITION not in state.attributes # Ensure API was not called - # pylint: disable=protected-access + assert device._api.post_device_command.call_count == 0 # type: ignore diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 8696fb5956e..9987d53a6b2 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -451,7 +451,6 @@ async def test_event_handler_dispatches_updated_devices( broker = smartthings.DeviceBroker(hass, config_entry, Mock(), Mock(), devices, []) broker.connect() - # pylint:disable=protected-access await broker._event_handler(request, None, None) await hass.async_block_till_done() @@ -478,7 +477,6 @@ async def test_event_handler_ignores_other_installed_app( broker = smartthings.DeviceBroker(hass, config_entry, Mock(), Mock(), [device], []) broker.connect() - # pylint:disable=protected-access await broker._event_handler(request, None, None) await hass.async_block_till_done() @@ -516,7 +514,6 @@ async def test_event_handler_fires_button_events( broker = smartthings.DeviceBroker(hass, config_entry, Mock(), Mock(), [device], []) broker.connect() - # pylint:disable=protected-access await broker._event_handler(request, None, None) await hass.async_block_till_done() diff --git a/tests/components/smartthings/test_scene.py b/tests/components/smartthings/test_scene.py index 288fae046f5..ccb47144676 100644 --- a/tests/components/smartthings/test_scene.py +++ b/tests/components/smartthings/test_scene.py @@ -37,7 +37,6 @@ async def test_scene_activate(hass, scene): assert state.attributes["icon"] == scene.icon assert state.attributes["color"] == scene.color assert state.attributes["location_id"] == scene.location_id - # pylint: disable=protected-access assert scene.execute.call_count == 1 # type: ignore diff --git a/tests/components/smhi/common.py b/tests/components/smhi/common.py index 6f215840324..8a12cf651b7 100644 --- a/tests/components/smhi/common.py +++ b/tests/components/smhi/common.py @@ -5,7 +5,6 @@ from unittest.mock import Mock class AsyncMock(Mock): """Implements Mock async.""" - # pylint: disable=useless-super-delegation async def __call__(self, *args, **kwargs): """Hack for async support for Mock.""" return super().__call__(*args, **kwargs) diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index f33e8c9fa71..3b47e744b9a 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -31,7 +31,7 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_SPEED_UNIT, DOMAIN as WEATHER_DOMAIN, ) -from homeassistant.const import ATTR_ATTRIBUTION, SPEED_METERS_PER_SECOND, STATE_UNKNOWN +from homeassistant.const import ATTR_ATTRIBUTION, STATE_UNKNOWN, UnitOfSpeed from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.util.dt import utcnow @@ -343,7 +343,7 @@ async def test_custom_speed_unit( entity_reg.async_update_entity_options( state.entity_id, WEATHER_DOMAIN, - {ATTR_WEATHER_WIND_SPEED_UNIT: SPEED_METERS_PER_SECOND}, + {ATTR_WEATHER_WIND_SPEED_UNIT: UnitOfSpeed.METERS_PER_SECOND}, ) await hass.async_block_till_done() diff --git a/tests/components/snmp/test_sensor.py b/tests/components/snmp/test_sensor.py index 965bc0d3ae9..b15cc4bfa61 100644 --- a/tests/components/snmp/test_sensor.py +++ b/tests/components/snmp/test_sensor.py @@ -15,7 +15,7 @@ from homeassistant.setup import async_setup_component def hlapi_mock(): """Mock out 3rd party API.""" mock_data = MagicMock() - mock_data.prettyPrint = Mock(return_value="hello") + mock_data.prettyPrint = Mock(return_value="13.5") future = asyncio.get_event_loop().create_future() future.set_result((None, None, None, [[mock_data]])) with patch( @@ -40,7 +40,7 @@ async def test_basic_config(hass: HomeAssistant) -> None: await hass.async_block_till_done() state = hass.states.get("sensor.snmp") - assert state.state == "hello" + assert state.state == "13.5" assert state.attributes == {"friendly_name": "SNMP"} @@ -71,7 +71,7 @@ async def test_entity_config(hass: HomeAssistant) -> None: assert entity_registry.async_get("sensor.snmp_sensor").unique_id == "very_unique" state = hass.states.get("sensor.snmp_sensor") - assert state.state == "hello" + assert state.state == "13.5" assert state.attributes == { "device_class": "temperature", "entity_picture": "blabla.png", diff --git a/tests/components/snooz/test_fan.py b/tests/components/snooz/test_fan.py index 30528336e2d..19c794d6f04 100644 --- a/tests/components/snooz/test_fan.py +++ b/tests/components/snooz/test_fan.py @@ -10,7 +10,12 @@ from pysnooz.testing import MockSnoozDevice import pytest from homeassistant.components import fan -from homeassistant.components.snooz.const import DOMAIN +from homeassistant.components.snooz.const import ( + ATTR_DURATION, + DOMAIN, + SERVICE_TRANSITION_OFF, + SERVICE_TRANSITION_ON, +) from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, @@ -41,6 +46,20 @@ async def test_turn_on(hass: HomeAssistant, snooz_fan_entity_id: str): assert ATTR_ASSUMED_STATE not in state.attributes +async def test_transition_on(hass: HomeAssistant, snooz_fan_entity_id: str): + """Test transitioning on the device.""" + await hass.services.async_call( + DOMAIN, + SERVICE_TRANSITION_ON, + {ATTR_ENTITY_ID: [snooz_fan_entity_id], ATTR_DURATION: 1}, + blocking=True, + ) + + state = hass.states.get(snooz_fan_entity_id) + assert state.state == STATE_ON + assert ATTR_ASSUMED_STATE not in state.attributes + + @pytest.mark.parametrize("percentage", [1, 22, 50, 99, 100]) async def test_turn_on_with_percentage( hass: HomeAssistant, snooz_fan_entity_id: str, percentage: int @@ -115,6 +134,20 @@ async def test_turn_off(hass: HomeAssistant, snooz_fan_entity_id: str): assert ATTR_ASSUMED_STATE not in state.attributes +async def test_transition_off(hass: HomeAssistant, snooz_fan_entity_id: str): + """Test transitioning off the device.""" + await hass.services.async_call( + DOMAIN, + SERVICE_TRANSITION_OFF, + {ATTR_ENTITY_ID: [snooz_fan_entity_id], ATTR_DURATION: 1}, + blocking=True, + ) + + state = hass.states.get(snooz_fan_entity_id) + assert state.state == STATE_OFF + assert ATTR_ASSUMED_STATE not in state.attributes + + async def test_push_events( hass: HomeAssistant, mock_connected_snooz: SnoozFixture, snooz_fan_entity_id: str ): diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index 0eae7cfb8a8..9240e99d3e0 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -9,8 +9,8 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - DATA_GIGABYTES, STATE_UNAVAILABLE, + UnitOfInformation, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -56,7 +56,7 @@ async def test_sensors( state = hass.states.get("sensor.sonarr_disk_space") assert state assert state.attributes.get(ATTR_ICON) == "mdi:harddisk" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.attributes.get("C:\\") == "263.10/465.42GB (56.53%)" assert state.state == "263.10" diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 2ac1cb460cb..ef420c11ef2 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -10,7 +10,7 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture class SonosMockService: @@ -66,13 +66,14 @@ async def async_autosetup_sonos(async_setup_sonos): @pytest.fixture -def async_setup_sonos(hass, config_entry): +def async_setup_sonos(hass, config_entry, fire_zgs_event): """Return a coroutine to set up a Sonos integration instance on demand.""" async def _wrapper(): config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + await fire_zgs_event() return _wrapper @@ -349,3 +350,24 @@ def tv_event_fixture(soco): def mock_get_source_ip(mock_get_source_ip): """Mock network util's async_get_source_ip in all sonos tests.""" return mock_get_source_ip + + +@pytest.fixture(name="zgs_discovery", scope="session") +def zgs_discovery_fixture(): + """Load ZoneGroupState discovery payload and return it.""" + return load_fixture("sonos/zgs_discovery.xml") + + +@pytest.fixture(name="fire_zgs_event") +def zgs_event_fixture(hass, soco, zgs_discovery): + """Create alarm_event fixture.""" + variables = {"ZoneGroupState": zgs_discovery} + + async def _wrapper(): + event = SonosMockEvent(soco, soco.zoneGroupTopology, variables) + subscription = soco.zoneGroupTopology.subscribe.return_value + sub_callback = subscription.callback + sub_callback(event) + await hass.async_block_till_done() + + return _wrapper diff --git a/tests/components/sonos/fixtures/zgs_discovery.xml b/tests/components/sonos/fixtures/zgs_discovery.xml new file mode 100644 index 00000000000..3433bc0f32f --- /dev/null +++ b/tests/components/sonos/fixtures/zgs_discovery.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/components/sonos/test_config_flow.py b/tests/components/sonos/test_config_flow.py index f0e6c81a411..ebb8e0234e0 100644 --- a/tests/components/sonos/test_config_flow.py +++ b/tests/components/sonos/test_config_flow.py @@ -62,8 +62,12 @@ async def test_user_form( async def test_user_form_already_created(hass: core.HomeAssistant): """Ensure we abort a flow if the entry is already created from config.""" config = {DOMAIN: {MP_DOMAIN: {CONF_HOSTS: "192.168.4.2"}}} - await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() + with patch( + "homeassistant.components.sonos.async_setup_entry", + return_value=True, + ): + await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index 49ddbffc41a..6c79cdc7367 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -184,7 +184,7 @@ async def test_microphone_binary_sensor( assert mic_binary_sensor_state.state == STATE_ON -async def test_favorites_sensor(hass, async_autosetup_sonos, soco): +async def test_favorites_sensor(hass, async_autosetup_sonos, soco, fire_zgs_event): """Test Sonos favorites sensor.""" entity_registry = ent_reg.async_get(hass) favorites = entity_registry.entities["sensor.sonos_favorites"] @@ -208,6 +208,9 @@ async def test_favorites_sensor(hass, async_autosetup_sonos, soco): ) await hass.async_block_till_done() + # Trigger subscription callback for speaker discovery + await fire_zgs_event() + favorites_updated_event = SonosMockEvent( soco, service, {"favorites_update_id": "2", "container_update_i_ds": "FV:2,2"} ) diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py index 2b794657565..8b3fed98902 100644 --- a/tests/components/sonos/test_switch.py +++ b/tests/components/sonos/test_switch.py @@ -37,7 +37,7 @@ async def test_entity_registry(hass, async_autosetup_sonos): assert "switch.zone_a_touch_controls" in entity_registry.entities -async def test_switch_attributes(hass, async_autosetup_sonos, soco): +async def test_switch_attributes(hass, async_autosetup_sonos, soco, fire_zgs_event): """Test for correct Sonos switch states.""" entity_registry = ent_reg.async_get(hass) @@ -114,6 +114,9 @@ async def test_switch_attributes(hass, async_autosetup_sonos, soco): await hass.async_block_till_done() assert m.called + # Trigger subscription callback for speaker discovery + await fire_zgs_event() + status_light_state = hass.states.get(status_light.entity_id) assert status_light_state.state == STATE_ON diff --git a/tests/components/soundtouch/conftest.py b/tests/components/soundtouch/conftest.py index e944da89d8c..dc5621ed507 100644 --- a/tests/components/soundtouch/conftest.py +++ b/tests/components/soundtouch/conftest.py @@ -20,9 +20,6 @@ DEVICE_1_ENTITY_ID = f"{MEDIA_PLAYER_DOMAIN}.my_soundtouch_1" DEVICE_2_ENTITY_ID = f"{MEDIA_PLAYER_DOMAIN}.my_soundtouch_2" -# pylint: disable=redefined-outer-name - - @pytest.fixture def device1_config() -> MockConfigEntry: """Mock SoundTouch device 1 config entry.""" diff --git a/tests/components/soundtouch/test_config_flow.py b/tests/components/soundtouch/test_config_flow.py index cbeb27be979..92a96d4c9a8 100644 --- a/tests/components/soundtouch/test_config_flow.py +++ b/tests/components/soundtouch/test_config_flow.py @@ -25,7 +25,6 @@ async def test_user_flow_create_entry( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "user" - assert "flow_id" in result with patch( "homeassistant.components.soundtouch.async_setup_entry", return_value=True diff --git a/tests/components/spaceapi/test_init.py b/tests/components/spaceapi/test_init.py index 35da5bf27c7..124bbe07c09 100644 --- a/tests/components/spaceapi/test_init.py +++ b/tests/components/spaceapi/test_init.py @@ -1,13 +1,11 @@ """The tests for the Home Assistant SpaceAPI component.""" from http import HTTPStatus - -# pylint: disable=protected-access from unittest.mock import patch import pytest from homeassistant.components.spaceapi import DOMAIN, SPACEAPI_VERSION, URL_API_SPACEAPI -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, UnitOfTemperature from homeassistant.setup import async_setup_component from tests.common import mock_coro @@ -62,8 +60,18 @@ CONFIG = { SENSOR_OUTPUT = { "temperature": [ - {"location": "Home", "name": "temp1", "unit": TEMP_CELSIUS, "value": "25"}, - {"location": "Home", "name": "temp2", "unit": TEMP_CELSIUS, "value": "23"}, + { + "location": "Home", + "name": "temp1", + "unit": UnitOfTemperature.CELSIUS, + "value": "25", + }, + { + "location": "Home", + "name": "temp2", + "unit": UnitOfTemperature.CELSIUS, + "value": "23", + }, ], "humidity": [ {"location": "Home", "name": "hum1", "unit": PERCENTAGE, "value": "88"} @@ -78,10 +86,14 @@ def mock_client(hass, hass_client): hass.loop.run_until_complete(async_setup_component(hass, "spaceapi", CONFIG)) hass.states.async_set( - "test.temp1", 25, attributes={ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.temp1", + 25, + attributes={ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) hass.states.async_set( - "test.temp2", 23, attributes={ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "test.temp2", + 23, + attributes={ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) hass.states.async_set( "test.hum1", 88, attributes={ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE} diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 8f3b5799374..20fc63d9f8b 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -84,7 +84,6 @@ async def test_full_flow( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -151,7 +150,6 @@ async def test_abort_if_spotify_error( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -213,7 +211,6 @@ async def test_reauthentication( result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -279,7 +276,6 @@ async def test_reauth_account_mismatch( flows = hass.config_entries.flow.async_progress() result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/sql/__init__.py b/tests/components/sql/__init__.py index db65034bd11..1c096dfef6a 100644 --- a/tests/components/sql/__init__.py +++ b/tests/components/sql/__init__.py @@ -6,7 +6,12 @@ from typing import Any from homeassistant.components.recorder import CONF_DB_URL from homeassistant.components.sql.const import CONF_COLUMN_NAME, CONF_QUERY, DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT +from homeassistant.const import ( + CONF_NAME, + CONF_UNIQUE_ID, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, +) from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -42,6 +47,36 @@ ENTRY_CONFIG_NO_RESULTS = { CONF_UNIT_OF_MEASUREMENT: "MiB", } +YAML_CONFIG = { + "sql": { + CONF_DB_URL: "sqlite://", + CONF_NAME: "Get Value", + CONF_QUERY: "SELECT 5 as value", + CONF_COLUMN_NAME: "value", + CONF_UNIT_OF_MEASUREMENT: "MiB", + CONF_UNIQUE_ID: "unique_id_12345", + CONF_VALUE_TEMPLATE: "{{ value }}", + } +} + +YAML_CONFIG_INVALID = { + "sql": { + CONF_DB_URL: "sqlite://", + CONF_QUERY: "SELECT 5 as value", + CONF_COLUMN_NAME: "value", + CONF_UNIT_OF_MEASUREMENT: "MiB", + CONF_UNIQUE_ID: "unique_id_12345", + } +} + +YAML_CONFIG_NO_DB = { + "sql": { + CONF_NAME: "Get Value", + CONF_QUERY: "SELECT 5 as value", + CONF_COLUMN_NAME: "value", + } +} + async def init_integration( hass: HomeAssistant, diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index e5bbc163249..d26d3f9ae52 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -1,7 +1,7 @@ """Test the SQL config flow.""" from __future__ import annotations -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from sqlalchemy.exc import SQLAlchemyError @@ -21,7 +21,7 @@ from . import ( from tests.common import MockConfigEntry -async def test_form(recorder_mock, hass: HomeAssistant) -> None: +async def test_form(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( @@ -53,57 +53,7 @@ async def test_form(recorder_mock, hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_import_flow_success(recorder_mock, hass: HomeAssistant) -> None: - """Test a successful import of yaml.""" - - with patch( - "homeassistant.components.sql.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=ENTRY_CONFIG, - ) - await hass.async_block_till_done() - - assert result2["type"] == FlowResultType.CREATE_ENTRY - assert result2["title"] == "Get Value" - assert result2["options"] == { - "db_url": "sqlite://", - "name": "Get Value", - "query": "SELECT 5 as value", - "column": "value", - "unit_of_measurement": "MiB", - "value_template": None, - } - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_import_flow_already_exist(recorder_mock, hass: HomeAssistant) -> None: - """Test import of yaml already exist.""" - - MockConfigEntry( - domain=DOMAIN, - data=ENTRY_CONFIG, - ).add_to_hass(hass) - - with patch( - "homeassistant.components.sql.async_setup_entry", - return_value=True, - ): - result3 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=ENTRY_CONFIG, - ) - await hass.async_block_till_done() - - assert result3["type"] == FlowResultType.ABORT - assert result3["reason"] == "already_configured" - - -async def test_flow_fails_db_url(recorder_mock, hass: HomeAssistant) -> None: +async def test_flow_fails_db_url(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test config flow fails incorrect db url.""" result4 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -124,7 +74,9 @@ async def test_flow_fails_db_url(recorder_mock, hass: HomeAssistant) -> None: assert result4["errors"] == {"db_url": "db_url_invalid"} -async def test_flow_fails_invalid_query(recorder_mock, hass: HomeAssistant) -> None: +async def test_flow_fails_invalid_query( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: """Test config flow fails incorrect db url.""" result4 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -170,7 +122,7 @@ async def test_flow_fails_invalid_query(recorder_mock, hass: HomeAssistant) -> N } -async def test_options_flow(recorder_mock, hass: HomeAssistant) -> None: +async def test_options_flow(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test options config flow.""" entry = MockConfigEntry( domain=DOMAIN, @@ -219,7 +171,7 @@ async def test_options_flow(recorder_mock, hass: HomeAssistant) -> None: async def test_options_flow_name_previously_removed( - recorder_mock, hass: HomeAssistant + recorder_mock: AsyncMock, hass: HomeAssistant ) -> None: """Test options config flow where the name was missing.""" entry = MockConfigEntry( @@ -270,7 +222,9 @@ async def test_options_flow_name_previously_removed( } -async def test_options_flow_fails_db_url(recorder_mock, hass: HomeAssistant) -> None: +async def test_options_flow_fails_db_url( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: """Test options flow fails incorrect db url.""" entry = MockConfigEntry( domain=DOMAIN, @@ -313,7 +267,7 @@ async def test_options_flow_fails_db_url(recorder_mock, hass: HomeAssistant) -> async def test_options_flow_fails_invalid_query( - recorder_mock, hass: HomeAssistant + recorder_mock: AsyncMock, hass: HomeAssistant ) -> None: """Test options flow fails incorrect query and template.""" entry = MockConfigEntry( @@ -369,7 +323,9 @@ async def test_options_flow_fails_invalid_query( } -async def test_options_flow_db_url_empty(recorder_mock, hass: HomeAssistant) -> None: +async def test_options_flow_db_url_empty( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: """Test options config flow with leaving db_url empty.""" entry = MockConfigEntry( domain=DOMAIN, diff --git a/tests/components/sql/test_init.py b/tests/components/sql/test_init.py index 5c3f237ac49..46693195669 100644 --- a/tests/components/sql/test_init.py +++ b/tests/components/sql/test_init.py @@ -1,17 +1,27 @@ """Test for SQL component Init.""" +from __future__ import annotations + +from unittest.mock import AsyncMock, patch + +import pytest +import voluptuous as vol + from homeassistant import config_entries +from homeassistant.components.sql import validate_sql_select +from homeassistant.components.sql.const import DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component -from . import init_integration +from . import YAML_CONFIG_INVALID, YAML_CONFIG_NO_DB, init_integration -async def test_setup_entry(hass: HomeAssistant) -> None: +async def test_setup_entry(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test setup entry.""" config_entry = await init_integration(hass) assert config_entry.state == config_entries.ConfigEntryState.LOADED -async def test_unload_entry(hass: HomeAssistant) -> None: +async def test_unload_entry(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test unload an entry.""" config_entry = await init_integration(hass) assert config_entry.state == config_entries.ConfigEntryState.LOADED @@ -19,3 +29,29 @@ async def test_unload_entry(hass: HomeAssistant) -> None: assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is config_entries.ConfigEntryState.NOT_LOADED + + +async def test_setup_config(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: + """Test setup from yaml config.""" + with patch( + "homeassistant.components.sql.config_flow.sqlalchemy.create_engine", + ): + assert await async_setup_component(hass, DOMAIN, YAML_CONFIG_NO_DB) + await hass.async_block_till_done() + + +async def test_setup_invalid_config( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: + """Test setup from yaml with invalid config.""" + with patch( + "homeassistant.components.sql.config_flow.sqlalchemy.create_engine", + ): + assert not await async_setup_component(hass, DOMAIN, YAML_CONFIG_INVALID) + await hass.async_block_till_done() + + +async def test_invalid_query(hass: HomeAssistant) -> None: + """Test invalid query.""" + with pytest.raises(vol.Invalid): + validate_sql_select("DROP TABLE *") diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index 588e1c824b7..fdcf8fe1a5b 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -1,22 +1,25 @@ """The test for the sql sensor platform.""" -from unittest.mock import patch +from __future__ import annotations + +from datetime import timedelta +from unittest.mock import AsyncMock, patch import pytest from sqlalchemy.exc import SQLAlchemyError from homeassistant.components.sql.const import DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_NAME, STATE_UNKNOWN +from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component +from homeassistant.util import dt -from . import init_integration +from . import YAML_CONFIG, init_integration -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed -async def test_query(hass: HomeAssistant) -> None: +async def test_query(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test the SQL sensor.""" config = { "db_url": "sqlite://", @@ -31,31 +34,9 @@ async def test_query(hass: HomeAssistant) -> None: assert state.attributes["value"] == 5 -async def test_import_query(hass: HomeAssistant) -> None: - """Test the SQL sensor.""" - config = { - "sensor": { - "platform": "sql", - "db_url": "sqlite://", - "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() - - assert hass.config_entries.async_entries(DOMAIN) - options = hass.config_entries.async_entries(DOMAIN)[0].options - assert options[CONF_NAME] == "count_tables" - - -async def test_query_value_template(hass: HomeAssistant) -> None: +async def test_query_value_template( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: """Test the SQL sensor.""" config = { "db_url": "sqlite://", @@ -70,7 +51,9 @@ async def test_query_value_template(hass: HomeAssistant) -> None: assert state.state == "5" -async def test_query_value_template_invalid(hass: HomeAssistant) -> None: +async def test_query_value_template_invalid( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: """Test the SQL sensor.""" config = { "db_url": "sqlite://", @@ -85,7 +68,7 @@ async def test_query_value_template_invalid(hass: HomeAssistant) -> None: assert state.state == "5.01" -async def test_query_limit(hass: HomeAssistant) -> None: +async def test_query_limit(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: """Test the SQL sensor with a query containing 'LIMIT' in lowercase.""" config = { "db_url": "sqlite://", @@ -101,7 +84,7 @@ async def test_query_limit(hass: HomeAssistant) -> None: async def test_query_no_value( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + recorder_mock: AsyncMock, hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test the SQL sensor with a query that returns no value.""" config = { @@ -120,7 +103,7 @@ async def test_query_no_value( async def test_query_mssql_no_result( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + recorder_mock: AsyncMock, hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: """Test the SQL sensor with a query that returns no value.""" config = { @@ -158,6 +141,7 @@ async def test_query_mssql_no_result( ], ) async def test_invalid_url_setup( + recorder_mock: AsyncMock, hass: HomeAssistant, caplog: pytest.LogCaptureFixture, url: str, @@ -218,13 +202,97 @@ async def test_invalid_url_on_update( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - with patch( + with patch("homeassistant.components.recorder",), patch( "homeassistant.components.sql.sensor.sqlalchemy.engine.cursor.CursorResult", side_effect=SQLAlchemyError( "sqlite://homeassistant:hunter2@homeassistant.local" ), ): - await async_update_entity(hass, "sensor.count_tables") + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(minutes=1), + ) + await hass.async_block_till_done() assert "sqlite://homeassistant:hunter2@homeassistant.local" not in caplog.text assert "sqlite://****:****@homeassistant.local" in caplog.text + + +async def test_query_from_yaml(recorder_mock: AsyncMock, hass: HomeAssistant) -> None: + """Test the SQL sensor from yaml config.""" + + assert await async_setup_component(hass, DOMAIN, YAML_CONFIG) + await hass.async_block_till_done() + + state = hass.states.get("sensor.get_value") + assert state.state == "5" + + +async def test_config_from_old_yaml( + recorder_mock: AsyncMock, hass: HomeAssistant +) -> None: + """Test the SQL sensor from old yaml config does not create any entity.""" + config = { + "sensor": { + "platform": "sql", + "db_url": "sqlite://", + "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 not state + + +@pytest.mark.parametrize( + "url,expected_patterns,not_expected_patterns", + [ + ( + "sqlite://homeassistant:hunter2@homeassistant.local", + ["sqlite://****:****@homeassistant.local"], + ["sqlite://homeassistant:hunter2@homeassistant.local"], + ), + ( + "sqlite://homeassistant.local", + ["sqlite://homeassistant.local"], + [], + ), + ], +) +async def test_invalid_url_setup_from_yaml( + recorder_mock: AsyncMock, + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + url: str, + expected_patterns: str, + not_expected_patterns: str, +): + """Test invalid db url with redacted credentials from yaml setup.""" + config = { + "sql": { + "db_url": url, + "query": "SELECT 5 as value", + "column": "value", + "name": "count_tables", + } + } + + with patch( + "homeassistant.components.sql.sensor.sqlalchemy.create_engine", + side_effect=SQLAlchemyError(url), + ): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + for pattern in not_expected_patterns: + assert pattern not in caplog.text + for pattern in expected_patterns: + assert pattern in caplog.text diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py index 3cf2837353e..fab56dbc286 100644 --- a/tests/components/srp_energy/test_sensor.py +++ b/tests/components/srp_energy/test_sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.srp_energy.const import ( SRP_ENERGY_DOMAIN, ) from homeassistant.components.srp_energy.sensor import SrpEntity, async_setup_entry -from homeassistant.const import ENERGY_KILO_WATT_HOUR +from homeassistant.const import UnitOfEnergy async def test_async_setup_entry(hass): @@ -89,7 +89,7 @@ async def test_srp_entity(hass): assert srp_entity.name == f"{DEFAULT_NAME} {SENSOR_NAME}" assert srp_entity.unique_id == SENSOR_TYPE assert srp_entity.state is None - assert srp_entity.unit_of_measurement == ENERGY_KILO_WATT_HOUR + assert srp_entity.unit_of_measurement == UnitOfEnergy.KILO_WATT_HOUR assert srp_entity.icon == ICON assert srp_entity.usage == "2.00" assert srp_entity.should_poll is False diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 959a35dba78..cf708f2fb4c 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -1,5 +1,5 @@ """Test the SSDP integration.""" -# pylint: disable=protected-access + from datetime import datetime, timedelta from ipaddress import IPv4Address diff --git a/tests/components/starlink/__init__.py b/tests/components/starlink/__init__.py new file mode 100644 index 00000000000..8c55e8f0d99 --- /dev/null +++ b/tests/components/starlink/__init__.py @@ -0,0 +1 @@ +"""Tests for the Starlink integration.""" diff --git a/tests/components/starlink/patchers.py b/tests/components/starlink/patchers.py new file mode 100644 index 00000000000..0013b4e56ba --- /dev/null +++ b/tests/components/starlink/patchers.py @@ -0,0 +1,25 @@ +"""General Starlink patchers.""" +from unittest.mock import patch + +from starlink_grpc import StatusDict + +SETUP_ENTRY_PATCHER = patch( + "homeassistant.components.starlink.async_setup_entry", return_value=True +) + +COORDINATOR_SUCCESS_PATCHER = patch( + "homeassistant.components.starlink.coordinator.status_data", + return_value=[ + StatusDict(id="1", software_version="1", hardware_version="1"), + {}, + {}, + ], +) + +DEVICE_FOUND_PATCHER = patch( + "homeassistant.components.starlink.config_flow.get_id", return_value="some-valid-id" +) + +NO_DEVICE_PATCHER = patch( + "homeassistant.components.starlink.config_flow.get_id", return_value=None +) diff --git a/tests/components/starlink/test_config_flow.py b/tests/components/starlink/test_config_flow.py new file mode 100644 index 00000000000..3bb3f286638 --- /dev/null +++ b/tests/components/starlink/test_config_flow.py @@ -0,0 +1,88 @@ +"""Test the Starlink config flow.""" +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.starlink.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.core import HomeAssistant + +from .patchers import DEVICE_FOUND_PATCHER, NO_DEVICE_PATCHER, SETUP_ENTRY_PATCHER + +from tests.common import MockConfigEntry + + +async def test_flow_user_fails_can_succeed(hass: HomeAssistant) -> None: + """Test user initialized flow can still succeed after failure when Starlink is available.""" + user_input = {CONF_IP_ADDRESS: "192.168.100.1:9200"} + + with NO_DEVICE_PATCHER, SETUP_ENTRY_PATCHER: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + 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"] == data_entry_flow.FlowResultType.FORM + assert result["errors"] + + with DEVICE_FOUND_PATCHER, SETUP_ENTRY_PATCHER: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=user_input, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == user_input + + +async def test_flow_user_success(hass: HomeAssistant) -> None: + """Test user initialized flow succeeds when Starlink is available.""" + user_input = {CONF_IP_ADDRESS: "192.168.100.1:9200"} + + with DEVICE_FOUND_PATCHER, SETUP_ENTRY_PATCHER: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + 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"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == user_input + + +async def test_flow_user_duplicate_abort(hass: HomeAssistant) -> None: + """Test user initialized flow aborts when Starlink is already configured.""" + user_input = {CONF_IP_ADDRESS: "192.168.100.1:9200"} + + entry = MockConfigEntry( + domain=DOMAIN, + data=user_input, + unique_id="some-valid-id", + state=config_entries.ConfigEntryState.LOADED, + ) + entry.add_to_hass(hass) + + with DEVICE_FOUND_PATCHER, SETUP_ENTRY_PATCHER: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + 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"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/starlink/test_init.py b/tests/components/starlink/test_init.py new file mode 100644 index 00000000000..72d3be52b4a --- /dev/null +++ b/tests/components/starlink/test_init.py @@ -0,0 +1,46 @@ +"""Tests Starlink integration init/unload.""" +from homeassistant.components.starlink.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.core import HomeAssistant + +from .patchers import COORDINATOR_SUCCESS_PATCHER + +from tests.common import MockConfigEntry + + +async def test_successful_entry(hass: HomeAssistant) -> None: + """Test configuring Starlink.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_IP_ADDRESS: "1.2.3.4:0000"}, + ) + + with COORDINATOR_SUCCESS_PATCHER: + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + assert entry.entry_id in hass.data[DOMAIN] + + +async def test_unload_entry(hass: HomeAssistant) -> None: + """Test removing Starlink.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_IP_ADDRESS: "1.2.3.4:0000"}, + ) + + with COORDINATOR_SUCCESS_PATCHER: + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + assert entry.entry_id not in hass.data[DOMAIN] diff --git a/tests/components/startca/test_sensor.py b/tests/components/startca/test_sensor.py index 11fc3120b5b..4d3540e7849 100644 --- a/tests/components/startca/test_sensor.py +++ b/tests/components/startca/test_sensor.py @@ -3,7 +3,7 @@ from http import HTTPStatus from homeassistant.bootstrap import async_setup_component from homeassistant.components.startca.sensor import StartcaData -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, DATA_GIGABYTES, PERCENTAGE +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, UnitOfInformation from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -59,47 +59,47 @@ async def test_capped_setup(hass, aioclient_mock): assert state.state == "76.24" state = hass.states.get("sensor.start_ca_usage") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_data_limit") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "400" state = hass.states.get("sensor.start_ca_used_download") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_used_upload") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_used_total") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "311.43" state = hass.states.get("sensor.start_ca_grace_download") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_grace_upload") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_grace_total") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "311.43" state = hass.states.get("sensor.start_ca_total_download") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_total_upload") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_remaining") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "95.05" @@ -155,47 +155,47 @@ async def test_unlimited_setup(hass, aioclient_mock): assert state.state == "0" state = hass.states.get("sensor.start_ca_usage") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "0.0" state = hass.states.get("sensor.start_ca_data_limit") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "inf" state = hass.states.get("sensor.start_ca_used_download") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "0.0" state = hass.states.get("sensor.start_ca_used_upload") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "0.0" state = hass.states.get("sensor.start_ca_used_total") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "0.0" state = hass.states.get("sensor.start_ca_grace_download") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_grace_upload") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_grace_total") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "311.43" state = hass.states.get("sensor.start_ca_total_download") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "304.95" state = hass.states.get("sensor.start_ca_total_upload") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "6.48" state = hass.states.get("sensor.start_ca_remaining") - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_GIGABYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.GIGABYTES assert state.state == "inf" diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index bd73216d69e..7f68ae68973 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -21,7 +21,7 @@ from homeassistant.const import ( SERVICE_RELOAD, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -85,14 +85,14 @@ async def test_sensor_defaults_numeric(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() state = hass.states.get("sensor.test") assert state is not None assert state.state == str(round(sum(VALUES_NUMERIC) / len(VALUES_NUMERIC), 2)) - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT assert state.attributes.get("buffer_usage_ratio") == round(9 / 20, 2) assert state.attributes.get("source_value_valid") is True @@ -113,14 +113,16 @@ async def test_sensor_defaults_numeric(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", "0", - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() new_state = hass.states.get("sensor.test") new_mean = round(sum(VALUES_NUMERIC) / (len(VALUES_NUMERIC) + 1), 2) assert new_state is not None assert new_state.state == str(new_mean) - assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert ( + new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS + ) assert new_state.attributes.get("buffer_usage_ratio") == round(10 / 20, 2) assert new_state.attributes.get("source_value_valid") is True @@ -131,7 +133,9 @@ async def test_sensor_defaults_numeric(hass: HomeAssistant): new_state = hass.states.get("sensor.test") assert new_state is not None assert new_state.state == str(new_mean) - assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert ( + new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS + ) assert new_state.attributes.get("source_value_valid") is False # Source sensor has the STATE_UNKNOWN state, unit and state should not change @@ -141,7 +145,9 @@ async def test_sensor_defaults_numeric(hass: HomeAssistant): new_state = hass.states.get("sensor.test") assert new_state is not None assert new_state.state == str(new_mean) - assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert ( + new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS + ) assert new_state.attributes.get("source_value_valid") is False # Source sensor is removed, unit and state should not change @@ -151,7 +157,9 @@ async def test_sensor_defaults_numeric(hass: HomeAssistant): new_state = hass.states.get("sensor.test") assert new_state is not None assert new_state.state == str(new_mean) - assert new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert ( + new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS + ) assert new_state.attributes.get("source_value_valid") is False @@ -178,7 +186,7 @@ async def test_sensor_defaults_binary(hass: HomeAssistant): hass.states.async_set( "binary_sensor.test_monitored", value, - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -223,12 +231,12 @@ async def test_sensor_source_with_force_update(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored_normal", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) hass.states.async_set( "sensor.test_monitored_force", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, force_update=True, ) await hass.async_block_till_done() @@ -265,7 +273,7 @@ async def test_sampling_size_reduced(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -299,7 +307,7 @@ async def test_sampling_size_1(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -347,7 +355,7 @@ async def test_age_limit_expiry(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -431,7 +439,7 @@ async def test_precision(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -483,7 +491,7 @@ async def test_percentile(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -539,7 +547,7 @@ async def test_device_class(hass: HomeAssistant): "sensor.test_monitored", str(value), { - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE, }, ) @@ -586,7 +594,7 @@ async def test_state_class(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -1024,12 +1032,12 @@ async def test_state_characteristics(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(VALUES_NUMERIC[i]), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) hass.states.async_set( "binary_sensor.test_monitored", str(VALUES_BINARY[i]), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -1126,7 +1134,7 @@ async def test_invalid_state_characteristic(hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(VALUES_NUMERIC[0]), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -1146,7 +1154,7 @@ async def test_initialize_from_database(recorder_mock, hass: HomeAssistant): hass.states.async_set( "sensor.test_monitored", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() await async_wait_recording_done(hass) @@ -1172,7 +1180,7 @@ async def test_initialize_from_database(recorder_mock, hass: HomeAssistant): state = hass.states.get("sensor.test") assert state is not None assert state.state == str(round(sum(VALUES_NUMERIC) / len(VALUES_NUMERIC), 2)) - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.CELSIUS async def test_initialize_from_database_with_maxage(recorder_mock, hass: HomeAssistant): @@ -1201,7 +1209,7 @@ async def test_initialize_from_database_with_maxage(recorder_mock, hass: HomeAss hass.states.async_set( "sensor.test_monitored", str(value), - {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() mock_data["return_time"] += timedelta(hours=1) diff --git a/tests/components/steam_online/test_config_flow.py b/tests/components/steam_online/test_config_flow.py index 1844611530d..a9d81a16fba 100644 --- a/tests/components/steam_online/test_config_flow.py +++ b/tests/components/steam_online/test_config_flow.py @@ -150,11 +150,10 @@ async def test_options_flow(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_ACCOUNTS: [ACCOUNT_1, ACCOUNT_2]}, ) - await hass.async_block_till_done() + await hass.async_block_till_done() assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == CONF_OPTIONS_2 - assert len(er.async_get(hass).entities) == 2 async def test_options_flow_deselect(hass: HomeAssistant) -> None: @@ -165,6 +164,10 @@ async def test_options_flow_deselect(hass: HomeAssistant) -> None: result = await hass.config_entries.options.async_init(entry.entry_id) await hass.async_block_till_done() + with patch_interface(), patch( + "homeassistant.components.steam_online.async_setup_entry", + return_value=True, + ): assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "init" @@ -172,6 +175,7 @@ async def test_options_flow_deselect(hass: HomeAssistant) -> None: result["flow_id"], user_input={CONF_ACCOUNTS: []}, ) + await hass.async_block_till_done() assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_ACCOUNTS: {}} diff --git a/tests/components/steamist/test_sensor.py b/tests/components/steamist/test_sensor.py index 4d83a0eb80d..30dfc419f91 100644 --- a/tests/components/steamist/test_sensor.py +++ b/tests/components/steamist/test_sensor.py @@ -1,7 +1,7 @@ """Tests for the steamist sensos.""" from __future__ import annotations -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, TIME_MINUTES +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfTemperature, UnitOfTime from homeassistant.core import HomeAssistant from . import ( @@ -16,10 +16,10 @@ async def test_steam_active(hass: HomeAssistant) -> None: await _async_setup_entry_with_status(hass, MOCK_ASYNC_GET_STATUS_ACTIVE) state = hass.states.get("sensor.steam_temperature") assert state.state == "39" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS state = hass.states.get("sensor.steam_minutes_remain") assert state.state == "14" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TIME_MINUTES + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTime.MINUTES async def test_steam_inactive(hass: HomeAssistant) -> None: @@ -27,7 +27,7 @@ async def test_steam_inactive(hass: HomeAssistant) -> None: await _async_setup_entry_with_status(hass, MOCK_ASYNC_GET_STATUS_INACTIVE) state = hass.states.get("sensor.steam_temperature") assert state.state == "21" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS state = hass.states.get("sensor.steam_minutes_remain") assert state.state == "0" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TIME_MINUTES + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTime.MINUTES diff --git a/tests/components/stookalert/test_config_flow.py b/tests/components/stookalert/test_config_flow.py index 50cd56341d6..aeff4b01de9 100644 --- a/tests/components/stookalert/test_config_flow.py +++ b/tests/components/stookalert/test_config_flow.py @@ -17,7 +17,6 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result with patch( "homeassistant.components.stookalert.async_setup_entry", return_value=True @@ -48,8 +47,6 @@ async def test_already_configured(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER} ) - assert "flow_id" in result - result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ diff --git a/tests/components/stookwijzer/__init__.py b/tests/components/stookwijzer/__init__.py new file mode 100644 index 00000000000..af7da13c8a6 --- /dev/null +++ b/tests/components/stookwijzer/__init__.py @@ -0,0 +1 @@ +"""Tests for the Stookwijzer integration.""" diff --git a/tests/components/stookwijzer/test_config_flow.py b/tests/components/stookwijzer/test_config_flow.py new file mode 100644 index 00000000000..b18eb54b322 --- /dev/null +++ b/tests/components/stookwijzer/test_config_flow.py @@ -0,0 +1,42 @@ +"""Tests for the Stookwijzer config flow.""" +from unittest.mock import patch + +from homeassistant.components.stookwijzer.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def test_full_user_flow(hass: HomeAssistant) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == FlowResultType.FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + with patch( + "homeassistant.components.stookwijzer.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LOCATION: { + CONF_LATITUDE: 1.0, + CONF_LONGITUDE: 1.1, + } + }, + ) + + assert result2.get("type") == FlowResultType.CREATE_ENTRY + assert result2.get("data") == { + "location": { + "latitude": 1.0, + "longitude": 1.1, + }, + } + + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index a5a1f00d90a..22a7627c062 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -58,6 +58,7 @@ STREAM_SOURCE = "some-stream-source" AUDIO_STREAM_FORMAT = "mp3" VIDEO_STREAM_FORMAT = "h264" VIDEO_FRAME_RATE = 12 +VIDEO_TIME_BASE = fractions.Fraction(1 / 90000) AUDIO_SAMPLE_RATE = 11025 KEYFRAME_INTERVAL = 1 # in seconds PACKET_DURATION = fractions.Fraction(1, VIDEO_FRAME_RATE) # in seconds @@ -97,10 +98,10 @@ def mock_stream_settings(hass): class FakeAvInputStream: """A fake pyav Stream.""" - def __init__(self, name, rate): + def __init__(self, name, time_base): """Initialize the stream.""" self.name = name - self.time_base = fractions.Fraction(1, rate) + self.time_base = time_base self.profile = "ignored-profile" class FakeCodec: @@ -124,8 +125,10 @@ class FakeAvInputStream: return f"FakePyAvStream<{self.name}, {self.time_base}>" -VIDEO_STREAM = FakeAvInputStream(VIDEO_STREAM_FORMAT, VIDEO_FRAME_RATE) -AUDIO_STREAM = FakeAvInputStream(AUDIO_STREAM_FORMAT, AUDIO_SAMPLE_RATE) +VIDEO_STREAM = FakeAvInputStream(VIDEO_STREAM_FORMAT, VIDEO_TIME_BASE) +AUDIO_STREAM = FakeAvInputStream( + AUDIO_STREAM_FORMAT, fractions.Fraction(1 / AUDIO_SAMPLE_RATE) +) class PacketSequence: @@ -158,10 +161,10 @@ class PacketSequence: def __init__(self): super().__init__(3) - time_base = fractions.Fraction(1, VIDEO_FRAME_RATE) - dts = int(self.packet * PACKET_DURATION / time_base) - pts = int(self.packet * PACKET_DURATION / time_base) - duration = int(PACKET_DURATION / time_base) + time_base = VIDEO_TIME_BASE + dts = round(self.packet * PACKET_DURATION / time_base) + pts = round(self.packet * PACKET_DURATION / time_base) + duration = round(PACKET_DURATION / time_base) stream = VIDEO_STREAM # Pretend we get 1 keyframe every second is_keyframe = not (self.packet - 1) % (VIDEO_FRAME_RATE * KEYFRAME_INTERVAL) @@ -405,7 +408,9 @@ async def test_discard_old_packets(hass): packets = list(PacketSequence(TEST_SEQUENCE_LENGTH)) # Packets after this one are considered out of order - packets[OUT_OF_ORDER_PACKET_INDEX - 1].dts = 9090 + packets[OUT_OF_ORDER_PACKET_INDEX - 1].dts = round( + TEST_SEQUENCE_LENGTH / VIDEO_FRAME_RATE / VIDEO_TIME_BASE + ) decoded_stream = await async_decode_stream(hass, packets) segments = decoded_stream.segments @@ -430,7 +435,7 @@ async def test_packet_overflow(hass): packets[OUT_OF_ORDER_PACKET_INDEX].dts = -9000000 py_av = MockPyAv() - with pytest.raises(StreamWorkerError, match=r"Timestamp overflow detected"): + with pytest.raises(StreamWorkerError, match=r"Timestamp discontinuity detected"): await async_decode_stream(hass, packets, py_av=py_av) decoded_stream = py_av.capture_buffer segments = decoded_stream.segments @@ -578,12 +583,12 @@ async def test_audio_is_first_packet(hass): packets = list(PacketSequence(num_packets)) # Pair up an audio packet for each video packet packets[0].stream = AUDIO_STREAM - packets[0].dts = int(packets[1].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) - packets[0].pts = int(packets[1].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) + packets[0].dts = round(packets[1].dts * VIDEO_TIME_BASE * AUDIO_SAMPLE_RATE) + packets[0].pts = round(packets[1].pts * VIDEO_TIME_BASE * AUDIO_SAMPLE_RATE) packets[1].is_keyframe = True # Move the video keyframe from packet 0 to packet 1 packets[2].stream = AUDIO_STREAM - packets[2].dts = int(packets[3].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) - packets[2].pts = int(packets[3].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) + packets[2].dts = round(packets[3].dts * VIDEO_TIME_BASE * AUDIO_SAMPLE_RATE) + packets[2].pts = round(packets[3].pts * VIDEO_TIME_BASE * AUDIO_SAMPLE_RATE) decoded_stream = await async_decode_stream(hass, packets, py_av=py_av) complete_segments = decoded_stream.complete_segments @@ -600,8 +605,8 @@ async def test_audio_packets_found(hass): num_packets = PACKETS_TO_WAIT_FOR_AUDIO + 1 packets = list(PacketSequence(num_packets)) packets[1].stream = AUDIO_STREAM - packets[1].dts = int(packets[0].dts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) - packets[1].pts = int(packets[0].pts / VIDEO_FRAME_RATE * AUDIO_SAMPLE_RATE) + packets[1].dts = round(packets[0].dts * VIDEO_TIME_BASE * AUDIO_SAMPLE_RATE) + packets[1].pts = round(packets[0].pts * VIDEO_TIME_BASE * AUDIO_SAMPLE_RATE) decoded_stream = await async_decode_stream(hass, packets, py_av=py_av) complete_segments = decoded_stream.complete_segments diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py index 62f69017a82..0a48c736ef7 100644 --- a/tests/components/subaru/test_config_flow.py +++ b/tests/components/subaru/test_config_flow.py @@ -1,5 +1,4 @@ """Tests for the Subaru component config flow.""" -# pylint: disable=redefined-outer-name from copy import deepcopy from unittest import mock from unittest.mock import PropertyMock, patch diff --git a/tests/components/sun/test_config_flow.py b/tests/components/sun/test_config_flow.py index 7d20a57ba27..2d4e2d83249 100644 --- a/tests/components/sun/test_config_flow.py +++ b/tests/components/sun/test_config_flow.py @@ -19,7 +19,6 @@ async def test_full_user_flow(hass: HomeAssistant) -> None: assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result with patch( "homeassistant.components.sun.async_setup_entry", diff --git a/tests/components/synology_dsm/conftest.py b/tests/components/synology_dsm/conftest.py index 74de072e229..315ae7a5c2a 100644 --- a/tests/components/synology_dsm/conftest.py +++ b/tests/components/synology_dsm/conftest.py @@ -1,5 +1,5 @@ """Configure Synology DSM tests.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest @@ -21,3 +21,17 @@ def bypass_setup_fixture(request): "homeassistant.components.synology_dsm.async_setup_entry", return_value=True ): yield + + +@pytest.fixture(name="mock_dsm") +def fixture_dsm(): + """Set up SynologyDSM API fixture.""" + with patch("homeassistant.components.synology_dsm.common.SynologyDSM") as dsm: + dsm.login = AsyncMock(return_value=True) + dsm.update = AsyncMock(return_value=True) + + dsm.network.update = AsyncMock(return_value=True) + dsm.surveillance_station.update = AsyncMock(return_value=True) + dsm.upgrade.update = AsyncMock(return_value=True) + + yield dsm diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 25259ac7ee9..402dcd2f602 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -1,5 +1,5 @@ """Tests for the Synology DSM config flow.""" -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from synology_dsm.exceptions import ( @@ -11,7 +11,7 @@ from synology_dsm.exceptions import ( ) from homeassistant import data_entry_flow -from homeassistant.components import ssdp +from homeassistant.components import ssdp, zeroconf from homeassistant.components.synology_dsm.config_flow import CONF_OTP_CODE from homeassistant.components.synology_dsm.const import ( CONF_SNAPSHOT_QUALITY, @@ -25,7 +25,12 @@ from homeassistant.components.synology_dsm.const import ( DEFAULT_VERIFY_SSL, DOMAIN, ) -from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_SSDP, SOURCE_USER +from homeassistant.config_entries import ( + SOURCE_REAUTH, + SOURCE_SSDP, + SOURCE_USER, + SOURCE_ZEROCONF, +) from homeassistant.const import ( CONF_DISKS, CONF_HOST, @@ -59,60 +64,89 @@ from tests.common import MockConfigEntry @pytest.fixture(name="service") def mock_controller_service(): """Mock a successful service.""" - with patch( - "homeassistant.components.synology_dsm.config_flow.SynologyDSM" - ) as service_mock: - service_mock.return_value.information.serial = SERIAL - service_mock.return_value.utilisation.cpu_user_load = 1 - service_mock.return_value.storage.disks_ids = ["sda", "sdb", "sdc"] - service_mock.return_value.storage.volumes_ids = ["volume_1"] - service_mock.return_value.network.macs = MACS - yield service_mock + with patch("homeassistant.components.synology_dsm.config_flow.SynologyDSM") as dsm: + + dsm.login = AsyncMock(return_value=True) + dsm.update = AsyncMock(return_value=True) + + dsm.surveillance_station.update = AsyncMock(return_value=True) + dsm.upgrade.update = AsyncMock(return_value=True) + dsm.utilisation = Mock(cpu_user_load=1, update=AsyncMock(return_value=True)) + dsm.network = Mock(update=AsyncMock(return_value=True), macs=MACS) + dsm.storage = Mock( + disks_ids=["sda", "sdb", "sdc"], + volumes_ids=["volume_1"], + update=AsyncMock(return_value=True), + ) + dsm.information = Mock(serial=SERIAL) + + yield dsm @pytest.fixture(name="service_2sa") def mock_controller_service_2sa(): """Mock a successful service with 2SA login.""" - with patch( - "homeassistant.components.synology_dsm.config_flow.SynologyDSM" - ) as service_mock: - service_mock.return_value.login = Mock( + with patch("homeassistant.components.synology_dsm.config_flow.SynologyDSM") as dsm: + dsm.login = AsyncMock( side_effect=SynologyDSMLogin2SARequiredException(USERNAME) ) - service_mock.return_value.information.serial = SERIAL - service_mock.return_value.utilisation.cpu_user_load = 1 - service_mock.return_value.storage.disks_ids = ["sda", "sdb", "sdc"] - service_mock.return_value.storage.volumes_ids = ["volume_1"] - service_mock.return_value.network.macs = MACS - yield service_mock + dsm.update = AsyncMock(return_value=True) + + dsm.surveillance_station.update = AsyncMock(return_value=True) + dsm.upgrade.update = AsyncMock(return_value=True) + dsm.utilisation = Mock(cpu_user_load=1, update=AsyncMock(return_value=True)) + dsm.network = Mock(update=AsyncMock(return_value=True), macs=MACS) + dsm.storage = Mock( + disks_ids=["sda", "sdb", "sdc"], + volumes_ids=["volume_1"], + update=AsyncMock(return_value=True), + ) + dsm.information = Mock(serial=SERIAL) + yield dsm @pytest.fixture(name="service_vdsm") def mock_controller_service_vdsm(): """Mock a successful service.""" - with patch( - "homeassistant.components.synology_dsm.config_flow.SynologyDSM" - ) as service_mock: - service_mock.return_value.information.serial = SERIAL - service_mock.return_value.utilisation.cpu_user_load = 1 - service_mock.return_value.storage.disks_ids = [] - service_mock.return_value.storage.volumes_ids = ["volume_1"] - service_mock.return_value.network.macs = MACS - yield service_mock + with patch("homeassistant.components.synology_dsm.config_flow.SynologyDSM") as dsm: + + dsm.login = AsyncMock(return_value=True) + dsm.update = AsyncMock(return_value=True) + + dsm.surveillance_station.update = AsyncMock(return_value=True) + dsm.upgrade.update = AsyncMock(return_value=True) + dsm.utilisation = Mock(cpu_user_load=1, update=AsyncMock(return_value=True)) + dsm.network = Mock(update=AsyncMock(return_value=True), macs=MACS) + dsm.storage = Mock( + disks_ids=[], + volumes_ids=["volume_1"], + update=AsyncMock(return_value=True), + ) + dsm.information = Mock(serial=SERIAL) + + yield dsm @pytest.fixture(name="service_failed") def mock_controller_service_failed(): """Mock a failed service.""" - with patch( - "homeassistant.components.synology_dsm.config_flow.SynologyDSM" - ) as service_mock: - service_mock.return_value.information.serial = None - service_mock.return_value.utilisation.cpu_user_load = None - service_mock.return_value.storage.disks_ids = [] - service_mock.return_value.storage.volumes_ids = [] - service_mock.return_value.network.macs = [] - yield service_mock + with patch("homeassistant.components.synology_dsm.config_flow.SynologyDSM") as dsm: + + dsm.login = AsyncMock(return_value=True) + dsm.update = AsyncMock(return_value=True) + + dsm.surveillance_station.update = AsyncMock(return_value=True) + dsm.upgrade.update = AsyncMock(return_value=True) + dsm.utilisation = Mock(cpu_user_load=None, update=AsyncMock(return_value=True)) + dsm.network = Mock(update=AsyncMock(return_value=True), macs=[]) + dsm.storage = Mock( + disks_ids=[], + volumes_ids=[], + update=AsyncMock(return_value=True), + ) + dsm.information = Mock(serial=None) + + yield dsm async def test_user(hass: HomeAssistant, service: MagicMock): @@ -123,19 +157,23 @@ async def test_user(hass: HomeAssistant, service: MagicMock): assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - # test with all provided - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={ - CONF_HOST: HOST, - CONF_PORT: PORT, - CONF_SSL: USE_SSL, - CONF_VERIFY_SSL: VERIFY_SSL, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - }, - ) + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service, + ): + # test with all provided + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_SSL: USE_SSL, + CONF_VERIFY_SSL: VERIFY_SSL, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + }, + ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST @@ -150,19 +188,23 @@ async def test_user(hass: HomeAssistant, service: MagicMock): assert result["data"].get(CONF_DISKS) is None assert result["data"].get(CONF_VOLUMES) is None - service.return_value.information.serial = SERIAL_2 - # test without port + False SSL - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={ - CONF_HOST: HOST, - CONF_SSL: False, - CONF_VERIFY_SSL: VERIFY_SSL, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - }, - ) + service.information.serial = SERIAL_2 + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service, + ): + # test without port + False SSL + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_HOST: HOST, + CONF_SSL: False, + CONF_VERIFY_SSL: VERIFY_SSL, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + }, + ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL_2 assert result["title"] == HOST @@ -180,11 +222,15 @@ async def test_user(hass: HomeAssistant, service: MagicMock): async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock): """Test user with 2sa authentication config.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service_2sa, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "2sa" @@ -200,11 +246,16 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock): assert result["errors"] == {CONF_OTP_CODE: "otp_failed"} # Successful login with 2SA code - service_2sa.return_value.login = Mock(return_value=True) - service_2sa.return_value.device_token = DEVICE_TOKEN - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_OTP_CODE: "123456"} - ) + service_2sa.login = AsyncMock(return_value=True) + service_2sa.device_token = DEVICE_TOKEN + + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service_2sa, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_OTP_CODE: "123456"} + ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL @@ -223,25 +274,33 @@ async def test_user_2sa(hass: HomeAssistant, service_2sa: MagicMock): async def test_user_vdsm(hass: HomeAssistant, service_vdsm: MagicMock): """Test user config.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=None - ) + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service_vdsm, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=None + ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "user" - # test with all provided - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={ - CONF_HOST: HOST, - CONF_PORT: PORT, - CONF_SSL: USE_SSL, - CONF_VERIFY_SSL: VERIFY_SSL, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - }, - ) + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service_vdsm, + ): + # test with all provided + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_SSL: USE_SSL, + CONF_VERIFY_SSL: VERIFY_SSL, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + }, + ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL assert result["title"] == HOST @@ -289,9 +348,13 @@ async def test_reauth(hass: HomeAssistant, service: MagicMock): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "reauth_confirm" + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service, + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -299,8 +362,8 @@ async def test_reauth(hass: HomeAssistant, service: MagicMock): CONF_PASSWORD: PASSWORD, }, ) - assert result["type"] == data_entry_flow.FlowResultType.ABORT - assert result["reason"] == "reauth_successful" + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "reauth_successful" async def test_reconfig_user(hass: HomeAssistant, service: MagicMock): @@ -318,6 +381,9 @@ async def test_reconfig_user(hass: HomeAssistant, service: MagicMock): with patch( "homeassistant.config_entries.ConfigEntries.async_reload", return_value=True, + ), patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service, ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -375,11 +441,15 @@ async def test_unknown_failed(hass: HomeAssistant, service: MagicMock): async def test_missing_data_after_login(hass: HomeAssistant, service_failed: MagicMock): """Test when we have errors during connection.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, - ) + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service_failed, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: HOST, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}, + ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == {"base": "missing_data"} @@ -404,9 +474,13 @@ async def test_form_ssdp(hass: HomeAssistant, service: MagicMock): assert result["step_id"] == "link" assert result["errors"] == {} - result = await hass.config_entries.flow.async_configure( - result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} - ) + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["result"].unique_id == SERIAL @@ -560,3 +634,70 @@ async def test_options_flow(hass: HomeAssistant, service: MagicMock): assert config_entry.options[CONF_SCAN_INTERVAL] == 2 assert config_entry.options[CONF_TIMEOUT] == 30 assert config_entry.options[CONF_SNAPSHOT_QUALITY] == 0 + + +async def test_discovered_via_zeroconf(hass: HomeAssistant, service: MagicMock): + """Test we can setup from zeroconf.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.5", + addresses=["192.168.1.5"], + port=5000, + hostname="mydsm.local.", + type="_http._tcp.local.", + name="mydsm._http._tcp.local.", + properties={ + "mac_address": "00:11:32:XX:XX:99|00:11:22:33:44:55", # MAC address, but SSDP does not have `-` + }, + ), + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "link" + assert result["errors"] == {} + + with patch( + "homeassistant.components.synology_dsm.config_flow.SynologyDSM", + return_value=service, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD} + ) + + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["result"].unique_id == SERIAL + assert result["title"] == "mydsm" + assert result["data"][CONF_HOST] == "192.168.1.5" + assert result["data"][CONF_PORT] == 5001 + assert result["data"][CONF_SSL] == DEFAULT_USE_SSL + assert result["data"][CONF_VERIFY_SSL] == DEFAULT_VERIFY_SSL + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_MAC] == MACS + assert result["data"].get("device_token") is None + assert result["data"].get(CONF_DISKS) is None + assert result["data"].get(CONF_VOLUMES) is None + + +async def test_discovered_via_zeroconf_missing_mac( + hass: HomeAssistant, service: MagicMock +): + """Test we abort if the mac address is missing.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + host="192.168.1.5", + addresses=["192.168.1.5"], + port=5000, + hostname="mydsm.local.", + type="_http._tcp.local.", + name="mydsm._http._tcp.local.", + properties={}, + ), + ) + assert result["type"] == data_entry_flow.FlowResultType.ABORT + assert result["reason"] == "no_mac_address" diff --git a/tests/components/synology_dsm/test_init.py b/tests/components/synology_dsm/test_init.py index da2916b8c81..fe383bbfeab 100644 --- a/tests/components/synology_dsm/test_init.py +++ b/tests/components/synology_dsm/test_init.py @@ -1,5 +1,5 @@ """Tests for the Synology DSM component.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest from synology_dsm.exceptions import SynologyDSMLoginInvalidException @@ -22,11 +22,12 @@ from tests.common import MockConfigEntry @pytest.mark.no_bypass_setup -async def test_services_registered(hass: HomeAssistant): +async def test_services_registered(hass: HomeAssistant, mock_dsm: MagicMock): """Test if all services are registered.""" - with patch("homeassistant.components.synology_dsm.common.SynologyDSM"), patch( - "homeassistant.components.synology_dsm.PLATFORMS", return_value=[] - ): + with patch( + "homeassistant.components.synology_dsm.common.SynologyDSM", + return_value=mock_dsm, + ), patch("homeassistant.components.synology_dsm.PLATFORMS", return_value=[]): entry = MockConfigEntry( domain=DOMAIN, data={ diff --git a/tests/components/tado/test_climate.py b/tests/components/tado/test_climate.py index 05471d9060e..ca1ed285df9 100644 --- a/tests/components/tado/test_climate.py +++ b/tests/components/tado/test_climate.py @@ -79,7 +79,7 @@ async def test_smartac_with_swing(hass): "min_temp": 16.0, "preset_mode": "home", "preset_modes": ["away", "home"], - "swing_modes": ["ON", "OFF"], + "swing_modes": ["on", "off"], "supported_features": 57, "target_temp_step": 1.0, "temperature": 20.0, diff --git a/tests/components/tailscale/test_config_flow.py b/tests/components/tailscale/test_config_flow.py index 78e3f20a61b..45e3e85d878 100644 --- a/tests/components/tailscale/test_config_flow.py +++ b/tests/components/tailscale/test_config_flow.py @@ -25,7 +25,6 @@ async def test_full_user_flow( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -62,7 +61,6 @@ async def test_full_flow_with_authentication_error( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_tailscale_config_flow.devices.side_effect = TailscaleAuthenticationError result2 = await hass.config_entries.flow.async_configure( @@ -76,7 +74,6 @@ async def test_full_flow_with_authentication_error( assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": "invalid_auth"} - assert "flow_id" in result2 assert len(mock_setup_entry.mock_calls) == 0 assert len(mock_tailscale_config_flow.devices.mock_calls) == 1 @@ -142,7 +139,6 @@ async def test_reauth_flow( ) assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -185,7 +181,6 @@ async def test_reauth_with_authentication_error( ) assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" - assert "flow_id" in result mock_tailscale_config_flow.devices.side_effect = TailscaleAuthenticationError result2 = await hass.config_entries.flow.async_configure( @@ -197,7 +192,6 @@ async def test_reauth_with_authentication_error( assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_confirm" assert result2.get("errors") == {"base": "invalid_auth"} - assert "flow_id" in result2 assert len(mock_setup_entry.mock_calls) == 0 assert len(mock_tailscale_config_flow.devices.mock_calls) == 1 @@ -239,7 +233,6 @@ async def test_reauth_api_error( ) assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "reauth_confirm" - assert "flow_id" in result mock_tailscale_config_flow.devices.side_effect = TailscaleConnectionError result2 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/tcp/test_sensor.py b/tests/components/tcp/test_sensor.py index 46db3367677..7d7bad5c58d 100644 --- a/tests/components/tcp/test_sensor.py +++ b/tests/components/tcp/test_sensor.py @@ -18,8 +18,8 @@ TEST_CONFIG = { tcp.CONF_TIMEOUT: tcp.DEFAULT_TIMEOUT + 1, tcp.CONF_PAYLOAD: "test_payload", tcp.CONF_UNIT_OF_MEASUREMENT: "test_unit", - tcp.CONF_VALUE_TEMPLATE: "{{ 'test_' + value }}", - tcp.CONF_VALUE_ON: "test_on", + tcp.CONF_VALUE_TEMPLATE: "{{ '7.' + value }}", + tcp.CONF_VALUE_ON: "7.on", tcp.CONF_BUFFER_SIZE: tcp.DEFAULT_BUFFER_SIZE + 1, } } @@ -35,7 +35,7 @@ KEYS_AND_DEFAULTS = { tcp.CONF_BUFFER_SIZE: tcp.DEFAULT_BUFFER_SIZE, } -socket_test_value = "value" +socket_test_value = "123" @pytest.fixture(name="mock_socket") @@ -64,7 +64,7 @@ def mock_ssl_context_fixture(): "homeassistant.components.tcp.common.ssl.create_default_context", ) as mock_ssl_context: mock_ssl_context.return_value.wrap_socket.return_value.recv.return_value = ( - socket_test_value + "_ssl" + socket_test_value + "567" ).encode() yield mock_ssl_context @@ -93,7 +93,7 @@ async def test_state(hass, mock_socket, mock_select): state = hass.states.get(TEST_ENTITY) assert state - assert state.state == "test_value" + assert state.state == "7.123" assert ( state.attributes["unit_of_measurement"] == SENSOR_TEST_CONFIG[tcp.CONF_UNIT_OF_MEASUREMENT] @@ -125,7 +125,7 @@ async def test_config_uses_defaults(hass, mock_socket): state = hass.states.get("sensor.tcp_sensor") assert state - assert state.state == "value" + assert state.state == "123" for key, default in KEYS_AND_DEFAULTS.items(): assert result_config["sensor"][0].get(key) == default @@ -184,7 +184,7 @@ async def test_ssl_state(hass, mock_socket, mock_select, mock_ssl_context): state = hass.states.get(TEST_ENTITY) assert state - assert state.state == "test_value_ssl" + assert state.state == "7.123567" assert mock_socket.connect.called assert mock_socket.connect.call_args == call( (SENSOR_TEST_CONFIG["host"], SENSOR_TEST_CONFIG["port"]) @@ -216,7 +216,7 @@ async def test_ssl_state_verify_off(hass, mock_socket, mock_select, mock_ssl_con state = hass.states.get(TEST_ENTITY) assert state - assert state.state == "test_value_ssl" + assert state.state == "7.123567" assert mock_socket.connect.called assert mock_socket.connect.call_args == call( (SENSOR_TEST_CONFIG["host"], SENSOR_TEST_CONFIG["port"]) diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 19d43f08d2b..70e80ba5027 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -305,6 +305,8 @@ async def test_template_attribute_missing(hass, start_ha): ) async def test_setup_valid_device_class(hass, start_ha): """Test setup with valid device_class.""" + hass.states.async_set("sensor.test_sensor", "75") + await hass.async_block_till_done() assert hass.states.get("sensor.test1").attributes["device_class"] == "temperature" assert "device_class" not in hass.states.get("sensor.test2").attributes @@ -607,7 +609,7 @@ async def test_sun_renders_once_per_sensor(hass, start_ha): def _record_async_render(self, *args, **kwargs): """Catch async_render.""" async_render_calls.append(self.template) - return "mocked" + return "75" later = dt_util.utcnow() @@ -615,8 +617,8 @@ async def test_sun_renders_once_per_sensor(hass, start_ha): hass.states.async_set("sun.sun", {"elevation": 50, "next_rising": later}) await hass.async_block_till_done() - assert hass.states.get("sensor.solar_angle").state == "mocked" - assert hass.states.get("sensor.sunrise").state == "mocked" + assert hass.states.get("sensor.solar_angle").state == "75" + assert hass.states.get("sensor.sunrise").state == "75" assert len(async_render_calls) == 2 assert set(async_render_calls) == { diff --git a/tests/components/thread/__init__.py b/tests/components/thread/__init__.py new file mode 100644 index 00000000000..4643d876d9e --- /dev/null +++ b/tests/components/thread/__init__.py @@ -0,0 +1 @@ +"""Tests for the Thread integration.""" diff --git a/tests/components/thread/conftest.py b/tests/components/thread/conftest.py new file mode 100644 index 00000000000..37555d07a90 --- /dev/null +++ b/tests/components/thread/conftest.py @@ -0,0 +1,22 @@ +"""Test fixtures for the Thread integration.""" + +import pytest + +from homeassistant.components import thread + +from tests.common import MockConfigEntry + +CONFIG_ENTRY_DATA = {} + + +@pytest.fixture(name="thread_config_entry") +async def thread_config_entry_fixture(hass): + """Mock Thread config entry.""" + config_entry = MockConfigEntry( + data=CONFIG_ENTRY_DATA, + domain=thread.DOMAIN, + options={}, + title="Thread", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/thread/test_config_flow.py b/tests/components/thread/test_config_flow.py new file mode 100644 index 00000000000..4b27144166b --- /dev/null +++ b/tests/components/thread/test_config_flow.py @@ -0,0 +1,29 @@ +"""Test the Thread config flow.""" +from unittest.mock import patch + +from homeassistant.components import thread +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def test_import(hass: HomeAssistant) -> None: + """Test the import flow.""" + with patch( + "homeassistant.components.thread.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + thread.DOMAIN, context={"source": "import"} + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "Thread" + assert result["data"] == {} + assert result["options"] == {} + assert len(mock_setup_entry.mock_calls) == 1 + + config_entry = hass.config_entries.async_entries(thread.DOMAIN)[0] + assert config_entry.data == {} + assert config_entry.options == {} + assert config_entry.title == "Thread" + assert config_entry.unique_id is None diff --git a/tests/components/thread/test_init.py b/tests/components/thread/test_init.py new file mode 100644 index 00000000000..c529f18d138 --- /dev/null +++ b/tests/components/thread/test_init.py @@ -0,0 +1,29 @@ +"""Test the Thread integration.""" + +from homeassistant.components import thread +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def test_create_entry(hass: HomeAssistant): + """Test an entry is created by async_setup.""" + assert len(hass.config_entries.async_entries(thread.DOMAIN)) == 0 + assert await async_setup_component(hass, thread.DOMAIN, {}) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(thread.DOMAIN)) == 1 + + +async def test_remove_entry(hass: HomeAssistant, thread_config_entry): + """Test removing the entry.""" + + config_entry = hass.config_entries.async_entries(thread.DOMAIN)[0] + assert await hass.config_entries.async_remove(config_entry.entry_id) == { + "require_restart": False + } + + +async def test_import_once(hass: HomeAssistant, thread_config_entry) -> None: + """Test only a single entry is created.""" + await hass.async_block_till_done() + assert len(hass.config_entries.async_entries(thread.DOMAIN)) == 1 diff --git a/tests/components/threshold/test_binary_sensor.py b/tests/components/threshold/test_binary_sensor.py index b7c4a871068..ae5974e0d37 100644 --- a/tests/components/threshold/test_binary_sensor.py +++ b/tests/components/threshold/test_binary_sensor.py @@ -4,7 +4,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfTemperature, ) from homeassistant.setup import async_setup_component @@ -23,7 +23,9 @@ async def test_sensor_upper(hass): await hass.async_block_till_done() hass.states.async_set( - "sensor.test_monitored", 16, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "sensor.test_monitored", + 16, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -156,7 +158,9 @@ async def test_sensor_in_range_no_hysteresis(hass): await hass.async_block_till_done() hass.states.async_set( - "sensor.test_monitored", 16, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "sensor.test_monitored", + 16, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -205,7 +209,9 @@ async def test_sensor_in_range_with_hysteresis(hass): await hass.async_block_till_done() hass.states.async_set( - "sensor.test_monitored", 16, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "sensor.test_monitored", + 16, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() @@ -303,7 +309,9 @@ async def test_sensor_in_range_unknown_state(hass, caplog): await hass.async_block_till_done() hass.states.async_set( - "sensor.test_monitored", 16, {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS} + "sensor.test_monitored", + 16, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, ) await hass.async_block_till_done() diff --git a/tests/components/tile/conftest.py b/tests/components/tile/conftest.py index 474c784ec3c..187d8f3a2c1 100644 --- a/tests/components/tile/conftest.py +++ b/tests/components/tile/conftest.py @@ -7,10 +7,12 @@ from pytile.tile import Tile from homeassistant.components.tile.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry, load_fixture +TEST_PASSWORD = "123abc" +TEST_USERNAME = "user@host.com" + @pytest.fixture(name="api") def api_fixture(hass, data_tile_details): @@ -34,11 +36,11 @@ def config_entry_fixture(hass, config): @pytest.fixture(name="config") -def config_fixture(hass): +def config_fixture(): """Define a config entry data fixture.""" return { - CONF_USERNAME: "user@host.com", - CONF_PASSWORD: "123abc", + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, } @@ -48,14 +50,18 @@ def data_tile_details_fixture(): return json.loads(load_fixture("tile_details_data.json", "tile")) -@pytest.fixture(name="setup_tile") -async def setup_tile_fixture(hass, api, config): - """Define a fixture to set up Tile.""" +@pytest.fixture(name="mock_pytile") +async def mock_pytile_fixture(api): + """Define a fixture to patch pytile.""" with patch( "homeassistant.components.tile.config_flow.async_login", return_value=api - ), patch("homeassistant.components.tile.async_login", return_value=api), patch( - "homeassistant.components.tile.PLATFORMS", [] - ): - assert await async_setup_component(hass, DOMAIN, config) - await hass.async_block_till_done() + ), patch("homeassistant.components.tile.async_login", return_value=api): yield + + +@pytest.fixture(name="setup_config_entry") +async def setup_config_entry_fixture(hass, config_entry, mock_pytile): + """Define a fixture to set up tile.""" + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + yield diff --git a/tests/components/tile/test_config_flow.py b/tests/components/tile/test_config_flow.py index 9200ed3f382..ff17f9b26e1 100644 --- a/tests/components/tile/test_config_flow.py +++ b/tests/components/tile/test_config_flow.py @@ -1,5 +1,5 @@ """Define tests for the Tile config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest from pytile.errors import InvalidAuthError, TileError @@ -9,8 +9,50 @@ from homeassistant.components.tile import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from .conftest import TEST_PASSWORD, TEST_USERNAME -async def test_duplicate_error(hass, config, config_entry): + +@pytest.mark.parametrize( + "mock_login_response,errors", + [ + (AsyncMock(side_effect=InvalidAuthError), {"base": "invalid_auth"}), + (AsyncMock(side_effect=TileError), {"base": "unknown"}), + ], +) +async def test_create_entry( + hass, api, config, errors, mock_login_response, mock_pytile +): + """Test creating an entry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + + # Test errors that can arise: + with patch( + "homeassistant.components.tile.config_flow.async_login", mock_login_response + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == errors + + # Test that we can recover from login errors: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=config + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_USERNAME + assert result["data"] == { + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + } + + +async def test_duplicate_error(hass, config, setup_config_entry): """Test that errors are shown when duplicates are added.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=config @@ -19,40 +61,20 @@ async def test_duplicate_error(hass, config, config_entry): assert result["reason"] == "already_configured" -@pytest.mark.parametrize( - "err,err_string", - [ - (InvalidAuthError, "invalid_auth"), - (TileError, "unknown"), - ], -) -async def test_errors(hass, config, err, err_string): - """Test that errors are handled correctly.""" - with patch( - "homeassistant.components.tile.config_flow.async_login", - side_effect=err, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["errors"] == {"base": err_string} - - -async def test_step_import(hass, config, setup_tile): - """Test that the import step works.""" +async def test_import_entry(hass, config, mock_pytile): + """Test importing an entry.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "user@host.com" + assert result["title"] == TEST_USERNAME assert result["data"] == { - CONF_USERNAME: "user@host.com", - CONF_PASSWORD: "123abc", + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, } -async def test_step_reauth(hass, config, config_entry, setup_tile): +async def test_step_reauth(hass, config, config_entry, setup_config_entry): """Test that the reauth step works.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_REAUTH}, data=config @@ -69,22 +91,3 @@ async def test_step_reauth(hass, config, config_entry, setup_tile): assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "reauth_successful" assert len(hass.config_entries.async_entries()) == 1 - - -async def test_step_user(hass, config, setup_tile): - """Test that the user step works.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} - ) - assert result["type"] == data_entry_flow.FlowResultType.FORM - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config - ) - assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY - assert result["title"] == "user@host.com" - assert result["data"] == { - CONF_USERNAME: "user@host.com", - CONF_PASSWORD: "123abc", - } diff --git a/tests/components/tile/test_diagnostics.py b/tests/components/tile/test_diagnostics.py index e9cf0d34d77..d2193519975 100644 --- a/tests/components/tile/test_diagnostics.py +++ b/tests/components/tile/test_diagnostics.py @@ -4,7 +4,7 @@ from homeassistant.components.diagnostics import REDACTED from tests.components.diagnostics import get_diagnostics_for_config_entry -async def test_entry_diagnostics(hass, config_entry, hass_client, setup_tile): +async def test_entry_diagnostics(hass, config_entry, hass_client, setup_config_entry): """Test config entry diagnostics.""" assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "tiles": [ diff --git a/tests/components/time_date/test_sensor.py b/tests/components/time_date/test_sensor.py index 56f58221529..82ddca10793 100644 --- a/tests/components/time_date/test_sensor.py +++ b/tests/components/time_date/test_sensor.py @@ -5,7 +5,6 @@ import homeassistant.components.time_date.sensor as time_date import homeassistant.util.dt as dt_util -# pylint: disable=protected-access async def test_intervals(hass): """Test timing intervals of sensors.""" device = time_date.TimeDateSensor(hass, "time") diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index ac2dde57c8d..99d84270d77 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -1,5 +1,5 @@ """The tests for the timer component.""" -# pylint: disable=protected-access + from datetime import timedelta import logging from unittest.mock import patch diff --git a/tests/components/todoist/test_calendar.py b/tests/components/todoist/test_calendar.py index b08d96a6b92..3ec587b5e8b 100644 --- a/tests/components/todoist/test_calendar.py +++ b/tests/components/todoist/test_calendar.py @@ -1,70 +1,70 @@ """Unit tests for the Todoist calendar platform.""" -from datetime import datetime -from typing import Any -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, patch import pytest +from todoist_api_python.models import Due, Label, Project, Task from homeassistant import setup -from homeassistant.components.todoist.calendar import DOMAIN, _parse_due_date -from homeassistant.components.todoist.types import DueDate +from homeassistant.components.todoist.calendar import DOMAIN from homeassistant.const import CONF_TOKEN from homeassistant.helpers import entity_registry -from homeassistant.util import dt -def test_parse_due_date_invalid(): - """Test None is returned if the due date can't be parsed.""" - data: DueDate = { - "date": "invalid", - "is_recurring": False, - "lang": "en", - "string": "", - "timezone": None, - } - assert _parse_due_date(data, timezone_offset=-8) is None +@pytest.fixture(name="task") +def mock_task() -> Task: + """Mock a todoist Task instance.""" + return Task( + assignee_id="1", + assigner_id="1", + comment_count=0, + is_completed=False, + content="A task", + created_at="2021-10-01T00:00:00", + creator_id="1", + description="A task", + due=Due(is_recurring=False, date="2022-01-01", string="today"), + id="1", + labels=[], + order=1, + parent_id=None, + priority=1, + project_id="12345", + section_id=None, + url="https://todoist.com", + sync_id=None, + ) -def test_parse_due_date_with_no_time_data(): - """Test due date is parsed correctly when it has no time data.""" - data: DueDate = { - "date": "2022-02-02", - "is_recurring": False, - "lang": "en", - "string": "Feb 2 2:00 PM", - "timezone": None, - } - actual = _parse_due_date(data, timezone_offset=-8) - assert datetime(2022, 2, 2, 8, 0, 0, tzinfo=dt.UTC) == actual - - -def test_parse_due_date_without_timezone_uses_offset(): - """Test due date uses user local timezone offset when it has no timezone.""" - data: DueDate = { - "date": "2022-02-02T14:00:00", - "is_recurring": False, - "lang": "en", - "string": "Feb 2 2:00 PM", - "timezone": None, - } - actual = _parse_due_date(data, timezone_offset=-8) - assert datetime(2022, 2, 2, 22, 0, 0, tzinfo=dt.UTC) == actual - - -@pytest.fixture(name="state") -def mock_state() -> dict[str, Any]: +@pytest.fixture(name="api") +def mock_api() -> AsyncMock: """Mock the api state.""" - return { - "collaborators": [], - "labels": [{"name": "label1", "id": 1}], - "projects": [{"id": "12345", "name": "Name"}], - } + api = AsyncMock() + api.get_projects.return_value = [ + Project( + id="12345", + color="blue", + comment_count=0, + is_favorite=False, + name="Name", + is_shared=False, + url="", + is_inbox_project=False, + is_team_inbox=False, + order=1, + parent_id=None, + view_style="list", + ) + ] + api.get_labels.return_value = [ + Label(id="1", name="label1", color="1", order=1, is_favorite=False) + ] + api.get_collaborators.return_value = [] + return api -@patch("homeassistant.components.todoist.calendar.TodoistAPI") -async def test_calendar_entity_unique_id(todoist_api, hass, state): +@patch("homeassistant.components.todoist.calendar.TodoistAPIAsync") +async def test_calendar_entity_unique_id(todoist_api, hass, api): """Test unique id is set to project id.""" - api = Mock(state=state) todoist_api.return_value = api assert await setup.async_setup_component( hass, @@ -83,10 +83,9 @@ async def test_calendar_entity_unique_id(todoist_api, hass, state): assert "12345" == entity.unique_id -@patch("homeassistant.components.todoist.calendar.TodoistAPI") -async def test_calendar_custom_project_unique_id(todoist_api, hass, state): +@patch("homeassistant.components.todoist.calendar.TodoistAPIAsync") +async def test_calendar_custom_project_unique_id(todoist_api, hass, api): """Test unique id is None for any custom projects.""" - api = Mock(state=state) todoist_api.return_value = api assert await setup.async_setup_component( hass, diff --git a/tests/components/tolo/test_config_flow.py b/tests/components/tolo/test_config_flow.py index 38542ad7db5..5f75a1fd5a2 100644 --- a/tests/components/tolo/test_config_flow.py +++ b/tests/components/tolo/test_config_flow.py @@ -46,7 +46,6 @@ async def test_user_walkthrough(hass: HomeAssistant, toloclient: Mock): assert result["type"] == FlowResultType.FORM assert result["step_id"] == SOURCE_USER - assert "flow_id" in result toloclient().get_status_info.side_effect = lambda *args, **kwargs: None @@ -58,7 +57,6 @@ async def test_user_walkthrough(hass: HomeAssistant, toloclient: Mock): assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == SOURCE_USER assert result2["errors"] == {"base": "cannot_connect"} - assert "flow_id" in result2 toloclient().get_status_info.side_effect = lambda *args, **kwargs: object() diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index 3d7a0613269..82c6ecd245a 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -54,7 +54,6 @@ async def test_full_flow_implementation( assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["step_id"] == "pick_implementation" - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -114,7 +113,6 @@ async def test_no_agreements( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -154,7 +152,6 @@ async def test_multiple_agreements( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -205,7 +202,6 @@ async def test_agreement_already_set_up( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -244,7 +240,7 @@ async def test_toon_abort( result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( hass, { @@ -306,7 +302,6 @@ async def test_import_migration( assert len(flows) == 1 assert flows[0]["context"][CONF_MIGRATE] == old_entry.entry_id - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py index 07cb5f3d40a..e52da526d3d 100644 --- a/tests/components/totalconnect/test_alarm_control_panel.py +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -31,6 +31,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_component import async_update_entity from homeassistant.util import dt from .common import ( @@ -67,11 +68,13 @@ DELAY = timedelta(seconds=10) async def test_attributes(hass: HomeAssistant) -> None: """Test the alarm control panel attributes are correct.""" + await setup_platform(hass, ALARM_DOMAIN) with patch( "homeassistant.components.totalconnect.TotalConnectClient.request", return_value=RESPONSE_DISARMED, ) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) assert state.state == STATE_ALARM_DISARMED mock_request.assert_called_once() @@ -91,8 +94,10 @@ async def test_attributes(hass: HomeAssistant) -> None: async def test_arm_home_success(hass: HomeAssistant) -> None: """Test arm home method success.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_STAY] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert hass.states.get(ENTITY_ID_2).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -113,8 +118,10 @@ async def test_arm_home_success(hass: HomeAssistant) -> None: async def test_arm_home_failure(hass: HomeAssistant) -> None: """Test arm home method failure.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_USER_CODE_INVALID] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -143,8 +150,10 @@ async def test_arm_home_failure(hass: HomeAssistant) -> None: async def test_arm_home_instant_success(hass: HomeAssistant) -> None: """Test arm home instant method success.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_STAY] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert hass.states.get(ENTITY_ID_2).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -163,8 +172,10 @@ async def test_arm_home_instant_success(hass: HomeAssistant) -> None: async def test_arm_home_instant_failure(hass: HomeAssistant) -> None: """Test arm home instant method failure.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_USER_CODE_INVALID] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -196,8 +207,10 @@ async def test_arm_home_instant_failure(hass: HomeAssistant) -> None: async def test_arm_away_instant_success(hass: HomeAssistant) -> None: """Test arm home instant method success.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_AWAY] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert hass.states.get(ENTITY_ID_2).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -216,8 +229,10 @@ async def test_arm_away_instant_success(hass: HomeAssistant) -> None: async def test_arm_away_instant_failure(hass: HomeAssistant) -> None: """Test arm home instant method failure.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_USER_CODE_INVALID] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -249,8 +264,10 @@ async def test_arm_away_instant_failure(hass: HomeAssistant) -> None: async def test_arm_away_success(hass: HomeAssistant) -> None: """Test arm away method success.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_AWAY] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -268,8 +285,10 @@ async def test_arm_away_success(hass: HomeAssistant) -> None: async def test_arm_away_failure(hass: HomeAssistant) -> None: """Test arm away method failure.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_USER_CODE_INVALID] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -298,8 +317,10 @@ async def test_arm_away_failure(hass: HomeAssistant) -> None: async def test_disarm_success(hass: HomeAssistant) -> None: """Test disarm method success.""" responses = [RESPONSE_ARMED_AWAY, RESPONSE_DISARM_SUCCESS, RESPONSE_DISARMED] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY assert mock_request.call_count == 1 @@ -321,8 +342,10 @@ async def test_disarm_failure(hass: HomeAssistant) -> None: RESPONSE_DISARM_FAILURE, RESPONSE_USER_CODE_INVALID, ] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY assert mock_request.call_count == 1 @@ -351,8 +374,10 @@ async def test_disarm_failure(hass: HomeAssistant) -> None: async def test_arm_night_success(hass: HomeAssistant) -> None: """Test arm night method success.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_NIGHT] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -370,8 +395,10 @@ async def test_arm_night_success(hass: HomeAssistant) -> None: async def test_arm_night_failure(hass: HomeAssistant) -> None: """Test arm night method failure.""" responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_USER_CODE_INVALID] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -400,8 +427,10 @@ async def test_arm_night_failure(hass: HomeAssistant) -> None: async def test_arming(hass: HomeAssistant) -> None: """Test arming.""" responses = [RESPONSE_DISARMED, RESPONSE_SUCCESS, RESPONSE_ARMING] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 @@ -419,8 +448,10 @@ async def test_arming(hass: HomeAssistant) -> None: async def test_disarming(hass: HomeAssistant) -> None: """Test disarming.""" responses = [RESPONSE_ARMED_AWAY, RESPONSE_SUCCESS, RESPONSE_DISARMING] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY assert mock_request.call_count == 1 @@ -438,8 +469,10 @@ async def test_disarming(hass: HomeAssistant) -> None: async def test_triggered_fire(hass: HomeAssistant) -> None: """Test triggered by fire.""" responses = [RESPONSE_TRIGGERED_FIRE] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) assert state.state == STATE_ALARM_TRIGGERED assert state.attributes.get("triggered_source") == "Fire/Smoke" @@ -449,8 +482,10 @@ async def test_triggered_fire(hass: HomeAssistant) -> None: async def test_triggered_police(hass: HomeAssistant) -> None: """Test triggered by police.""" responses = [RESPONSE_TRIGGERED_POLICE] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) assert state.state == STATE_ALARM_TRIGGERED assert state.attributes.get("triggered_source") == "Police/Medical" @@ -460,8 +495,10 @@ async def test_triggered_police(hass: HomeAssistant) -> None: async def test_triggered_carbon_monoxide(hass: HomeAssistant) -> None: """Test triggered by carbon monoxide.""" responses = [RESPONSE_TRIGGERED_CARBON_MONOXIDE] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) assert state.state == STATE_ALARM_TRIGGERED assert state.attributes.get("triggered_source") == "Carbon Monoxide" @@ -471,8 +508,10 @@ async def test_triggered_carbon_monoxide(hass: HomeAssistant) -> None: async def test_armed_custom(hass: HomeAssistant) -> None: """Test armed custom.""" responses = [RESPONSE_ARMED_CUSTOM] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_CUSTOM_BYPASS assert mock_request.call_count == 1 @@ -480,8 +519,10 @@ async def test_armed_custom(hass: HomeAssistant) -> None: async def test_unknown(hass: HomeAssistant) -> None: """Test unknown arm status.""" responses = [RESPONSE_UNKNOWN] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE assert mock_request.call_count == 1 @@ -496,9 +537,11 @@ async def test_other_update_failures(hass: HomeAssistant) -> None: RESPONSE_DISARMED, ValueError, ] + await setup_platform(hass, ALARM_DOMAIN) with patch(TOTALCONNECT_REQUEST, side_effect=responses) as mock_request: # first things work as planned - await setup_platform(hass, ALARM_DOMAIN) + await async_update_entity(hass, ENTITY_ID) + await hass.async_block_till_done() assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 1 diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py index 25f30237d0f..4b41c9cc773 100644 --- a/tests/components/tradfri/conftest.py +++ b/tests/components/tradfri/conftest.py @@ -5,8 +5,6 @@ import pytest from . import GATEWAY_ID, TRADFRI_PATH -# pylint: disable=protected-access - @pytest.fixture def mock_gateway_info(): diff --git a/tests/components/tradfri/test_sensor.py b/tests/components/tradfri/test_sensor.py index 6408613f4e3..10904b8ffa6 100644 --- a/tests/components/tradfri/test_sensor.py +++ b/tests/components/tradfri/test_sensor.py @@ -91,8 +91,8 @@ async def test_air_quality_sensor(hass, mock_gateway, mock_api_factory): 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" assert sensor_1.attributes["state_class"] == "measurement" + assert "device_class" not in sensor_1.attributes async def test_filter_time_left_sensor(hass, mock_gateway, mock_api_factory): diff --git a/tests/components/tts/conftest.py b/tests/components/tts/conftest.py index 6d995978391..a8b9b4cf5ce 100644 --- a/tests/components/tts/conftest.py +++ b/tests/components/tts/conftest.py @@ -55,9 +55,9 @@ def empty_cache_dir(tmp_path, mock_init_cache_dir, mock_get_cache_files, request return # Print contents of dir if failed - print("Content of dir for", request.node.nodeid) + print("Content of dir for", request.node.nodeid) # noqa: T201 for fil in tmp_path.iterdir(): - print(fil.relative_to(tmp_path)) + print(fil.relative_to(tmp_path)) # noqa: T201 # To show the log. assert False diff --git a/tests/components/twentemilieu/test_config_flow.py b/tests/components/twentemilieu/test_config_flow.py index 05e20dd06bd..83e7011b881 100644 --- a/tests/components/twentemilieu/test_config_flow.py +++ b/tests/components/twentemilieu/test_config_flow.py @@ -31,7 +31,6 @@ async def test_full_user_flow( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -68,7 +67,6 @@ async def test_invalid_address( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_twentemilieu_config_flow.unique_id.side_effect = TwenteMilieuAddressError result2 = await hass.config_entries.flow.async_configure( @@ -82,7 +80,6 @@ async def test_invalid_address( assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": "invalid_address"} - assert "flow_id" in result2 mock_twentemilieu_config_flow.unique_id.side_effect = None result3 = await hass.config_entries.flow.async_configure( diff --git a/tests/components/twentemilieu/test_diagnostics.py b/tests/components/twentemilieu/test_diagnostics.py index efbe2fc3104..ac5da4ea14f 100644 --- a/tests/components/twentemilieu/test_diagnostics.py +++ b/tests/components/twentemilieu/test_diagnostics.py @@ -16,9 +16,9 @@ async def test_diagnostics( assert await get_diagnostics_for_config_entry( hass, hass_client, init_integration ) == { - "0": ["2021-11-01", "2021-12-01"], - "1": ["2021-11-02"], - "2": [], - "6": ["2022-01-06"], - "10": ["2021-11-03"], + "WasteType.NON_RECYCLABLE": ["2021-11-01", "2021-12-01"], + "WasteType.ORGANIC": ["2021-11-02"], + "WasteType.PAPER": [], + "WasteType.TREE": ["2022-01-06"], + "WasteType.PACKAGES": ["2021-11-03"], } diff --git a/tests/components/twinkly/test_light.py b/tests/components/twinkly/test_light.py index 53e589c564b..d5da88cda11 100644 --- a/tests/components/twinkly/test_light.py +++ b/tests/components/twinkly/test_light.py @@ -34,9 +34,6 @@ async def test_initial_state(hass: HomeAssistant): assert state.attributes["friendly_name"] == entity.unique_id assert state.attributes["icon"] == "mdi:string-lights" - # Validates that custom properties of the API device_info are propagated through attributes - assert state.attributes["uuid"] == entity.unique_id - assert entity.original_name == entity.unique_id assert entity.original_icon == "mdi:string-lights" diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 078c068c8ed..c9ce0e74c21 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -12,7 +12,6 @@ from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, CONF_ALLOW_UPTIME_SENSORS, CONF_BLOCK_CLIENT, - CONF_CONTROLLER, CONF_DETECTION_TIME, CONF_DPI_RESTRICTIONS, CONF_IGNORE_WIRED_BUG, @@ -143,14 +142,6 @@ async def test_flow_works(hass, aioclient_mock, mock_discovery): CONF_PORT: 1234, CONF_SITE_ID: "site_id", CONF_VERIFY_SSL: True, - CONF_CONTROLLER: { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_PORT: 1234, - CONF_SITE_ID: "site_id", - CONF_VERIFY_SSL: True, - }, } diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 3861c5b38bd..c08fee21fdf 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -16,7 +16,6 @@ from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.unifi.const import ( - CONF_CONTROLLER, CONF_SITE_ID, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, @@ -67,7 +66,7 @@ CONTROLLER_HOST = { "uptime": 1562600160, } -CONTROLLER_DATA = { +ENTRY_CONFIG = { CONF_HOST: DEFAULT_HOST, CONF_USERNAME: "username", CONF_PASSWORD: "password", @@ -75,8 +74,6 @@ CONTROLLER_DATA = { CONF_SITE_ID: DEFAULT_SITE, CONF_VERIFY_SSL: False, } - -ENTRY_CONFIG = {**CONTROLLER_DATA, CONF_CONTROLLER: CONTROLLER_DATA} ENTRY_OPTIONS = {} CONFIGURATION = [] @@ -227,8 +224,8 @@ async def test_controller_setup(hass, aioclient_mock): assert forward_entry_setup.mock_calls[1][1] == (entry, SENSOR_DOMAIN) assert forward_entry_setup.mock_calls[2][1] == (entry, SWITCH_DOMAIN) - assert controller.host == CONTROLLER_DATA[CONF_HOST] - assert controller.site == CONTROLLER_DATA[CONF_SITE_ID] + assert controller.host == ENTRY_CONFIG[CONF_HOST] + assert controller.site == ENTRY_CONFIG[CONF_SITE_ID] assert controller.site_name == SITE[0]["desc"] assert controller.site_role == SITE[0]["role"] @@ -467,12 +464,12 @@ async def test_get_unifi_controller(hass): with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", return_value=True ): - assert await get_unifi_controller(hass, CONTROLLER_DATA) + assert await get_unifi_controller(hass, ENTRY_CONFIG) async def test_get_unifi_controller_verify_ssl_false(hass): """Successful call with verify ssl set to false.""" - controller_data = dict(CONTROLLER_DATA) + controller_data = dict(ENTRY_CONFIG) controller_data[CONF_VERIFY_SSL] = False with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", return_value=True @@ -500,4 +497,4 @@ async def test_get_unifi_controller_fails_to_connect( with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( "aiounifi.Controller.login", side_effect=side_effect ), pytest.raises(raised_exception): - await get_unifi_controller(hass, CONTROLLER_DATA) + await get_unifi_controller(hass, ENTRY_CONFIG) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index b8f1aa771a4..4aacc239b22 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -313,6 +313,7 @@ async def test_tracked_devices( # State change signalling work device_1["next_interval"] = 20 + device_2["state"] = 1 device_2["next_interval"] = 50 mock_unifi_websocket(message=MessageKey.DEVICE, data=[device_1, device_2]) await hass.async_block_till_done() @@ -607,15 +608,6 @@ async def test_option_track_devices(hass, aioclient_mock, mock_device_registry): assert hass.states.get("device_tracker.client") assert not hass.states.get("device_tracker.device") - hass.config_entries.async_update_entry( - config_entry, - options={CONF_TRACK_DEVICES: True}, - ) - await hass.async_block_till_done() - - assert hass.states.get("device_tracker.client") - assert hass.states.get("device_tracker.device") - async def test_option_ssid_filter( hass, aioclient_mock, mock_unifi_websocket, mock_device_registry @@ -1007,7 +999,7 @@ async def test_dont_track_devices(hass, aioclient_mock, mock_device_registry): "version": "4.0.42.10433", } - config_entry = await setup_unifi_integration( + await setup_unifi_integration( hass, aioclient_mock, options={CONF_TRACK_DEVICES: False}, @@ -1019,16 +1011,6 @@ async def test_dont_track_devices(hass, aioclient_mock, mock_device_registry): assert hass.states.get("device_tracker.client") assert not hass.states.get("device_tracker.device") - hass.config_entries.async_update_entry( - config_entry, - options={CONF_TRACK_DEVICES: True}, - ) - await hass.async_block_till_done() - - assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 - assert hass.states.get("device_tracker.client") - assert hass.states.get("device_tracker.device") - async def test_dont_track_wired_clients(hass, aioclient_mock, mock_device_registry): """Test don't track wired clients config works.""" diff --git a/tests/components/unifi/test_diagnostics.py b/tests/components/unifi/test_diagnostics.py index 9de0e4b6154..0899153033e 100644 --- a/tests/components/unifi/test_diagnostics.py +++ b/tests/components/unifi/test_diagnostics.py @@ -29,6 +29,7 @@ async def test_entry_diagnostics(hass, hass_client, aioclient_mock): "wired-tx_bytes": 5678000000, } device = { + "board_rev": "1.2.3", "ethernet_table": [ { "mac": "22:22:22:22:22:22", @@ -112,7 +113,6 @@ async def test_entry_diagnostics(hass, hass_client, aioclient_mock): assert await get_diagnostics_for_config_entry(hass, hass_client, config_entry) == { "config": { "data": { - "controller": REDACTED, "host": REDACTED, "password": REDACTED, "port": 1234, @@ -154,6 +154,7 @@ async def test_entry_diagnostics(hass, hass_client, aioclient_mock): }, "devices": { "00:00:00:00:00:01": { + "board_rev": "1.2.3", "ethernet_table": [ { "mac": "00:00:00:00:00:02", diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index ebdea40fe73..3aa4114e829 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -1,25 +1,100 @@ """UniFi Network sensor platform tests.""" -from datetime import datetime +from copy import deepcopy +from datetime import datetime, timedelta from unittest.mock import patch from aiounifi.models.message import MessageKey +from aiounifi.websocket import WebsocketState import pytest from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, CONF_ALLOW_UPTIME_SENSORS, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, ) +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.entity import EntityCategory +from homeassistant.helpers.entity_registry import RegistryEntryDisabler import homeassistant.util.dt as dt_util from .test_controller import setup_unifi_integration +from tests.common import async_fire_time_changed + +DEVICE_1 = { + "board_rev": 2, + "device_id": "mock-id", + "ip": "10.0.1.1", + "mac": "10:00:00:00:01:01", + "last_seen": 1562600145, + "model": "US16P150", + "name": "mock-name", + "port_overrides": [], + "port_table": [ + { + "media": "GE", + "name": "Port 1", + "port_idx": 1, + "poe_class": "Class 4", + "poe_enable": True, + "poe_mode": "auto", + "poe_power": "2.56", + "poe_voltage": "53.40", + "portconf_id": "1a1", + "port_poe": True, + "up": True, + }, + { + "media": "GE", + "name": "Port 2", + "port_idx": 2, + "poe_class": "Class 4", + "poe_enable": True, + "poe_mode": "auto", + "poe_power": "2.56", + "poe_voltage": "53.40", + "portconf_id": "1a2", + "port_poe": True, + "up": True, + }, + { + "media": "GE", + "name": "Port 3", + "port_idx": 3, + "poe_class": "Unknown", + "poe_enable": False, + "poe_mode": "off", + "poe_power": "0.00", + "poe_voltage": "0.00", + "portconf_id": "1a3", + "port_poe": False, + "up": True, + }, + { + "media": "GE", + "name": "Port 4", + "port_idx": 4, + "poe_class": "Unknown", + "poe_enable": False, + "poe_mode": "auto", + "poe_power": "0.00", + "poe_voltage": "0.00", + "portconf_id": "1a4", + "port_poe": True, + "up": True, + }, + ], + "state": 1, + "type": "usw", + "version": "4.0.42.10433", +} + async def test_no_clients(hass, aioclient_mock): """Test the update_clients function when no clients are found.""" @@ -243,3 +318,73 @@ async def test_remove_sensors(hass, aioclient_mock, mock_unifi_websocket): assert hass.states.get("sensor.wireless_client_rx") assert hass.states.get("sensor.wireless_client_tx") assert hass.states.get("sensor.wireless_client_uptime") + + +async def test_poe_port_switches(hass, aioclient_mock, mock_unifi_websocket): + """Test the update_items function with some clients.""" + await setup_unifi_integration(hass, aioclient_mock, devices_response=[DEVICE_1]) + assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 0 + + ent_reg = er.async_get(hass) + ent_reg_entry = ent_reg.async_get("sensor.mock_name_port_1_poe_power") + assert ent_reg_entry.disabled_by == RegistryEntryDisabler.INTEGRATION + assert ent_reg_entry.entity_category is EntityCategory.DIAGNOSTIC + + # Enable entity + ent_reg.async_update_entity( + entity_id="sensor.mock_name_port_1_poe_power", disabled_by=None + ) + await hass.async_block_till_done() + + async_fire_time_changed( + hass, + dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), + ) + await hass.async_block_till_done() + + # Validate state object + poe_sensor = hass.states.get("sensor.mock_name_port_1_poe_power") + assert poe_sensor.state == "2.56" + assert poe_sensor.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER + + # Update state object + device_1 = deepcopy(DEVICE_1) + device_1["port_table"][0]["poe_power"] = "5.12" + mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + await hass.async_block_till_done() + assert hass.states.get("sensor.mock_name_port_1_poe_power").state == "5.12" + + # PoE is disabled + device_1 = deepcopy(DEVICE_1) + device_1["port_table"][0]["poe_mode"] = "off" + mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + await hass.async_block_till_done() + assert hass.states.get("sensor.mock_name_port_1_poe_power").state == "0" + + # Availability signalling + + # Controller disconnects + mock_unifi_websocket(state=WebsocketState.DISCONNECTED) + await hass.async_block_till_done() + assert ( + hass.states.get("sensor.mock_name_port_1_poe_power").state == STATE_UNAVAILABLE + ) + + # Controller reconnects + mock_unifi_websocket(state=WebsocketState.RUNNING) + await hass.async_block_till_done() + assert hass.states.get("sensor.mock_name_port_1_poe_power") + + # Device gets disabled + device_1["disabled"] = True + mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + await hass.async_block_till_done() + assert ( + hass.states.get("sensor.mock_name_port_1_poe_power").state == STATE_UNAVAILABLE + ) + + # Device gets re-enabled + device_1["disabled"] = False + mock_unifi_websocket(message=MessageKey.DEVICE, data=device_1) + await hass.async_block_till_done() + assert hass.states.get("sensor.mock_name_port_1_poe_power") diff --git a/tests/components/unifi_direct/test_device_tracker.py b/tests/components/unifi_direct/test_device_tracker.py index 700f490bba5..9f71bb54c63 100644 --- a/tests/components/unifi_direct/test_device_tracker.py +++ b/tests/components/unifi_direct/test_device_tracker.py @@ -126,7 +126,7 @@ async def test_to_get_update(mock_sendline, mock_prompt, mock_login, mock_logout scanner = get_scanner(hass, conf_dict) # mock_sendline.side_effect = AssertionError("Test") mock_prompt.side_effect = AssertionError("Test") - devices = scanner._get_update() # pylint: disable=protected-access + devices = scanner._get_update() assert devices is None diff --git a/tests/components/unifiprotect/conftest.py b/tests/components/unifiprotect/conftest.py index ea270e28fcc..d66ed0ea060 100644 --- a/tests/components/unifiprotect/conftest.py +++ b/tests/components/unifiprotect/conftest.py @@ -1,5 +1,5 @@ """Fixtures and test data for UniFi Protect methods.""" -# pylint: disable=protected-access + from __future__ import annotations from collections.abc import Callable diff --git a/tests/components/unifiprotect/fixtures/sample_camera.json b/tests/components/unifiprotect/fixtures/sample_camera.json index e7ffbd0abcc..0b5e3437919 100644 --- a/tests/components/unifiprotect/fixtures/sample_camera.json +++ b/tests/components/unifiprotect/fixtures/sample_camera.json @@ -24,7 +24,7 @@ "canAdopt": false, "isAttemptingToConnect": false, "lastMotion": 1640021213927, - "micVolume": 0, + "micVolume": 1, "isMicEnabled": true, "isRecording": false, "isWirelessUplinkEnabled": true, @@ -121,7 +121,7 @@ "aeMode": "auto", "irLedMode": "auto", "irLedLevel": 255, - "wdr": 0, + "wdr": 1, "icrSensitivity": 0, "brightness": 50, "contrast": 50, @@ -145,7 +145,7 @@ "focusPosition": 0, "touchFocusX": 1001, "touchFocusY": 1001, - "zoomPosition": 0, + "zoomPosition": 1, "mountPosition": "wall" }, "talkbackSettings": { diff --git a/tests/components/unifiprotect/test_binary_sensor.py b/tests/components/unifiprotect/test_binary_sensor.py index 152628c75f9..c1f166e0110 100644 --- a/tests/components/unifiprotect/test_binary_sensor.py +++ b/tests/components/unifiprotect/test_binary_sensor.py @@ -1,5 +1,5 @@ """Test the UniFi Protect binary_sensor platform.""" -# pylint: disable=protected-access + from __future__ import annotations from datetime import datetime, timedelta diff --git a/tests/components/unifiprotect/test_button.py b/tests/components/unifiprotect/test_button.py index ac158b8121e..7c7fcf613ef 100644 --- a/tests/components/unifiprotect/test_button.py +++ b/tests/components/unifiprotect/test_button.py @@ -1,5 +1,5 @@ """Test the UniFi Protect button platform.""" -# pylint: disable=protected-access + from __future__ import annotations from unittest.mock import AsyncMock, Mock diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index 2727d11285f..98b75d402bf 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -1,5 +1,5 @@ """Test the UniFi Protect camera platform.""" -# pylint: disable=protected-access + from __future__ import annotations from unittest.mock import AsyncMock, Mock diff --git a/tests/components/unifiprotect/test_init.py b/tests/components/unifiprotect/test_init.py index 04b4928aaec..42536d937bc 100644 --- a/tests/components/unifiprotect/test_init.py +++ b/tests/components/unifiprotect/test_init.py @@ -1,5 +1,5 @@ """Test the UniFi Protect setup flow.""" -# pylint: disable=protected-access + from __future__ import annotations from collections.abc import Awaitable, Callable diff --git a/tests/components/unifiprotect/test_light.py b/tests/components/unifiprotect/test_light.py index a1a3f9d071b..2e714e0fab9 100644 --- a/tests/components/unifiprotect/test_light.py +++ b/tests/components/unifiprotect/test_light.py @@ -1,5 +1,5 @@ """Test the UniFi Protect light platform.""" -# pylint: disable=protected-access + from __future__ import annotations from unittest.mock import AsyncMock, Mock diff --git a/tests/components/unifiprotect/test_lock.py b/tests/components/unifiprotect/test_lock.py index 2c58d5fff66..d29c3ac7f44 100644 --- a/tests/components/unifiprotect/test_lock.py +++ b/tests/components/unifiprotect/test_lock.py @@ -1,5 +1,5 @@ """Test the UniFi Protect lock platform.""" -# pylint: disable=protected-access + from __future__ import annotations from unittest.mock import AsyncMock, Mock diff --git a/tests/components/unifiprotect/test_media_player.py b/tests/components/unifiprotect/test_media_player.py index 1679d17c96c..1f191b63e0f 100644 --- a/tests/components/unifiprotect/test_media_player.py +++ b/tests/components/unifiprotect/test_media_player.py @@ -1,5 +1,5 @@ """Test the UniFi Protect media_player platform.""" -# pylint: disable=protected-access + from __future__ import annotations from unittest.mock import AsyncMock, Mock, patch diff --git a/tests/components/unifiprotect/test_migrate.py b/tests/components/unifiprotect/test_migrate.py index 20e4ec61ca0..c89d9e4faf8 100644 --- a/tests/components/unifiprotect/test_migrate.py +++ b/tests/components/unifiprotect/test_migrate.py @@ -1,5 +1,5 @@ """Test the UniFi Protect setup flow.""" -# pylint: disable=protected-access + from __future__ import annotations from unittest.mock import AsyncMock diff --git a/tests/components/unifiprotect/test_number.py b/tests/components/unifiprotect/test_number.py index 5a5bf400169..302d5dc1d76 100644 --- a/tests/components/unifiprotect/test_number.py +++ b/tests/components/unifiprotect/test_number.py @@ -1,5 +1,5 @@ """Test the UniFi Protect number platform.""" -# pylint: disable=protected-access + from __future__ import annotations from datetime import timedelta @@ -97,8 +97,10 @@ async def test_number_setup_camera_all( ): """Test number entity setup for camera devices (all features).""" + camera.feature_flags.has_chime = True + camera.chime_duration = timedelta(seconds=1) await init_entry(hass, ufp, [camera]) - assert_entity_counts(hass, Platform.NUMBER, 3, 3) + assert_entity_counts(hass, Platform.NUMBER, 4, 4) entity_registry = er.async_get(hass) @@ -113,7 +115,7 @@ async def test_number_setup_camera_all( state = hass.states.get(entity_id) assert state - assert state.state == "0" + assert state.state == "1" assert state.attributes[ATTR_ATTRIBUTION] == DEFAULT_ATTRIBUTION @@ -203,7 +205,6 @@ async def test_number_camera_simple( camera.__fields__[description.ufp_set_method] = Mock(final=False) setattr(camera, description.ufp_set_method, AsyncMock()) - set_method = getattr(camera, description.ufp_set_method) _, entity_id = ids_from_device_description(Platform.NUMBER, camera, description) @@ -211,8 +212,6 @@ async def test_number_camera_simple( "number", "set_value", {ATTR_ENTITY_ID: entity_id, "value": 1.0}, blocking=True ) - set_method.assert_called_once_with(1.0) - async def test_number_lock_auto_close( hass: HomeAssistant, ufp: MockUFPFixture, doorlock: Doorlock diff --git a/tests/components/unifiprotect/test_select.py b/tests/components/unifiprotect/test_select.py index cecf899aba9..ef0856b5f04 100644 --- a/tests/components/unifiprotect/test_select.py +++ b/tests/components/unifiprotect/test_select.py @@ -1,5 +1,5 @@ """Test the UniFi Protect select platform.""" -# pylint: disable=protected-access + from __future__ import annotations from copy import copy diff --git a/tests/components/unifiprotect/test_sensor.py b/tests/components/unifiprotect/test_sensor.py index f5779e78b1c..d68742ba33d 100644 --- a/tests/components/unifiprotect/test_sensor.py +++ b/tests/components/unifiprotect/test_sensor.py @@ -1,5 +1,5 @@ """Test the UniFi Protect sensor platform.""" -# pylint: disable=protected-access + from __future__ import annotations from datetime import datetime, timedelta diff --git a/tests/components/unifiprotect/test_services.py b/tests/components/unifiprotect/test_services.py index 9da6b1107c3..78b9f69af78 100644 --- a/tests/components/unifiprotect/test_services.py +++ b/tests/components/unifiprotect/test_services.py @@ -1,5 +1,5 @@ """Test the UniFi Protect global services.""" -# pylint: disable=protected-access + from __future__ import annotations from unittest.mock import AsyncMock, Mock diff --git a/tests/components/unifiprotect/test_switch.py b/tests/components/unifiprotect/test_switch.py index 2ede00e60f2..36bfd560c79 100644 --- a/tests/components/unifiprotect/test_switch.py +++ b/tests/components/unifiprotect/test_switch.py @@ -1,5 +1,5 @@ """Test the UniFi Protect switch platform.""" -# pylint: disable=protected-access + from __future__ import annotations from unittest.mock import AsyncMock, Mock diff --git a/tests/components/unifiprotect/test_text.py b/tests/components/unifiprotect/test_text.py index 17fe3ee7bc2..df1c6e628e6 100644 --- a/tests/components/unifiprotect/test_text.py +++ b/tests/components/unifiprotect/test_text.py @@ -1,5 +1,5 @@ """Test the UniFi Protect text platform.""" -# pylint: disable=protected-access + from __future__ import annotations from unittest.mock import AsyncMock, Mock diff --git a/tests/components/unifiprotect/utils.py b/tests/components/unifiprotect/utils.py index bee479b8e2b..2d6ab9937a3 100644 --- a/tests/components/unifiprotect/utils.py +++ b/tests/components/unifiprotect/utils.py @@ -1,11 +1,11 @@ """Test helpers for UniFi Protect.""" -# pylint: disable=protected-access + from __future__ import annotations -from collections.abc import Sequence +from collections.abc import Callable, Sequence from dataclasses import dataclass from datetime import timedelta -from typing import Any, Callable +from typing import Any from unittest.mock import Mock from pyunifiprotect import ProtectApiClient diff --git a/tests/components/uptime/test_config_flow.py b/tests/components/uptime/test_config_flow.py index 9db909003e9..4a7bb11b839 100644 --- a/tests/components/uptime/test_config_flow.py +++ b/tests/components/uptime/test_config_flow.py @@ -1,11 +1,8 @@ """Tests for the Uptime config flow.""" from unittest.mock import MagicMock -import pytest - from homeassistant.components.uptime.const import DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import CONF_NAME +from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -23,7 +20,6 @@ async def test_full_user_flow( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -35,34 +31,16 @@ async def test_full_user_flow( assert result2.get("data") == {} -@pytest.mark.parametrize("source", [SOURCE_USER, SOURCE_IMPORT]) async def test_single_instance_allowed( hass: HomeAssistant, mock_config_entry: MockConfigEntry, - source: str, ) -> None: """Test we abort if already setup.""" mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": source} + DOMAIN, context={"source": SOURCE_USER} ) assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "single_instance_allowed" - - -async def test_import_flow( - hass: HomeAssistant, - mock_setup_entry: MagicMock, -) -> None: - """Test the import configuration flow.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_NAME: "My Uptime"}, - ) - - assert result.get("type") == FlowResultType.CREATE_ENTRY - assert result.get("title") == "My Uptime" - assert result.get("data") == {} diff --git a/tests/components/uptime/test_init.py b/tests/components/uptime/test_init.py index 0f966734550..3535f846013 100644 --- a/tests/components/uptime/test_init.py +++ b/tests/components/uptime/test_init.py @@ -1,12 +1,7 @@ """Tests for the Uptime integration.""" -from unittest.mock import AsyncMock - -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.uptime.const import DOMAIN from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -27,29 +22,3 @@ async def test_load_unload_config_entry( assert not hass.data.get(DOMAIN) assert mock_config_entry.state is ConfigEntryState.NOT_LOADED - - -async def test_import_config( - hass: HomeAssistant, - mock_setup_entry: AsyncMock, -) -> None: - """Test Uptime being set up from config via import.""" - assert await async_setup_component( - hass, - SENSOR_DOMAIN, - { - SENSOR_DOMAIN: { - "platform": DOMAIN, - CONF_NAME: "My Uptime", - } - }, - ) - await hass.async_block_till_done() - - config_entries = hass.config_entries.async_entries(DOMAIN) - assert len(config_entries) == 1 - - entry = config_entries[0] - assert entry.title == "My Uptime" - assert entry.unique_id is None - assert entry.data == {} diff --git a/tests/components/uptimerobot/test_init.py b/tests/components/uptimerobot/test_init.py index 00e7f5c27e0..00d9b1c6a85 100644 --- a/tests/components/uptimerobot/test_init.py +++ b/tests/components/uptimerobot/test_init.py @@ -220,6 +220,7 @@ async def test_device_management(hass: HomeAssistant): ): async_fire_time_changed(hass, dt.utcnow() + COORDINATOR_UPDATE_INTERVAL) await hass.async_block_till_done() + await hass.async_block_till_done() devices = dr.async_entries_for_config_entry(dev_reg, mock_entry.entry_id) assert len(devices) == 1 diff --git a/tests/components/usgs_earthquakes_feed/test_geo_location.py b/tests/components/usgs_earthquakes_feed/test_geo_location.py index b8fcbbcbe7d..8d69006db51 100644 --- a/tests/components/usgs_earthquakes_feed/test_geo_location.py +++ b/tests/components/usgs_earthquakes_feed/test_geo_location.py @@ -30,7 +30,7 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_RADIUS, EVENT_HOMEASSISTANT_START, - LENGTH_KILOMETERS, + UnitOfLength, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -152,7 +152,7 @@ async def test_setup(hass): ATTR_TYPE: "Type 1", ATTR_ALERT: "Alert 1", ATTR_MAGNITUDE: 5.7, - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "usgs_earthquakes_feed", ATTR_ICON: "mdi:pulse", } @@ -166,7 +166,7 @@ async def test_setup(hass): ATTR_LATITUDE: -31.1, ATTR_LONGITUDE: 150.1, ATTR_FRIENDLY_NAME: "Title 2", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "usgs_earthquakes_feed", ATTR_ICON: "mdi:pulse", } @@ -180,7 +180,7 @@ async def test_setup(hass): ATTR_LATITUDE: -31.2, ATTR_LONGITUDE: 150.2, ATTR_FRIENDLY_NAME: "Title 3", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfLength.KILOMETERS, ATTR_SOURCE: "usgs_earthquakes_feed", ATTR_ICON: "mdi:pulse", } diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index cae291aac5a..ae4a97aa3b7 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -18,9 +18,9 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONF_PLATFORM, - ENERGY_KILO_WATT_HOUR, EVENT_HOMEASSISTANT_START, Platform, + UnitOfEnergy, ) from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er @@ -90,7 +90,7 @@ async def test_services(hass, meter): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) entity_id = config[DOMAIN]["energy_bill"]["source"] hass.states.async_set( - entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) await hass.async_block_till_done() @@ -99,7 +99,7 @@ async def test_services(hass, meter): hass.states.async_set( entity_id, 3, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -120,7 +120,7 @@ async def test_services(hass, meter): hass.states.async_set( entity_id, 4, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -148,7 +148,7 @@ async def test_services(hass, meter): hass.states.async_set( entity_id, 5, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -214,7 +214,7 @@ async def test_services_config_entry(hass): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) entity_id = "sensor.energy" hass.states.async_set( - entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) await hass.async_block_till_done() @@ -223,7 +223,7 @@ async def test_services_config_entry(hass): hass.states.async_set( entity_id, 3, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -244,7 +244,7 @@ async def test_services_config_entry(hass): hass.states.async_set( entity_id, 4, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -272,7 +272,7 @@ async def test_services_config_entry(hass): hass.states.async_set( entity_id, 5, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index ec12120bebd..0189557ca9b 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -32,10 +32,10 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, - ENERGY_KILO_WATT_HOUR, EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE, STATE_UNKNOWN, + UnitOfEnergy, ) from homeassistant.core import CoreState, State from homeassistant.helpers import entity_registry @@ -105,7 +105,7 @@ async def test_state(hass, yaml_config, config_entry_config): await hass.async_block_till_done() hass.states.async_set( - entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) await hass.async_block_till_done() @@ -113,26 +113,26 @@ async def test_state(hass, yaml_config, config_entry_config): assert state is not None assert state.state == "0" assert state.attributes.get("status") == COLLECTING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR state = hass.states.get("sensor.energy_bill_midpeak") assert state is not None assert state.state == "0" assert state.attributes.get("status") == PAUSED - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR state = hass.states.get("sensor.energy_bill_offpeak") assert state is not None assert state.state == "0" assert state.attributes.get("status") == PAUSED - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR now = dt_util.utcnow() + timedelta(seconds=10) with patch("homeassistant.util.dt.utcnow", return_value=now): hass.states.async_set( entity_id, 3, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -166,7 +166,7 @@ async def test_state(hass, yaml_config, config_entry_config): hass.states.async_set( entity_id, 6, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -210,7 +210,7 @@ async def test_state(hass, yaml_config, config_entry_config): # test invalid state hass.states.async_set( - entity_id, "*", {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, "*", {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) await hass.async_block_till_done() state = hass.states.get("sensor.energy_bill_midpeak") @@ -219,7 +219,9 @@ async def test_state(hass, yaml_config, config_entry_config): # test unavailable source hass.states.async_set( - entity_id, STATE_UNAVAILABLE, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, + STATE_UNAVAILABLE, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, ) await hass.async_block_till_done() state = hass.states.get("sensor.energy_bill_midpeak") @@ -306,7 +308,7 @@ async def test_init(hass, yaml_config, config_entry_config): assert state.state == STATE_UNKNOWN hass.states.async_set( - entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) await hass.async_block_till_done() @@ -314,12 +316,12 @@ async def test_init(hass, yaml_config, config_entry_config): state = hass.states.get("sensor.energy_bill_onpeak") assert state is not None assert state.state == "0" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR state = hass.states.get("sensor.energy_bill_offpeak") assert state is not None assert state.state == "0" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR async def test_unique_id(hass): @@ -467,7 +469,7 @@ async def test_device_class(hass, yaml_config, config_entry_configs): await hass.async_block_till_done() hass.states.async_set( - entity_id_energy, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id_energy, 2, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) hass.states.async_set( entity_id_gas, 2, {ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit"} @@ -479,7 +481,7 @@ async def test_device_class(hass, yaml_config, config_entry_configs): assert state.state == "0" assert state.attributes.get(ATTR_DEVICE_CLASS) is SensorDeviceClass.ENERGY.value assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR state = hass.states.get("sensor.gas_meter") assert state is not None @@ -534,7 +536,7 @@ async def test_restore_state(hass, yaml_config, config_entry_config): attributes={ ATTR_STATUS: PAUSED, ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, ), { @@ -555,7 +557,7 @@ async def test_restore_state(hass, yaml_config, config_entry_config): attributes={ ATTR_STATUS: PAUSED, ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, ), { @@ -573,7 +575,7 @@ async def test_restore_state(hass, yaml_config, config_entry_config): attributes={ ATTR_STATUS: COLLECTING, ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, ), { @@ -591,7 +593,7 @@ async def test_restore_state(hass, yaml_config, config_entry_config): attributes={ ATTR_STATUS: COLLECTING, ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, ), {}, @@ -618,7 +620,7 @@ async def test_restore_state(hass, yaml_config, config_entry_config): assert state.state == "3" assert state.attributes.get("status") == PAUSED assert state.attributes.get("last_reset") == last_reset - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR state = hass.states.get("sensor.energy_bill_midpeak") assert state.state == "5" @@ -627,7 +629,7 @@ async def test_restore_state(hass, yaml_config, config_entry_config): assert state.state == "6" assert state.attributes.get("status") == COLLECTING assert state.attributes.get("last_reset") == last_reset - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR state = hass.states.get("sensor.energy_bill_superpeak") assert state.state == STATE_UNKNOWN @@ -694,7 +696,7 @@ async def test_net_consumption(hass, yaml_config, config_entry_config): hass.bus.async_fire(EVENT_HOMEASSISTANT_START) hass.states.async_set( - entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) await hass.async_block_till_done() @@ -703,7 +705,7 @@ async def test_net_consumption(hass, yaml_config, config_entry_config): hass.states.async_set( entity_id, 1, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -762,7 +764,7 @@ async def test_non_net_consumption(hass, yaml_config, config_entry_config, caplo hass.bus.async_fire(EVENT_HOMEASSISTANT_START) hass.states.async_set( - entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, 2, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) await hass.async_block_till_done() @@ -771,7 +773,7 @@ async def test_non_net_consumption(hass, yaml_config, config_entry_config, caplo hass.states.async_set( entity_id, 1, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -781,7 +783,7 @@ async def test_non_net_consumption(hass, yaml_config, config_entry_config, caplo hass.states.async_set( entity_id, None, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -848,7 +850,7 @@ async def test_delta_values(hass, yaml_config, config_entry_config, caplog): async_fire_time_changed(hass, now) hass.states.async_set( - entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) await hass.async_block_till_done() @@ -861,7 +863,7 @@ async def test_delta_values(hass, yaml_config, config_entry_config, caplog): hass.states.async_set( entity_id, None, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -873,7 +875,7 @@ async def test_delta_values(hass, yaml_config, config_entry_config, caplog): hass.states.async_set( entity_id, 3, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -888,7 +890,7 @@ async def test_delta_values(hass, yaml_config, config_entry_config, caplog): hass.states.async_set( entity_id, 6, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -925,7 +927,7 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True): async_fire_time_changed(hass, now) hass.states.async_set( - entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} ) await hass.async_block_till_done() @@ -935,7 +937,7 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True): hass.states.async_set( entity_id, 3, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -947,7 +949,7 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True): hass.states.async_set( entity_id, 6, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() @@ -978,7 +980,7 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True): hass.states.async_set( entity_id, 10, - {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, force_update=True, ) await hass.async_block_till_done() diff --git a/tests/components/velbus/conftest.py b/tests/components/velbus/conftest.py index 62aa0d0b505..c5db7f0ec73 100644 --- a/tests/components/velbus/conftest.py +++ b/tests/components/velbus/conftest.py @@ -1,5 +1,6 @@ """Fixtures for the Velbus tests.""" -from unittest.mock import AsyncMock, patch +from collections.abc import Generator +from unittest.mock import MagicMock, patch import pytest @@ -14,10 +15,9 @@ from tests.common import MockConfigEntry @pytest.fixture(name="controller") -def mock_controller(): +def mock_controller() -> Generator[MagicMock, None, None]: """Mock a successful velbus controller.""" - controller = AsyncMock() - with patch("velbusaio.controller.Velbus", return_value=controller): + with patch("homeassistant.components.velbus.Velbus", autospec=True) as controller: yield controller diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 454290b3581..af11f83adb3 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Velbus config flow.""" +from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -36,8 +37,18 @@ def com_port(): return port +@pytest.fixture(name="controller") +def mock_controller() -> Generator[MagicMock, None, None]: + """Mock a successful velbus controller.""" + with patch( + "homeassistant.components.velbus.config_flow.velbusaio.controller.Velbus", + autospec=True, + ) as controller: + yield controller + + @pytest.fixture(autouse=True) -def override_async_setup_entry() -> AsyncMock: +def override_async_setup_entry() -> Generator[AsyncMock, None, None]: """Override async_setup_entry.""" with patch( "homeassistant.components.velbus.async_setup_entry", return_value=True @@ -74,6 +85,7 @@ async def test_user(hass: HomeAssistant): assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result.get("title") == "velbus_test_serial" data = result.get("data") + assert data assert data[CONF_PORT] == PORT_SERIAL # try with a ip:port combination @@ -86,6 +98,7 @@ async def test_user(hass: HomeAssistant): assert result.get("type") == data_entry_flow.FlowResultType.CREATE_ENTRY assert result.get("title") == "velbus_test_tcp" data = result.get("data") + assert data assert data[CONF_PORT] == PORT_TCP diff --git a/tests/components/velbus/test_init.py b/tests/components/velbus/test_init.py index dee00cce16b..7dfb573901a 100644 --- a/tests/components/velbus/test_init.py +++ b/tests/components/velbus/test_init.py @@ -1,11 +1,14 @@ """Tests for the Velbus component initialisation.""" +from unittest.mock import patch + import pytest from homeassistant.components.velbus.const import DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState +from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant -from tests.common import mock_device_registry +from tests.common import MockConfigEntry, mock_device_registry @pytest.mark.usefixtures("controller") @@ -15,12 +18,12 @@ async def test_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert config_entry.state is ConfigEntryState.LOADED + assert config_entry.state == ConfigEntryState.LOADED assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state is ConfigEntryState.NOT_LOADED + assert config_entry.state == ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -35,22 +38,43 @@ async def test_device_identifier_migration( device_registry = mock_device_registry(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - identifiers=original_identifiers, + identifiers=original_identifiers, # type: ignore[arg-type] name="channel_name", manufacturer="Velleman", model="module_type_name", sw_version="module_sw_version", ) - assert device_registry.async_get_device(original_identifiers) + assert device_registry.async_get_device( + original_identifiers # type: ignore[arg-type] + ) assert not device_registry.async_get_device(target_identifiers) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert not device_registry.async_get_device(original_identifiers) + assert not device_registry.async_get_device( + original_identifiers # type: ignore[arg-type] + ) device_entry = device_registry.async_get_device(target_identifiers) assert device_entry assert device_entry.name == "channel_name" assert device_entry.manufacturer == "Velleman" assert device_entry.model == "module_type_name" assert device_entry.sw_version == "module_sw_version" + + +@pytest.mark.usefixtures("controller") +async def test_migrate_config_entry(hass: HomeAssistant) -> None: + """Test successful migration of entry data.""" + legacy_config = {CONF_NAME: "fake_name", CONF_PORT: "1.2.3.4:5678"} + entry = MockConfigEntry(domain=DOMAIN, unique_id="my own id", data=legacy_config) + entry.add_to_hass(hass) + + assert dict(entry.data) == legacy_config + assert entry.version == 1 + + # test in case we do not have a cache + with patch("os.path.isdir", return_value=True), patch("shutil.rmtree"): + await hass.config_entries.async_setup(entry.entry_id) + assert dict(entry.data) == legacy_config + assert entry.version == 2 diff --git a/tests/components/vera/test_common.py b/tests/components/vera/test_common.py index 3832daf0710..100a788313d 100644 --- a/tests/components/vera/test_common.py +++ b/tests/components/vera/test_common.py @@ -12,7 +12,6 @@ from tests.common import async_fire_time_changed async def test_subscription_registry(hass: HomeAssistant) -> None: """Test subscription registry polling.""" subscription_registry = SubscriptionRegistry(hass) - # pylint: disable=protected-access subscription_registry.poll_server_once = poll_server_once_mock = MagicMock() poll_server_once_mock.return_value = True diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index 43adc91c38c..8221ab36c04 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -34,7 +34,6 @@ async def test_full_user_flow_single_installation( assert result.get("step_id") == "user" assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} - assert "flow_id" in result mock_verisure_config_flow.installations = [ mock_verisure_config_flow.installations[0] @@ -73,7 +72,6 @@ async def test_full_user_flow_multiple_installations( assert result.get("step_id") == "user" assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -87,7 +85,6 @@ async def test_full_user_flow_multiple_installations( assert result2.get("step_id") == "installation" assert result2.get("type") == FlowResultType.FORM assert result2.get("errors") is None - assert "flow_id" in result2 result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], {"giid": "54321"} @@ -118,7 +115,6 @@ async def test_full_user_flow_single_installation_with_mfa( assert result.get("step_id") == "user" assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} - assert "flow_id" in result mock_verisure_config_flow.login.side_effect = VerisureLoginError( "Multifactor authentication enabled, disable or create MFA cookie" @@ -135,7 +131,6 @@ async def test_full_user_flow_single_installation_with_mfa( assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "mfa" - assert "flow_id" in result2 mock_verisure_config_flow.login.side_effect = None mock_verisure_config_flow.installations = [ @@ -176,7 +171,6 @@ async def test_full_user_flow_multiple_installations_with_mfa( assert result.get("step_id") == "user" assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} - assert "flow_id" in result mock_verisure_config_flow.login.side_effect = VerisureLoginError( "Multifactor authentication enabled, disable or create MFA cookie" @@ -193,7 +187,6 @@ async def test_full_user_flow_multiple_installations_with_mfa( assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "mfa" - assert "flow_id" in result2 mock_verisure_config_flow.login.side_effect = None @@ -208,7 +201,6 @@ async def test_full_user_flow_multiple_installations_with_mfa( assert result3.get("step_id") == "installation" assert result3.get("type") == FlowResultType.FORM assert result3.get("errors") is None - assert "flow_id" in result2 result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], {"giid": "54321"} @@ -248,8 +240,6 @@ async def test_verisure_errors( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert "flow_id" in result - mock_verisure_config_flow.login.side_effect = side_effect result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -263,7 +253,6 @@ async def test_verisure_errors( assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "user" assert result2.get("errors") == {"base": error} - assert "flow_id" in result2 mock_verisure_config_flow.login.side_effect = VerisureLoginError( "Multifactor authentication enabled, disable or create MFA cookie" @@ -284,7 +273,6 @@ async def test_verisure_errors( assert result3.get("type") == FlowResultType.FORM assert result3.get("step_id") == "user" assert result3.get("errors") == {"base": "unknown_mfa"} - assert "flow_id" in result3 result4 = await hass.config_entries.flow.async_configure( result3["flow_id"], @@ -297,7 +285,6 @@ async def test_verisure_errors( assert result4.get("type") == FlowResultType.FORM assert result4.get("step_id") == "mfa" - assert "flow_id" in result4 mock_verisure_config_flow.mfa_validate.side_effect = side_effect @@ -310,7 +297,6 @@ async def test_verisure_errors( assert result5.get("type") == FlowResultType.FORM assert result5.get("step_id") == "mfa" assert result5.get("errors") == {"base": error} - assert "flow_id" in result5 mock_verisure_config_flow.installations = [ mock_verisure_config_flow.installations[0] @@ -376,7 +362,6 @@ async def test_reauth_flow( assert result.get("step_id") == "reauth_confirm" assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -420,7 +405,6 @@ async def test_reauth_flow_with_mfa( assert result.get("step_id") == "reauth_confirm" assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} - assert "flow_id" in result mock_verisure_config_flow.login.side_effect = VerisureLoginError( "Multifactor authentication enabled, disable or create MFA cookie" @@ -437,7 +421,6 @@ async def test_reauth_flow_with_mfa( assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == "reauth_mfa" - assert "flow_id" in result2 mock_verisure_config_flow.login.side_effect = None @@ -491,8 +474,6 @@ async def test_reauth_flow_errors( data=mock_config_entry.data, ) - assert "flow_id" in result - mock_verisure_config_flow.login.side_effect = side_effect result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -506,7 +487,6 @@ async def test_reauth_flow_errors( assert result2.get("step_id") == "reauth_confirm" assert result2.get("type") == FlowResultType.FORM assert result2.get("errors") == {"base": error} - assert "flow_id" in result2 mock_verisure_config_flow.login.side_effect = VerisureLoginError( "Multifactor authentication enabled, disable or create MFA cookie" @@ -525,7 +505,6 @@ async def test_reauth_flow_errors( assert result3.get("type") == FlowResultType.FORM assert result3.get("step_id") == "reauth_confirm" assert result3.get("errors") == {"base": "unknown_mfa"} - assert "flow_id" in result3 mock_verisure_config_flow.login_mfa.side_effect = None @@ -540,7 +519,6 @@ async def test_reauth_flow_errors( assert result4.get("type") == FlowResultType.FORM assert result4.get("step_id") == "reauth_mfa" - assert "flow_id" in result4 mock_verisure_config_flow.mfa_validate.side_effect = side_effect @@ -553,7 +531,6 @@ async def test_reauth_flow_errors( assert result5.get("type") == FlowResultType.FORM assert result5.get("step_id") == "reauth_mfa" assert result5.get("errors") == {"base": error} - assert "flow_id" in result5 mock_verisure_config_flow.mfa_validate.side_effect = None mock_verisure_config_flow.login.side_effect = None @@ -627,7 +604,6 @@ async def test_options_flow( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" - assert "flow_id" in result result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -659,7 +635,6 @@ async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" assert result.get("errors") == {} - assert "flow_id" in result result = await hass.config_entries.options.async_configure( result["flow_id"], diff --git a/tests/components/vultr/fixtures/server_list.json b/tests/components/vultr/fixtures/server_list.json index fa29da3177d..259f2931e7f 100644 --- a/tests/components/vultr/fixtures/server_list.json +++ b/tests/components/vultr/fixtures/server_list.json @@ -50,7 +50,7 @@ "DCID": "1", "default_password": "nreqnusibni", "date_created": "2014-10-13 14:45:41", - "pending_charges": "not a number", + "pending_charges": "3.72", "status": "active", "cost_per_month": "73.25", "current_bandwidth_gb": 957.457, diff --git a/tests/components/vultr/test_sensor.py b/tests/components/vultr/test_sensor.py index a0b93a59124..95f91270fa9 100644 --- a/tests/components/vultr/test_sensor.py +++ b/tests/components/vultr/test_sensor.py @@ -9,7 +9,7 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PLATFORM, - DATA_GIGABYTES, + UnitOfInformation, ) from homeassistant.core import HomeAssistant @@ -58,7 +58,9 @@ def test_sensor(hass: HomeAssistant): device.update() - if device.unit_of_measurement == DATA_GIGABYTES: # Test Bandwidth Used + if ( + device.unit_of_measurement == UnitOfInformation.GIGABYTES + ): # Test Bandwidth Used if device.subscription == "576965": assert device.name == "Vultr my new server Current Bandwidth Used" assert device.icon == "mdi:chart-histogram" @@ -82,7 +84,7 @@ def test_sensor(hass: HomeAssistant): elif device.subscription == "123456": # Custom name with 1 {} assert device.name == "Server Pending Charges" - assert device.state == "not a number" + assert device.state == 3.72 tested += 1 elif device.subscription == "555555": # No {} in name diff --git a/tests/components/wallbox/test_sensor.py b/tests/components/wallbox/test_sensor.py index a224085f65b..d8a3926fd4c 100644 --- a/tests/components/wallbox/test_sensor.py +++ b/tests/components/wallbox/test_sensor.py @@ -1,5 +1,5 @@ """Test Wallbox Switch component.""" -from homeassistant.const import CONF_ICON, CONF_UNIT_OF_MEASUREMENT, POWER_KILO_WATT +from homeassistant.const import CONF_ICON, CONF_UNIT_OF_MEASUREMENT, UnitOfPower from homeassistant.core import HomeAssistant from . import entry, setup_integration @@ -16,7 +16,7 @@ async def test_wallbox_sensor_class(hass: HomeAssistant) -> None: await setup_integration(hass) state = hass.states.get(MOCK_SENSOR_CHARGING_POWER_ID) - assert state.attributes[CONF_UNIT_OF_MEASUREMENT] == POWER_KILO_WATT + assert state.attributes[CONF_UNIT_OF_MEASUREMENT] == UnitOfPower.KILO_WATT assert state.name == "Mock Title Charging Power" state = hass.states.get(MOCK_SENSOR_CHARGING_SPEED_ID) diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index 4b4f3a82d07..74d9f73bf6d 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -31,29 +31,23 @@ from homeassistant.components.weather import ( ) from homeassistant.const import ( ATTR_FRIENDLY_NAME, - LENGTH_INCHES, - LENGTH_KILOMETERS, - LENGTH_MILES, - LENGTH_MILLIMETERS, PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, - PRESSURE_HPA, - PRESSURE_INHG, - PRESSURE_PA, - SPEED_KILOMETERS_PER_HOUR, - SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, + UnitOfLength, + UnitOfPressure, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from homeassistant.util.distance import convert as convert_distance -from homeassistant.util.pressure import convert as convert_pressure -from homeassistant.util.speed import convert as convert_speed -from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.util.unit_conversion import ( + DistanceConverter, + PressureConverter, + SpeedConverter, + TemperatureConverter, +) from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM from tests.testing_config.custom_components.test import weather as WeatherPlatform @@ -66,15 +60,15 @@ class MockWeatherEntity(WeatherEntity): """Initiate Entity.""" super().__init__() self._attr_condition = ATTR_CONDITION_SUNNY - self._attr_native_precipitation_unit = LENGTH_MILLIMETERS + self._attr_native_precipitation_unit = UnitOfLength.MILLIMETERS self._attr_native_pressure = 10 - self._attr_native_pressure_unit = PRESSURE_HPA + self._attr_native_pressure_unit = UnitOfPressure.HPA self._attr_native_temperature = 20 - self._attr_native_temperature_unit = TEMP_CELSIUS + self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS self._attr_native_visibility = 30 - self._attr_native_visibility_unit = LENGTH_KILOMETERS + self._attr_native_visibility_unit = UnitOfLength.KILOMETERS self._attr_native_wind_speed = 3 - self._attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + self._attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND self._attr_forecast = [ Forecast( datetime=datetime(2022, 6, 20, 20, 00, 00), @@ -92,7 +86,7 @@ class MockWeatherEntityPrecision(WeatherEntity): super().__init__() self._attr_condition = ATTR_CONDITION_SUNNY self._attr_native_temperature = 20.3 - self._attr_native_temperature_unit = TEMP_CELSIUS + self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS self._attr_precision = PRECISION_HALVES @@ -103,15 +97,15 @@ class MockWeatherEntityCompat(WeatherEntity): """Initiate Entity.""" super().__init__() self._attr_condition = ATTR_CONDITION_SUNNY - self._attr_precipitation_unit = LENGTH_MILLIMETERS + self._attr_precipitation_unit = UnitOfLength.MILLIMETERS self._attr_pressure = 10 - self._attr_pressure_unit = PRESSURE_HPA + self._attr_pressure_unit = UnitOfPressure.HPA self._attr_temperature = 20 - self._attr_temperature_unit = TEMP_CELSIUS + self._attr_temperature_unit = UnitOfTemperature.CELSIUS self._attr_visibility = 30 - self._attr_visibility_unit = LENGTH_KILOMETERS + self._attr_visibility_unit = UnitOfLength.KILOMETERS self._attr_wind_speed = 3 - self._attr_wind_speed_unit = SPEED_METERS_PER_SECOND + self._attr_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND self._attr_forecast = [ Forecast( datetime=datetime(2022, 6, 20, 20, 00, 00), @@ -140,10 +134,15 @@ async def create_entity(hass: HomeAssistant, **kwargs): return entity0 -@pytest.mark.parametrize("native_unit", (TEMP_FAHRENHEIT, TEMP_CELSIUS)) +@pytest.mark.parametrize( + "native_unit", (UnitOfTemperature.FAHRENHEIT, UnitOfTemperature.CELSIUS) +) @pytest.mark.parametrize( "state_unit, unit_system", - ((TEMP_CELSIUS, METRIC_SYSTEM), (TEMP_FAHRENHEIT, US_CUSTOMARY_SYSTEM)), + ( + (UnitOfTemperature.CELSIUS, METRIC_SYSTEM), + (UnitOfTemperature.FAHRENHEIT, US_CUSTOMARY_SYSTEM), + ), ) async def test_temperature( hass: HomeAssistant, @@ -155,7 +154,7 @@ async def test_temperature( """Test temperature.""" hass.config.units = unit_system native_value = 38 - state_value = convert_temperature(native_value, native_unit, state_unit) + state_value = TemperatureConverter.convert(native_value, native_unit, state_unit) entity0 = await create_entity( hass, native_temperature=native_value, native_temperature_unit=native_unit @@ -176,7 +175,10 @@ async def test_temperature( @pytest.mark.parametrize("native_unit", (None,)) @pytest.mark.parametrize( "state_unit, unit_system", - ((TEMP_CELSIUS, METRIC_SYSTEM), (TEMP_FAHRENHEIT, US_CUSTOMARY_SYSTEM)), + ( + (UnitOfTemperature.CELSIUS, METRIC_SYSTEM), + (UnitOfTemperature.FAHRENHEIT, US_CUSTOMARY_SYSTEM), + ), ) async def test_temperature_no_unit( hass: HomeAssistant, @@ -206,10 +208,10 @@ async def test_temperature_no_unit( assert float(forecast[ATTR_FORECAST_TEMP_LOW]) == approx(expected, rel=0.1) -@pytest.mark.parametrize("native_unit", (PRESSURE_INHG, PRESSURE_INHG)) +@pytest.mark.parametrize("native_unit", (UnitOfPressure.INHG, UnitOfPressure.INHG)) @pytest.mark.parametrize( "state_unit, unit_system", - ((PRESSURE_HPA, METRIC_SYSTEM), (PRESSURE_INHG, US_CUSTOMARY_SYSTEM)), + ((UnitOfPressure.HPA, METRIC_SYSTEM), (UnitOfPressure.INHG, US_CUSTOMARY_SYSTEM)), ) async def test_pressure( hass: HomeAssistant, @@ -221,7 +223,7 @@ async def test_pressure( """Test pressure.""" hass.config.units = unit_system native_value = 30 - state_value = convert_pressure(native_value, native_unit, state_unit) + state_value = PressureConverter.convert(native_value, native_unit, state_unit) entity0 = await create_entity( hass, native_pressure=native_value, native_pressure_unit=native_unit @@ -237,7 +239,7 @@ async def test_pressure( @pytest.mark.parametrize("native_unit", (None,)) @pytest.mark.parametrize( "state_unit, unit_system", - ((PRESSURE_HPA, METRIC_SYSTEM), (PRESSURE_INHG, US_CUSTOMARY_SYSTEM)), + ((UnitOfPressure.HPA, METRIC_SYSTEM), (UnitOfPressure.INHG, US_CUSTOMARY_SYSTEM)), ) async def test_pressure_no_unit( hass: HomeAssistant, @@ -264,13 +266,17 @@ async def test_pressure_no_unit( @pytest.mark.parametrize( "native_unit", - (SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR, SPEED_METERS_PER_SECOND), + ( + UnitOfSpeed.MILES_PER_HOUR, + UnitOfSpeed.KILOMETERS_PER_HOUR, + UnitOfSpeed.METERS_PER_SECOND, + ), ) @pytest.mark.parametrize( "state_unit, unit_system", ( - (SPEED_KILOMETERS_PER_HOUR, METRIC_SYSTEM), - (SPEED_MILES_PER_HOUR, US_CUSTOMARY_SYSTEM), + (UnitOfSpeed.KILOMETERS_PER_HOUR, METRIC_SYSTEM), + (UnitOfSpeed.MILES_PER_HOUR, US_CUSTOMARY_SYSTEM), ), ) async def test_wind_speed( @@ -283,7 +289,7 @@ async def test_wind_speed( """Test wind speed.""" hass.config.units = unit_system native_value = 10 - state_value = convert_speed(native_value, native_unit, state_unit) + state_value = SpeedConverter.convert(native_value, native_unit, state_unit) entity0 = await create_entity( hass, native_wind_speed=native_value, native_wind_speed_unit=native_unit @@ -303,8 +309,8 @@ async def test_wind_speed( @pytest.mark.parametrize( "state_unit, unit_system", ( - (SPEED_KILOMETERS_PER_HOUR, METRIC_SYSTEM), - (SPEED_MILES_PER_HOUR, US_CUSTOMARY_SYSTEM), + (UnitOfSpeed.KILOMETERS_PER_HOUR, METRIC_SYSTEM), + (UnitOfSpeed.MILES_PER_HOUR, US_CUSTOMARY_SYSTEM), ), ) async def test_wind_speed_no_unit( @@ -333,12 +339,12 @@ async def test_wind_speed_no_unit( assert float(forecast[ATTR_FORECAST_WIND_SPEED]) == approx(expected, rel=1e-2) -@pytest.mark.parametrize("native_unit", (LENGTH_MILES, LENGTH_KILOMETERS)) +@pytest.mark.parametrize("native_unit", (UnitOfLength.MILES, UnitOfLength.KILOMETERS)) @pytest.mark.parametrize( "state_unit, unit_system", ( - (LENGTH_KILOMETERS, METRIC_SYSTEM), - (LENGTH_MILES, US_CUSTOMARY_SYSTEM), + (UnitOfLength.KILOMETERS, METRIC_SYSTEM), + (UnitOfLength.MILES, US_CUSTOMARY_SYSTEM), ), ) async def test_visibility( @@ -351,7 +357,7 @@ async def test_visibility( """Test visibility.""" hass.config.units = unit_system native_value = 10 - state_value = convert_distance(native_value, native_unit, state_unit) + state_value = DistanceConverter.convert(native_value, native_unit, state_unit) entity0 = await create_entity( hass, native_visibility=native_value, native_visibility_unit=native_unit @@ -368,8 +374,8 @@ async def test_visibility( @pytest.mark.parametrize( "state_unit, unit_system", ( - (LENGTH_KILOMETERS, METRIC_SYSTEM), - (LENGTH_MILES, US_CUSTOMARY_SYSTEM), + (UnitOfLength.KILOMETERS, METRIC_SYSTEM), + (UnitOfLength.MILES, US_CUSTOMARY_SYSTEM), ), ) async def test_visibility_no_unit( @@ -395,12 +401,12 @@ async def test_visibility_no_unit( ) -@pytest.mark.parametrize("native_unit", (LENGTH_INCHES, LENGTH_MILLIMETERS)) +@pytest.mark.parametrize("native_unit", (UnitOfLength.INCHES, UnitOfLength.MILLIMETERS)) @pytest.mark.parametrize( "state_unit, unit_system", ( - (LENGTH_MILLIMETERS, METRIC_SYSTEM), - (LENGTH_INCHES, US_CUSTOMARY_SYSTEM), + (UnitOfLength.MILLIMETERS, METRIC_SYSTEM), + (UnitOfLength.INCHES, US_CUSTOMARY_SYSTEM), ), ) async def test_precipitation( @@ -413,7 +419,7 @@ async def test_precipitation( """Test precipitation.""" hass.config.units = unit_system native_value = 30 - state_value = convert_distance(native_value, native_unit, state_unit) + state_value = DistanceConverter.convert(native_value, native_unit, state_unit) entity0 = await create_entity( hass, native_precipitation=native_value, native_precipitation_unit=native_unit @@ -430,8 +436,8 @@ async def test_precipitation( @pytest.mark.parametrize( "state_unit, unit_system", ( - (LENGTH_MILLIMETERS, METRIC_SYSTEM), - (LENGTH_INCHES, US_CUSTOMARY_SYSTEM), + (UnitOfLength.MILLIMETERS, METRIC_SYSTEM), + (UnitOfLength.INCHES, US_CUSTOMARY_SYSTEM), ), ) async def test_precipitation_no_unit( @@ -482,11 +488,11 @@ async def test_none_forecast( entity0 = await create_entity( hass, native_pressure=None, - native_pressure_unit=PRESSURE_INHG, + native_pressure_unit=UnitOfPressure.INHG, native_wind_speed=None, - native_wind_speed_unit=SPEED_METERS_PER_SECOND, + native_wind_speed_unit=UnitOfSpeed.METERS_PER_SECOND, native_precipitation=None, - native_precipitation_unit=LENGTH_MILLIMETERS, + native_precipitation_unit=UnitOfLength.MILLIMETERS, ) state = hass.states.get(entity0.entity_id) @@ -500,22 +506,22 @@ async def test_none_forecast( async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> None: """Test custom unit.""" wind_speed_value = 5 - wind_speed_unit = SPEED_METERS_PER_SECOND + wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND pressure_value = 110 - pressure_unit = PRESSURE_HPA + pressure_unit = UnitOfPressure.HPA temperature_value = 20 - temperature_unit = TEMP_CELSIUS + temperature_unit = UnitOfTemperature.CELSIUS visibility_value = 11 - visibility_unit = LENGTH_KILOMETERS + visibility_unit = UnitOfLength.KILOMETERS precipitation_value = 1.1 - precipitation_unit = LENGTH_MILLIMETERS + precipitation_unit = UnitOfLength.MILLIMETERS set_options = { - "wind_speed_unit": SPEED_MILES_PER_HOUR, - "precipitation_unit": LENGTH_INCHES, - "pressure_unit": PRESSURE_INHG, - "temperature_unit": TEMP_FAHRENHEIT, - "visibility_unit": LENGTH_MILES, + "wind_speed_unit": UnitOfSpeed.MILES_PER_HOUR, + "precipitation_unit": UnitOfLength.INCHES, + "pressure_unit": UnitOfPressure.INHG, + "temperature_unit": UnitOfTemperature.FAHRENHEIT, + "visibility_unit": UnitOfLength.MILES, } entity_registry = er.async_get(hass) @@ -554,22 +560,28 @@ async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> forecast = state.attributes[ATTR_FORECAST][0] expected_wind_speed = round( - convert_speed(wind_speed_value, wind_speed_unit, SPEED_MILES_PER_HOUR), + SpeedConverter.convert( + wind_speed_value, wind_speed_unit, UnitOfSpeed.MILES_PER_HOUR + ), ROUNDING_PRECISION, ) - expected_temperature = convert_temperature( - temperature_value, temperature_unit, TEMP_FAHRENHEIT + expected_temperature = TemperatureConverter.convert( + temperature_value, temperature_unit, UnitOfTemperature.FAHRENHEIT ) expected_pressure = round( - convert_pressure(pressure_value, pressure_unit, PRESSURE_INHG), + PressureConverter.convert(pressure_value, pressure_unit, UnitOfPressure.INHG), ROUNDING_PRECISION, ) expected_visibility = round( - convert_distance(visibility_value, visibility_unit, LENGTH_MILES), + DistanceConverter.convert( + visibility_value, visibility_unit, UnitOfLength.MILES + ), ROUNDING_PRECISION, ) expected_precipitation = round( - convert_distance(precipitation_value, precipitation_unit, LENGTH_INCHES), + DistanceConverter.convert( + precipitation_value, precipitation_unit, UnitOfLength.INCHES + ), ROUNDING_PRECISION, ) @@ -609,15 +621,15 @@ async def test_backwards_compatibility( ) -> None: """Test backwards compatibility.""" wind_speed_value = 5 - wind_speed_unit = SPEED_METERS_PER_SECOND + wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND pressure_value = 110000 - pressure_unit = PRESSURE_PA + pressure_unit = UnitOfPressure.PA temperature_value = 20 - temperature_unit = TEMP_CELSIUS + temperature_unit = UnitOfTemperature.CELSIUS visibility_value = 11 - visibility_unit = LENGTH_KILOMETERS + visibility_unit = UnitOfLength.KILOMETERS precipitation_value = 1 - precipitation_unit = LENGTH_MILLIMETERS + precipitation_unit = UnitOfLength.MILLIMETERS hass.config.units = METRIC_SYSTEM @@ -672,36 +684,44 @@ async def test_backwards_compatibility( assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == approx( wind_speed_value * 3.6 ) - assert state.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] == SPEED_KILOMETERS_PER_HOUR + assert ( + state.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] + == UnitOfSpeed.KILOMETERS_PER_HOUR + ) assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( temperature_value, rel=0.1 ) - assert state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == TEMP_CELSIUS + assert state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == UnitOfTemperature.CELSIUS assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == approx( pressure_value / 100 ) - assert state.attributes[ATTR_WEATHER_PRESSURE_UNIT] == PRESSURE_HPA + assert state.attributes[ATTR_WEATHER_PRESSURE_UNIT] == UnitOfPressure.HPA assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == approx(visibility_value) - assert state.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == LENGTH_KILOMETERS + assert state.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == UnitOfLength.KILOMETERS assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx( precipitation_value, rel=1e-2 ) - assert state.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == LENGTH_MILLIMETERS + assert state.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == UnitOfLength.MILLIMETERS assert float(state1.attributes[ATTR_WEATHER_WIND_SPEED]) == approx(wind_speed_value) - assert state1.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] == SPEED_KILOMETERS_PER_HOUR + assert ( + state1.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] + == UnitOfSpeed.KILOMETERS_PER_HOUR + ) assert float(state1.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( temperature_value, rel=0.1 ) - assert state1.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == TEMP_CELSIUS + assert state1.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == UnitOfTemperature.CELSIUS assert float(state1.attributes[ATTR_WEATHER_PRESSURE]) == approx(pressure_value) - assert state1.attributes[ATTR_WEATHER_PRESSURE_UNIT] == PRESSURE_HPA + assert state1.attributes[ATTR_WEATHER_PRESSURE_UNIT] == UnitOfPressure.HPA assert float(state1.attributes[ATTR_WEATHER_VISIBILITY]) == approx(visibility_value) - assert state1.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == LENGTH_KILOMETERS + assert state1.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == UnitOfLength.KILOMETERS assert float(forecast1[ATTR_FORECAST_PRECIPITATION]) == approx( precipitation_value, rel=1e-2 ) - assert state1.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == LENGTH_MILLIMETERS + assert ( + state1.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == UnitOfLength.MILLIMETERS + ) async def test_backwards_compatibility_convert_values( @@ -709,15 +729,15 @@ async def test_backwards_compatibility_convert_values( ) -> None: """Test backward compatibility for converting values.""" wind_speed_value = 5 - wind_speed_unit = SPEED_METERS_PER_SECOND + wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND pressure_value = 110000 - pressure_unit = PRESSURE_PA + pressure_unit = UnitOfPressure.PA temperature_value = 20 - temperature_unit = TEMP_CELSIUS + temperature_unit = UnitOfTemperature.CELSIUS visibility_value = 11 - visibility_unit = LENGTH_KILOMETERS + visibility_unit = UnitOfLength.KILOMETERS precipitation_value = 1 - precipitation_unit = LENGTH_MILLIMETERS + precipitation_unit = UnitOfLength.MILLIMETERS hass.config.units = US_CUSTOMARY_SYSTEM @@ -750,22 +770,28 @@ async def test_backwards_compatibility_convert_values( state = hass.states.get(entity0.entity_id) expected_wind_speed = round( - convert_speed(wind_speed_value, wind_speed_unit, SPEED_MILES_PER_HOUR), + SpeedConverter.convert( + wind_speed_value, wind_speed_unit, UnitOfSpeed.MILES_PER_HOUR + ), ROUNDING_PRECISION, ) - expected_temperature = convert_temperature( - temperature_value, temperature_unit, TEMP_FAHRENHEIT + expected_temperature = TemperatureConverter.convert( + temperature_value, temperature_unit, UnitOfTemperature.FAHRENHEIT ) expected_pressure = round( - convert_pressure(pressure_value, pressure_unit, PRESSURE_INHG), + PressureConverter.convert(pressure_value, pressure_unit, UnitOfPressure.INHG), ROUNDING_PRECISION, ) expected_visibility = round( - convert_distance(visibility_value, visibility_unit, LENGTH_MILES), + DistanceConverter.convert( + visibility_value, visibility_unit, UnitOfLength.MILES + ), ROUNDING_PRECISION, ) expected_precipitation = round( - convert_distance(precipitation_value, precipitation_unit, LENGTH_INCHES), + DistanceConverter.convert( + precipitation_value, precipitation_unit, UnitOfLength.INCHES + ), ROUNDING_PRECISION, ) @@ -781,15 +807,15 @@ async def test_backwards_compatibility_convert_values( } ], ATTR_FRIENDLY_NAME: "Test", - ATTR_WEATHER_PRECIPITATION_UNIT: LENGTH_INCHES, + ATTR_WEATHER_PRECIPITATION_UNIT: UnitOfLength.INCHES, ATTR_WEATHER_PRESSURE: approx(expected_pressure, rel=0.1), - ATTR_WEATHER_PRESSURE_UNIT: PRESSURE_INHG, + ATTR_WEATHER_PRESSURE_UNIT: UnitOfPressure.INHG, ATTR_WEATHER_TEMPERATURE: approx(expected_temperature, rel=0.1), - ATTR_WEATHER_TEMPERATURE_UNIT: TEMP_FAHRENHEIT, + ATTR_WEATHER_TEMPERATURE_UNIT: UnitOfTemperature.FAHRENHEIT, ATTR_WEATHER_VISIBILITY: approx(expected_visibility, rel=0.1), - ATTR_WEATHER_VISIBILITY_UNIT: LENGTH_MILES, + ATTR_WEATHER_VISIBILITY_UNIT: UnitOfLength.MILES, ATTR_WEATHER_WIND_SPEED: approx(expected_wind_speed, rel=0.1), - ATTR_WEATHER_WIND_SPEED_UNIT: SPEED_MILES_PER_HOUR, + ATTR_WEATHER_WIND_SPEED_UNIT: UnitOfSpeed.MILES_PER_HOUR, } @@ -809,20 +835,20 @@ async def test_attr(hass: HomeAssistant) -> None: weather.hass = hass assert weather.condition == ATTR_CONDITION_SUNNY - assert weather.native_precipitation_unit == LENGTH_MILLIMETERS - assert weather._precipitation_unit == LENGTH_MILLIMETERS + assert weather.native_precipitation_unit == UnitOfLength.MILLIMETERS + assert weather._precipitation_unit == UnitOfLength.MILLIMETERS assert weather.native_pressure == 10 - assert weather.native_pressure_unit == PRESSURE_HPA - assert weather._pressure_unit == PRESSURE_HPA + assert weather.native_pressure_unit == UnitOfPressure.HPA + assert weather._pressure_unit == UnitOfPressure.HPA assert weather.native_temperature == 20 - assert weather.native_temperature_unit == TEMP_CELSIUS - assert weather._temperature_unit == TEMP_CELSIUS + assert weather.native_temperature_unit == UnitOfTemperature.CELSIUS + assert weather._temperature_unit == UnitOfTemperature.CELSIUS assert weather.native_visibility == 30 - assert weather.native_visibility_unit == LENGTH_KILOMETERS - assert weather._visibility_unit == LENGTH_KILOMETERS + assert weather.native_visibility_unit == UnitOfLength.KILOMETERS + assert weather._visibility_unit == UnitOfLength.KILOMETERS assert weather.native_wind_speed == 3 - assert weather.native_wind_speed_unit == SPEED_METERS_PER_SECOND - assert weather._wind_speed_unit == SPEED_KILOMETERS_PER_HOUR + assert weather.native_wind_speed_unit == UnitOfSpeed.METERS_PER_SECOND + assert weather._wind_speed_unit == UnitOfSpeed.KILOMETERS_PER_HOUR async def test_attr_compatibility(hass: HomeAssistant) -> None: @@ -832,15 +858,15 @@ async def test_attr_compatibility(hass: HomeAssistant) -> None: weather.hass = hass assert weather.condition == ATTR_CONDITION_SUNNY - assert weather._precipitation_unit == LENGTH_MILLIMETERS + assert weather._precipitation_unit == UnitOfLength.MILLIMETERS assert weather.pressure == 10 - assert weather._pressure_unit == PRESSURE_HPA + assert weather._pressure_unit == UnitOfPressure.HPA assert weather.temperature == 20 - assert weather._temperature_unit == TEMP_CELSIUS + assert weather._temperature_unit == UnitOfTemperature.CELSIUS assert weather.visibility == 30 - assert weather.visibility_unit == LENGTH_KILOMETERS + assert weather.visibility_unit == UnitOfLength.KILOMETERS assert weather.wind_speed == 3 - assert weather._wind_speed_unit == SPEED_KILOMETERS_PER_HOUR + assert weather._wind_speed_unit == UnitOfSpeed.KILOMETERS_PER_HOUR forecast_entry = [ Forecast( @@ -855,14 +881,14 @@ async def test_attr_compatibility(hass: HomeAssistant) -> None: assert weather.state_attributes == { ATTR_FORECAST: forecast_entry, ATTR_WEATHER_PRESSURE: 10.0, - ATTR_WEATHER_PRESSURE_UNIT: PRESSURE_HPA, + ATTR_WEATHER_PRESSURE_UNIT: UnitOfPressure.HPA, ATTR_WEATHER_TEMPERATURE: 20.0, - ATTR_WEATHER_TEMPERATURE_UNIT: TEMP_CELSIUS, + ATTR_WEATHER_TEMPERATURE_UNIT: UnitOfTemperature.CELSIUS, ATTR_WEATHER_VISIBILITY: 30.0, - ATTR_WEATHER_VISIBILITY_UNIT: LENGTH_KILOMETERS, + ATTR_WEATHER_VISIBILITY_UNIT: UnitOfLength.KILOMETERS, ATTR_WEATHER_WIND_SPEED: 3.0 * 3.6, - ATTR_WEATHER_WIND_SPEED_UNIT: SPEED_KILOMETERS_PER_HOUR, - ATTR_WEATHER_PRECIPITATION_UNIT: LENGTH_MILLIMETERS, + ATTR_WEATHER_WIND_SPEED_UNIT: UnitOfSpeed.KILOMETERS_PER_HOUR, + ATTR_WEATHER_PRECIPITATION_UNIT: UnitOfLength.MILLIMETERS, } @@ -874,7 +900,7 @@ async def test_precision_for_temperature(hass: HomeAssistant) -> None: assert weather.condition == ATTR_CONDITION_SUNNY assert weather.native_temperature == 20.3 - assert weather._temperature_unit == TEMP_CELSIUS + assert weather._temperature_unit == UnitOfTemperature.CELSIUS assert weather.precision == PRECISION_HALVES assert weather.state_attributes[ATTR_WEATHER_TEMPERATURE] == 20.5 diff --git a/tests/components/webostv/conftest.py b/tests/components/webostv/conftest.py index c8333c84447..5a55ac492df 100644 --- a/tests/components/webostv/conftest.py +++ b/tests/components/webostv/conftest.py @@ -4,7 +4,6 @@ from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant.components.webostv.const import LIVE_TV_APP_ID -from homeassistant.helpers import entity_registry from .const import CHANNEL_1, CHANNEL_2, CLIENT_KEY, FAKE_UUID, MOCK_APPS, MOCK_INPUTS @@ -48,28 +47,3 @@ def client_fixture(): client.mock_state_update = AsyncMock(side_effect=mock_state_update_callback) yield client - - -@pytest.fixture(name="client_entity_removed") -def client_entity_removed_fixture(hass): - """Patch of client library, entity removed waiting for connect.""" - with patch( - "homeassistant.components.webostv.WebOsClient", autospec=True - ) as mock_client_class: - client = mock_client_class.return_value - client.hello_info = {"deviceUUID": FAKE_UUID} - client.connected = False - - def mock_is_connected(): - return client.connected - - client.is_connected = Mock(side_effect=mock_is_connected) - - async def mock_connected(): - ent_reg = entity_registry.async_get(hass) - ent_reg.async_remove("media_player.webostv_some_secret") - client.connected = True - - client.connect = AsyncMock(side_effect=mock_connected) - - yield client diff --git a/tests/components/webostv/const.py b/tests/components/webostv/const.py index eca38837d8e..fbdb9c47c3b 100644 --- a/tests/components/webostv/const.py +++ b/tests/components/webostv/const.py @@ -7,8 +7,6 @@ TV_NAME = "fake_webos" ENTITY_ID = f"{MP_DOMAIN}.{TV_NAME}" HOST = "1.2.3.4" CLIENT_KEY = "some-secret" -MOCK_CLIENT_KEYS = {HOST: CLIENT_KEY} -MOCK_JSON = '{"1.2.3.4": "some-secret"}' CHANNEL_1 = { "channelNumber": "1", diff --git a/tests/components/webostv/test_config_flow.py b/tests/components/webostv/test_config_flow.py index b5ad3f4cc2b..952307d9c26 100644 --- a/tests/components/webostv/test_config_flow.py +++ b/tests/components/webostv/test_config_flow.py @@ -9,25 +9,15 @@ from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.webostv.const import CONF_SOURCES, DOMAIN, LIVE_TV_APP_ID from homeassistant.config_entries import SOURCE_SSDP -from homeassistant.const import ( - CONF_CLIENT_SECRET, - CONF_HOST, - CONF_ICON, - CONF_NAME, - CONF_SOURCE, - CONF_UNIQUE_ID, -) +from homeassistant.const import CONF_CLIENT_SECRET, CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.data_entry_flow import FlowResultType from . import setup_webostv from .const import CLIENT_KEY, FAKE_UUID, HOST, MOCK_APPS, MOCK_INPUTS, TV_NAME -MOCK_YAML_CONFIG = { +MOCK_USER_CONFIG = { CONF_HOST: HOST, CONF_NAME: TV_NAME, - CONF_ICON: "mdi:test", - CONF_CLIENT_SECRET: CLIENT_KEY, - CONF_UNIQUE_ID: FAKE_UUID, } MOCK_DISCOVERY_INFO = ssdp.SsdpServiceInfo( @@ -57,7 +47,7 @@ async def test_form(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER}, - data=MOCK_YAML_CONFIG, + data=MOCK_USER_CONFIG, ) await hass.async_block_till_done() @@ -67,7 +57,7 @@ async def test_form(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER}, - data=MOCK_YAML_CONFIG, + data=MOCK_USER_CONFIG, ) await hass.async_block_till_done() @@ -141,7 +131,7 @@ async def test_form_cannot_connect(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER}, - data=MOCK_YAML_CONFIG, + data=MOCK_USER_CONFIG, ) client.connect = Mock(side_effect=ConnectionRefusedError()) @@ -159,7 +149,7 @@ async def test_form_pairexception(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER}, - data=MOCK_YAML_CONFIG, + data=MOCK_USER_CONFIG, ) client.connect = Mock(side_effect=WebOsTvPairError("error")) @@ -180,7 +170,7 @@ async def test_entry_already_configured(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER}, - data=MOCK_YAML_CONFIG, + data=MOCK_USER_CONFIG, ) assert result["type"] == FlowResultType.ABORT @@ -208,7 +198,7 @@ async def test_ssdp_in_progress(hass, client): result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER}, - data=MOCK_YAML_CONFIG, + data=MOCK_USER_CONFIG, ) await hass.async_block_till_done() @@ -299,3 +289,64 @@ async def test_form_abort_uuid_configured(hass, client): assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_HOST] == "new_host" + + +async def test_reauth_successful(hass, client, monkeypatch): + """Test that the reauthorization is successful.""" + entry = await setup_webostv(hass) + assert client + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH, "entry_id": entry.entry_id}, + data=entry.data, + ) + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert entry.data[CONF_CLIENT_SECRET] == CLIENT_KEY + + monkeypatch.setattr(client, "client_key", "new_key") + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert entry.data[CONF_CLIENT_SECRET] == "new_key" + + +@pytest.mark.parametrize( + "side_effect,reason", + [ + (WebOsTvPairError, "error_pairing"), + (ConnectionRefusedError, "reauth_unsuccessful"), + ], +) +async def test_reauth_errors(hass, client, monkeypatch, side_effect, reason): + """Test reauthorization errors.""" + entry = await setup_webostv(hass) + assert client + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH, "entry_id": entry.entry_id}, + data=entry.data, + ) + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + monkeypatch.setattr(client, "connect", Mock(side_effect=side_effect)) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == reason diff --git a/tests/components/webostv/test_init.py b/tests/components/webostv/test_init.py new file mode 100644 index 00000000000..e48bb9d80fd --- /dev/null +++ b/tests/components/webostv/test_init.py @@ -0,0 +1,39 @@ +"""The tests for the LG webOS TV platform.""" +from unittest.mock import Mock + +from aiowebostv import WebOsTvPairError + +from homeassistant.components.webostv.const import DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState +from homeassistant.const import CONF_CLIENT_SECRET + +from . import setup_webostv + + +async def test_reauth_setup_entry(hass, client, monkeypatch): + """Test reauth flow triggered by setup entry.""" + monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) + monkeypatch.setattr(client, "connect", Mock(side_effect=WebOsTvPairError)) + entry = await setup_webostv(hass) + + assert entry.state == ConfigEntryState.SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == entry.entry_id + + +async def test_key_update_setup_entry(hass, client, monkeypatch): + """Test key update from setup entry.""" + monkeypatch.setattr(client, "client_key", "new_key") + entry = await setup_webostv(hass) + + assert entry.state == ConfigEntryState.LOADED + assert entry.data[CONF_CLIENT_SECRET] == "new_key" diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index e4e2e2ba45f..f12c07c66c9 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -4,6 +4,7 @@ from datetime import timedelta from http import HTTPStatus from unittest.mock import Mock +from aiowebostv import WebOsTvPairError import pytest from homeassistant.components import automation @@ -37,6 +38,7 @@ from homeassistant.components.webostv.media_player import ( SUPPORT_WEBOSTV, SUPPORT_WEBOSTV_VOLUME, ) +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import ( ATTR_COMMAND, ATTR_DEVICE_CLASS, @@ -763,3 +765,28 @@ async def test_get_image_https( content = await resp.read() assert content == b"https_image" + + +async def test_reauth_reconnect(hass, client, monkeypatch): + """Test reauth flow triggered by reconnect.""" + entry = await setup_webostv(hass) + monkeypatch.setattr(client, "is_connected", Mock(return_value=False)) + monkeypatch.setattr(client, "connect", Mock(side_effect=WebOsTvPairError)) + + assert entry.state == ConfigEntryState.LOADED + + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=20)) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + flow = flows[0] + assert flow.get("step_id") == "reauth_confirm" + assert flow.get("handler") == DOMAIN + + assert "context" in flow + assert flow["context"].get("source") == SOURCE_REAUTH + assert flow["context"].get("entry_id") == entry.entry_id diff --git a/tests/components/whirlpool/__init__.py b/tests/components/whirlpool/__init__.py index 3f50518b4ad..d47fb5337fd 100644 --- a/tests/components/whirlpool/__init__.py +++ b/tests/components/whirlpool/__init__.py @@ -1,21 +1,29 @@ """Tests for the Whirlpool Sixth Sense integration.""" from homeassistant.components.whirlpool.const import DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -async def init_integration(hass: HomeAssistant) -> MockConfigEntry: +async def init_integration(hass: HomeAssistant, region: str = "EU") -> MockConfigEntry: """Set up the Whirlpool integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, data={ CONF_USERNAME: "nobody", CONF_PASSWORD: "qwerty", + CONF_REGION: region, }, ) + return await init_integration_with_entry(hass, entry) + + +async def init_integration_with_entry( + hass: HomeAssistant, entry: MockConfigEntry +) -> MockConfigEntry: + """Set up the Whirlpool integration in Home Assistant.""" entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/whirlpool/conftest.py b/tests/components/whirlpool/conftest.py index eba58b07faa..dd06c2d768f 100644 --- a/tests/components/whirlpool/conftest.py +++ b/tests/components/whirlpool/conftest.py @@ -1,13 +1,25 @@ """Fixtures for the Whirlpool Sixth Sense integration tests.""" from unittest import mock -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, MagicMock import pytest import whirlpool import whirlpool.aircon +from whirlpool.backendselector import Brand, Region MOCK_SAID1 = "said1" MOCK_SAID2 = "said2" +MOCK_SAID3 = "said3" +MOCK_SAID4 = "said4" + + +@pytest.fixture( + name="region", + params=[("EU", Region.EU, Brand.Whirlpool), ("US", Region.US, Brand.Maytag)], +) +def fixture_region(request): + """Return a region for input.""" + return request.param @pytest.fixture(name="mock_auth_api") @@ -30,13 +42,28 @@ def fixture_mock_appliances_manager_api(): {"SAID": MOCK_SAID1, "NAME": "TestZone"}, {"SAID": MOCK_SAID2, "NAME": "TestZone"}, ] + mock_appliances_manager.return_value.washer_dryers = [ + {"SAID": MOCK_SAID3, "NAME": "washer"}, + {"SAID": MOCK_SAID4, "NAME": "dryer"}, + ] yield mock_appliances_manager +@pytest.fixture(name="mock_backend_selector_api") +def fixture_mock_backend_selector_api(): + """Set up BackendSelector fixture.""" + with mock.patch( + "homeassistant.components.whirlpool.BackendSelector" + ) as mock_backend_selector: + yield mock_backend_selector + + def get_aircon_mock(said): """Get a mock of an air conditioner.""" mock_aircon = mock.Mock(said=said) mock_aircon.connect = AsyncMock() + mock_aircon.disconnect = AsyncMock() + mock_aircon.register_attr_callback = MagicMock() mock_aircon.get_online.return_value = True mock_aircon.get_power_on.return_value = True mock_aircon.get_mode.return_value = whirlpool.aircon.Mode.Cool @@ -58,19 +85,19 @@ def get_aircon_mock(said): return mock_aircon -@pytest.fixture(name="mock_aircon1_api", autouse=True) +@pytest.fixture(name="mock_aircon1_api", autouse=False) def fixture_mock_aircon1_api(mock_auth_api, mock_appliances_manager_api): """Set up air conditioner API fixture.""" yield get_aircon_mock(MOCK_SAID1) -@pytest.fixture(name="mock_aircon2_api", autouse=True) +@pytest.fixture(name="mock_aircon2_api", autouse=False) def fixture_mock_aircon2_api(mock_auth_api, mock_appliances_manager_api): """Set up air conditioner API fixture.""" yield get_aircon_mock(MOCK_SAID2) -@pytest.fixture(name="mock_aircon_api_instances", autouse=True) +@pytest.fixture(name="mock_aircon_api_instances", autouse=False) def fixture_mock_aircon_api_instances(mock_aircon1_api, mock_aircon2_api): """Set up air conditioner API fixture.""" with mock.patch( @@ -78,3 +105,61 @@ def fixture_mock_aircon_api_instances(mock_aircon1_api, mock_aircon2_api): ) as mock_aircon_api: mock_aircon_api.side_effect = [mock_aircon1_api, mock_aircon2_api] yield mock_aircon_api + + +def side_effect_function(*args, **kwargs): + """Return correct value for attribute.""" + if args[0] == "Cavity_TimeStatusEstTimeRemaining": + return 3540 + if args[0] == "Cavity_OpStatusDoorOpen": + return "0" + if args[0] == "WashCavity_OpStatusBulkDispense1Level": + return "3" + if args[0] == "Cavity_TimeStatusEstTimeRemaining": + return "4000" + + +def get_sensor_mock(said): + """Get a mock of a sensor.""" + mock_sensor = mock.Mock(said=said) + mock_sensor.connect = AsyncMock() + mock_sensor.disconnect = AsyncMock() + mock_sensor.register_attr_callback = MagicMock() + mock_sensor.get_online.return_value = True + mock_sensor.get_machine_state.return_value = ( + whirlpool.washerdryer.MachineState.Standby + ) + mock_sensor.get_attribute.side_effect = side_effect_function + mock_sensor.get_cycle_status_filling.return_value = False + mock_sensor.get_cycle_status_rinsing.return_value = False + mock_sensor.get_cycle_status_sensing.return_value = False + mock_sensor.get_cycle_status_soaking.return_value = False + mock_sensor.get_cycle_status_spinning.return_value = False + mock_sensor.get_cycle_status_washing.return_value = False + + return mock_sensor + + +@pytest.fixture(name="mock_sensor1_api", autouse=False) +def fixture_mock_sensor1_api(mock_auth_api, mock_appliances_manager_api): + """Set up sensor API fixture.""" + yield get_sensor_mock(MOCK_SAID3) + + +@pytest.fixture(name="mock_sensor2_api", autouse=False) +def fixture_mock_sensor2_api(mock_auth_api, mock_appliances_manager_api): + """Set up sensor API fixture.""" + yield get_sensor_mock(MOCK_SAID4) + + +@pytest.fixture(name="mock_sensor_api_instances", autouse=False) +def fixture_mock_sensor_api_instances(mock_sensor1_api, mock_sensor2_api): + """Set up sensor API fixture.""" + with mock.patch( + "homeassistant.components.whirlpool.sensor.WasherDryer" + ) as mock_sensor_api: + mock_sensor_api.side_effect = [ + mock_sensor1_api, + mock_sensor2_api, + ] + yield mock_sensor_api diff --git a/tests/components/whirlpool/test_climate.py b/tests/components/whirlpool/test_climate.py index 26dcd5dbf9f..efbe1076749 100644 --- a/tests/components/whirlpool/test_climate.py +++ b/tests/components/whirlpool/test_climate.py @@ -51,15 +51,13 @@ from . import init_integration async def update_ac_state( hass: HomeAssistant, entity_id: str, - mock_aircon_api_instances: MagicMock, - mock_instance_idx: int, + mock_aircon_api_instance: MagicMock, ): """Simulate an update trigger from the API.""" - update_ha_state_cb = mock_aircon_api_instances.call_args_list[ - mock_instance_idx - ].args[3] - update_ha_state_cb() - await hass.async_block_till_done() + for call in mock_aircon_api_instance.register_attr_callback.call_args_list: + update_ha_state_cb = call[0][0] + update_ha_state_cb() + await hass.async_block_till_done() return hass.states.get(entity_id) @@ -72,7 +70,11 @@ async def test_no_appliances( assert len(hass.states.async_all()) == 0 -async def test_static_attributes(hass: HomeAssistant, mock_aircon1_api: MagicMock): +async def test_static_attributes( + hass: HomeAssistant, + mock_aircon1_api: MagicMock, + mock_aircon_api_instances: MagicMock, +): """Test static climate attributes.""" await init_integration(hass) @@ -137,81 +139,56 @@ async def test_dynamic_attributes( ): entity_id = clim_test_instance.entity_id mock_instance = clim_test_instance.mock_instance - mock_instance_idx = clim_test_instance.mock_instance_idx state = hass.states.get(entity_id) assert state is not None assert state.state == HVACMode.COOL mock_instance.get_power_on.return_value = False - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.state == HVACMode.OFF mock_instance.get_online.return_value = False - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.state == STATE_UNAVAILABLE mock_instance.get_power_on.return_value = True mock_instance.get_online.return_value = True - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.state == HVACMode.COOL mock_instance.get_mode.return_value = whirlpool.aircon.Mode.Heat - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.state == HVACMode.HEAT mock_instance.get_mode.return_value = whirlpool.aircon.Mode.Fan - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.state == HVACMode.FAN_ONLY mock_instance.get_fanspeed.return_value = whirlpool.aircon.FanSpeed.Auto - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.attributes[ATTR_FAN_MODE] == HVACMode.AUTO mock_instance.get_fanspeed.return_value = whirlpool.aircon.FanSpeed.Low - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.attributes[ATTR_FAN_MODE] == FAN_LOW mock_instance.get_fanspeed.return_value = whirlpool.aircon.FanSpeed.Medium - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.attributes[ATTR_FAN_MODE] == FAN_MEDIUM mock_instance.get_fanspeed.return_value = whirlpool.aircon.FanSpeed.High - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.attributes[ATTR_FAN_MODE] == FAN_HIGH mock_instance.get_fanspeed.return_value = whirlpool.aircon.FanSpeed.Off - state = await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) + state = await update_ac_state(hass, entity_id, mock_instance) assert state.attributes[ATTR_FAN_MODE] == FAN_OFF mock_instance.get_current_temp.return_value = 15 mock_instance.get_temp.return_value = 20 mock_instance.get_current_humidity.return_value = 80 mock_instance.get_h_louver_swing.return_value = True - attributes = ( - await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) - ).attributes + attributes = (await update_ac_state(hass, entity_id, mock_instance)).attributes assert attributes[ATTR_CURRENT_TEMPERATURE] == 15 assert attributes[ATTR_TEMPERATURE] == 20 assert attributes[ATTR_CURRENT_HUMIDITY] == 80 @@ -221,11 +198,7 @@ async def test_dynamic_attributes( mock_instance.get_temp.return_value = 21 mock_instance.get_current_humidity.return_value = 70 mock_instance.get_h_louver_swing.return_value = False - attributes = ( - await update_ac_state( - hass, entity_id, mock_aircon_api_instances, mock_instance_idx - ) - ).attributes + attributes = (await update_ac_state(hass, entity_id, mock_instance)).attributes assert attributes[ATTR_CURRENT_TEMPERATURE] == 16 assert attributes[ATTR_TEMPERATURE] == 21 assert attributes[ATTR_CURRENT_HUMIDITY] == 70 @@ -233,7 +206,10 @@ async def test_dynamic_attributes( async def test_service_calls( - hass: HomeAssistant, mock_aircon1_api: MagicMock, mock_aircon2_api: MagicMock + hass: HomeAssistant, + mock_aircon_api_instances: MagicMock, + mock_aircon1_api: MagicMock, + mock_aircon2_api: MagicMock, ): """Test controlling the entity through service calls.""" await init_integration(hass) diff --git a/tests/components/whirlpool/test_config_flow.py b/tests/components/whirlpool/test_config_flow.py index 6e188b68219..ad6620dc057 100644 --- a/tests/components/whirlpool/test_config_flow.py +++ b/tests/components/whirlpool/test_config_flow.py @@ -13,8 +13,16 @@ from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry +CONFIG_INPUT = { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", +} -async def test_form(hass): + +async def test_form( + hass: HomeAssistant, + region, +) -> None: """Test we get the form.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -27,15 +35,20 @@ async def test_form(hass): "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", return_value=True, ), patch( + "homeassistant.components.whirlpool.config_flow.BackendSelector" + ) as mock_backend_selector, patch( "homeassistant.components.whirlpool.async_setup_entry", return_value=True, - ) as mock_setup_entry: + ) as mock_setup_entry, patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.aircons", + return_value=["test"], + ), patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", + return_value=True, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - "username": "test-username", - "password": "test-password", - }, + CONFIG_INPUT | {"region": region[0]}, ) await hass.async_block_till_done() @@ -44,11 +57,13 @@ async def test_form(hass): assert result2["data"] == { "username": "test-username", "password": "test-password", + "region": region[0], } assert len(mock_setup_entry.mock_calls) == 1 + mock_backend_selector.assert_called_once_with(region[2], region[1]) -async def test_form_invalid_auth(hass): +async def test_form_invalid_auth(hass: HomeAssistant, region) -> None: """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -59,16 +74,16 @@ async def test_form_invalid_auth(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", + CONFIG_INPUT + | { + "region": region[0], }, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "invalid_auth"} -async def test_form_cannot_connect(hass): +async def test_form_cannot_connect(hass: HomeAssistant, region) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -79,16 +94,16 @@ async def test_form_cannot_connect(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", + CONFIG_INPUT + | { + "region": region[0], }, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} -async def test_form_auth_timeout(hass): +async def test_form_auth_timeout(hass: HomeAssistant, region) -> None: """Test we handle auth timeout error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -99,16 +114,16 @@ async def test_form_auth_timeout(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", + CONFIG_INPUT + | { + "region": region[0], }, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} -async def test_form_generic_auth_exception(hass): +async def test_form_generic_auth_exception(hass: HomeAssistant, region) -> None: """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -119,20 +134,20 @@ async def test_form_generic_auth_exception(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", + CONFIG_INPUT + | { + "region": region[0], }, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "unknown"} -async def test_form_already_configured(hass): +async def test_form_already_configured(hass: HomeAssistant, region) -> None: """Test we handle cannot connect error.""" mock_entry = MockConfigEntry( domain=DOMAIN, - data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + data=CONFIG_INPUT | {"region": region[0]}, unique_id="test-username", ) mock_entry.add_to_hass(hass) @@ -147,12 +162,18 @@ async def test_form_already_configured(hass): with patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), patch( "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", return_value=True, + ), patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.aircons", + return_value=["test"], + ), patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", + return_value=True, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", + CONFIG_INPUT + | { + "region": region[0], }, ) await hass.async_block_till_done() @@ -161,12 +182,38 @@ async def test_form_already_configured(hass): assert result2["reason"] == "already_configured" -async def test_reauth_flow(hass: HomeAssistant) -> None: - """Test a successful reauth flow.""" +async def test_no_appliances_flow(hass: HomeAssistant, region) -> None: + """Test we get and error with no appliances.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == config_entries.SOURCE_USER + + with patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), patch( + "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", + return_value=True, + ), patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", + return_value=True, + ): + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG_INPUT | {"region": region[0]}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "no_appliances"} + + +async def test_reauth_flow(hass: HomeAssistant, region) -> None: + """Test a successful reauth flow.""" mock_entry = MockConfigEntry( domain=DOMAIN, - data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + data=CONFIG_INPUT | {"region": region[0]}, unique_id="test-username", ) mock_entry.add_to_hass(hass) @@ -178,7 +225,7 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: "unique_id": mock_entry.unique_id, "entry_id": mock_entry.entry_id, }, - data={"username": "test-username", "password": "new-password"}, + data=CONFIG_INPUT | {"region": region[0]}, ) assert result["step_id"] == "reauth_confirm" @@ -191,6 +238,12 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: ), patch("homeassistant.components.whirlpool.config_flow.Auth.do_auth"), patch( "homeassistant.components.whirlpool.config_flow.Auth.is_access_token_valid", return_value=True, + ), patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.aircons", + return_value=["test"], + ), patch( + "homeassistant.components.whirlpool.config_flow.AppliancesManager.fetch_appliances", + return_value=True, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -203,15 +256,16 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: assert mock_entry.data == { CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password", + "region": region[0], } -async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None: +async def test_reauth_flow_auth_error(hass: HomeAssistant, region) -> None: """Test an authorization error reauth flow.""" mock_entry = MockConfigEntry( domain=DOMAIN, - data={"username": "test-username", "password": "test-password"}, + data=CONFIG_INPUT | {"region": region[0]}, unique_id="test-username", ) mock_entry.add_to_hass(hass) @@ -223,7 +277,11 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None: "unique_id": mock_entry.unique_id, "entry_id": mock_entry.entry_id, }, - data={"username": "test-username", "password": "new-password"}, + data={ + CONF_USERNAME: "test-username", + CONF_PASSWORD: "new-password", + "region": region[0], + }, ) assert result["step_id"] == "reauth_confirm" @@ -246,12 +304,12 @@ async def test_reauth_flow_auth_error(hass: HomeAssistant) -> None: assert result2["errors"] == {"base": "invalid_auth"} -async def test_reauth_flow_connnection_error(hass: HomeAssistant) -> None: +async def test_reauth_flow_connnection_error(hass: HomeAssistant, region) -> None: """Test a connection error reauth flow.""" mock_entry = MockConfigEntry( domain=DOMAIN, - data={"username": "test-username", "password": "test-password"}, + data=CONFIG_INPUT | {"region": region[0]}, unique_id="test-username", ) mock_entry.add_to_hass(hass) @@ -263,7 +321,7 @@ async def test_reauth_flow_connnection_error(hass: HomeAssistant) -> None: "unique_id": mock_entry.unique_id, "entry_id": mock_entry.entry_id, }, - data={CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password"}, + data=CONFIG_INPUT | {"region": region[0]}, ) assert result["step_id"] == "reauth_confirm" diff --git a/tests/components/whirlpool/test_init.py b/tests/components/whirlpool/test_init.py index 619c2c783b7..233b8247840 100644 --- a/tests/components/whirlpool/test_init.py +++ b/tests/components/whirlpool/test_init.py @@ -2,22 +2,59 @@ from unittest.mock import AsyncMock, MagicMock import aiohttp +from whirlpool.backendselector import Brand, Region from homeassistant.components.whirlpool.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from . import init_integration +from . import init_integration, init_integration_with_entry + +from tests.common import MockConfigEntry -async def test_setup(hass: HomeAssistant): +async def test_setup( + hass: HomeAssistant, + mock_backend_selector_api: MagicMock, + region, + mock_aircon_api_instances: MagicMock, +): """Test setup.""" - entry = await init_integration(hass) + entry = await init_integration(hass, region[0]) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state is ConfigEntryState.LOADED + mock_backend_selector_api.assert_called_once_with(region[2], region[1]) -async def test_setup_http_exception(hass: HomeAssistant, mock_auth_api: MagicMock): +async def test_setup_region_fallback( + hass: HomeAssistant, + mock_backend_selector_api: MagicMock, + mock_aircon_api_instances: MagicMock, +): + """Test setup when no region is available on the ConfigEntry. + + This can happen after a version update, since there was no region in the first versions. + """ + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "nobody", + CONF_PASSWORD: "qwerty", + }, + ) + entry = await init_integration_with_entry(hass, entry) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state is ConfigEntryState.LOADED + mock_backend_selector_api.assert_called_once_with(Brand.Whirlpool, Region.EU) + + +async def test_setup_http_exception( + hass: HomeAssistant, + mock_auth_api: MagicMock, + mock_aircon_api_instances: MagicMock, +): """Test setup with an http exception.""" mock_auth_api.return_value.do_auth = AsyncMock( side_effect=aiohttp.ClientConnectionError() @@ -27,7 +64,11 @@ async def test_setup_http_exception(hass: HomeAssistant, mock_auth_api: MagicMoc assert entry.state is ConfigEntryState.SETUP_RETRY -async def test_setup_auth_failed(hass: HomeAssistant, mock_auth_api: MagicMock): +async def test_setup_auth_failed( + hass: HomeAssistant, + mock_auth_api: MagicMock, + mock_aircon_api_instances: MagicMock, +): """Test setup with failed auth.""" mock_auth_api.return_value.do_auth = AsyncMock() mock_auth_api.return_value.is_access_token_valid.return_value = False @@ -37,7 +78,9 @@ async def test_setup_auth_failed(hass: HomeAssistant, mock_auth_api: MagicMock): async def test_setup_fetch_appliances_failed( - hass: HomeAssistant, mock_appliances_manager_api: MagicMock + hass: HomeAssistant, + mock_appliances_manager_api: MagicMock, + mock_aircon_api_instances: MagicMock, ): """Test setup with failed fetch_appliances.""" mock_appliances_manager_api.return_value.fetch_appliances.return_value = False @@ -46,7 +89,11 @@ async def test_setup_fetch_appliances_failed( assert entry.state is ConfigEntryState.SETUP_ERROR -async def test_unload_entry(hass: HomeAssistant): +async def test_unload_entry( + hass: HomeAssistant, + mock_aircon_api_instances: MagicMock, + mock_sensor_api_instances: MagicMock, +): """Test successful unload of entry.""" entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/whirlpool/test_sensor.py b/tests/components/whirlpool/test_sensor.py new file mode 100644 index 00000000000..b8801bd4fd5 --- /dev/null +++ b/tests/components/whirlpool/test_sensor.py @@ -0,0 +1,343 @@ +"""Test the Whirlpool Sensor domain.""" +from datetime import datetime, timezone +from unittest.mock import MagicMock + +from whirlpool.washerdryer import MachineState + +from homeassistant.core import CoreState, HomeAssistant, State +from homeassistant.helpers import entity_registry +from homeassistant.util.dt import as_timestamp, utc_from_timestamp + +from . import init_integration + +from tests.common import mock_restore_cache_with_extra_data + + +async def update_sensor_state( + hass: HomeAssistant, + entity_id: str, + mock_sensor_api_instance: MagicMock, +): + """Simulate an update trigger from the API.""" + + for call in mock_sensor_api_instance.register_attr_callback.call_args_list: + update_ha_state_cb = call[0][0] + update_ha_state_cb() + await hass.async_block_till_done() + + return hass.states.get(entity_id) + + +def side_effect_function_open_door(*args, **kwargs): + """Return correct value for attribute.""" + if args[0] == "Cavity_TimeStatusEstTimeRemaining": + return 3540 + + if args[0] == "Cavity_OpStatusDoorOpen": + return "1" + + if args[0] == "WashCavity_OpStatusBulkDispense1Level": + return "3" + + +async def test_dryer_sensor_values( + hass: HomeAssistant, + mock_sensor_api_instances: MagicMock, + mock_sensor2_api: MagicMock, +): + """Test the sensor value callbacks.""" + hass.state = CoreState.not_running + thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) + mock_restore_cache_with_extra_data( + hass, + ( + ( + State( + "sensor.washer_end_time", + "1", + ), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ( + State("sensor.dryer_end_time", "1"), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ), + ) + + await init_integration(hass) + + entity_id = "sensor.dryer_state" + mock_instance = mock_sensor2_api + registry = entity_registry.async_get(hass) + entry = registry.async_get(entity_id) + assert entry + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "standby" + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + state_id = f"{entity_id.split('_')[0]}_end_time" + state = hass.states.get(state_id) + assert state.state == thetimestamp.isoformat() + + mock_instance.get_machine_state.return_value = MachineState.RunningMainCycle + mock_instance.get_cycle_status_filling.return_value = False + mock_instance.attr_value_to_bool.side_effect = [ + False, + False, + False, + False, + False, + False, + ] + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + assert state.state == "running_maincycle" + + mock_instance.get_machine_state.return_value = MachineState.Complete + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + assert state.state == "complete" + + +async def test_washer_sensor_values( + hass: HomeAssistant, + mock_sensor_api_instances: MagicMock, + mock_sensor1_api: MagicMock, +): + """Test the sensor value callbacks.""" + hass.state = CoreState.not_running + thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) + mock_restore_cache_with_extra_data( + hass, + ( + ( + State( + "sensor.washer_end_time", + "1", + ), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ( + State("sensor.dryer_end_time", "1"), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ), + ) + + await init_integration(hass) + + entity_id = "sensor.washer_state" + mock_instance = mock_sensor1_api + registry = entity_registry.async_get(hass) + entry = registry.async_get(entity_id) + assert entry + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "standby" + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + state_id = f"{entity_id.split('_')[0]}_end_time" + state = hass.states.get(state_id) + assert state.state == thetimestamp.isoformat() + + state_id = f"{entity_id.split('_')[0]}_detergent_level" + state = hass.states.get(state_id) + assert state is not None + assert state.state == "50" + + # Test the washer cycle states + mock_instance.get_machine_state.return_value = MachineState.RunningMainCycle + mock_instance.get_cycle_status_filling.return_value = True + mock_instance.attr_value_to_bool.side_effect = [ + True, + False, + False, + False, + False, + False, + ] + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + assert state.state == "cycle_filling" + + mock_instance.get_cycle_status_filling.return_value = False + mock_instance.get_cycle_status_rinsing.return_value = True + mock_instance.attr_value_to_bool.side_effect = [ + False, + True, + False, + False, + False, + False, + ] + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + assert state.state == "cycle_rinsing" + + mock_instance.get_cycle_status_rinsing.return_value = False + mock_instance.get_cycle_status_sensing.return_value = True + mock_instance.attr_value_to_bool.side_effect = [ + False, + False, + True, + False, + False, + False, + ] + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + assert state.state == "cycle_sensing" + + mock_instance.get_cycle_status_sensing.return_value = False + mock_instance.get_cycle_status_soaking.return_value = True + mock_instance.attr_value_to_bool.side_effect = [ + False, + False, + False, + True, + False, + False, + ] + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + assert state.state == "cycle_soaking" + + mock_instance.get_cycle_status_soaking.return_value = False + mock_instance.get_cycle_status_spinning.return_value = True + mock_instance.attr_value_to_bool.side_effect = [ + False, + False, + False, + False, + True, + False, + ] + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + assert state.state == "cycle_spinning" + + mock_instance.get_cycle_status_spinning.return_value = False + mock_instance.get_cycle_status_washing.return_value = True + mock_instance.attr_value_to_bool.side_effect = [ + False, + False, + False, + False, + False, + True, + ] + + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + assert state.state == "cycle_washing" + + mock_instance.get_machine_state.return_value = MachineState.Complete + mock_instance.attr_value_to_bool.side_effect = None + mock_instance.get_attribute.side_effect = side_effect_function_open_door + state = await update_sensor_state(hass, entity_id, mock_instance) + assert state is not None + assert state.state == "door_open" + + +async def test_restore_state( + hass: HomeAssistant, + mock_sensor_api_instances: MagicMock, +): + """Test sensor restore state.""" + # Home assistant is not running yet + hass.state = CoreState.not_running + thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) + mock_restore_cache_with_extra_data( + hass, + ( + ( + State( + "sensor.washer_end_time", + "1", + ), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ( + State("sensor.dryer_end_time", "1"), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ), + ) + + # create and add entry + await init_integration(hass) + # restore from cache + state = hass.states.get("sensor.washer_end_time") + assert state.state == thetimestamp.isoformat() + state = hass.states.get("sensor.dryer_end_time") + assert state.state == thetimestamp.isoformat() + + +async def test_callback( + hass: HomeAssistant, + mock_sensor_api_instances: MagicMock, + mock_sensor1_api: MagicMock, +): + """Test callback timestamp callback function.""" + hass.state = CoreState.not_running + thetimestamp: datetime = datetime(2022, 11, 29, 00, 00, 00, 00, timezone.utc) + mock_restore_cache_with_extra_data( + hass, + ( + ( + State( + "sensor.washer_end_time", + "1", + ), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ( + State("sensor.dryer_end_time", "1"), + {"native_value": thetimestamp, "native_unit_of_measurement": None}, + ), + ), + ) + + # create and add entry + await init_integration(hass) + # restore from cache + state = hass.states.get("sensor.washer_end_time") + assert state.state == thetimestamp.isoformat() + callback = mock_sensor1_api.register_attr_callback.call_args_list[2][0][0] + callback() + # await hass.async_block_till_done() + state = hass.states.get("sensor.washer_end_time") + assert state.state == thetimestamp.isoformat() + mock_sensor1_api.get_machine_state.return_value = MachineState.RunningMainCycle + mock_sensor1_api.get_attribute.side_effect = None + mock_sensor1_api.get_attribute.return_value = "60" + callback() + + # Test new timestamp when machine starts a cycle. + state = hass.states.get("sensor.washer_end_time") + time = state.state + assert state.state != thetimestamp.isoformat() + + # Test no timestamp change for < 60 seconds time change. + mock_sensor1_api.get_attribute.return_value = "65" + callback() + state = hass.states.get("sensor.washer_end_time") + assert state.state == time + + # Test timestamp change for > 60 seconds. + mock_sensor1_api.get_attribute.return_value = "120" + callback() + state = hass.states.get("sensor.washer_end_time") + newtime = utc_from_timestamp(as_timestamp(time) + 60) + assert state.state == newtime.isoformat() diff --git a/tests/components/whois/test_config_flow.py b/tests/components/whois/test_config_flow.py index 7250a9d1567..beda26c42d1 100644 --- a/tests/components/whois/test_config_flow.py +++ b/tests/components/whois/test_config_flow.py @@ -30,7 +30,6 @@ async def test_full_user_flow( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -71,7 +70,6 @@ async def test_full_flow_with_error( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_whois_config_flow.side_effect = throw result2 = await hass.config_entries.flow.async_configure( @@ -82,7 +80,6 @@ async def test_full_flow_with_error( assert result2.get("type") == FlowResultType.FORM assert result2.get("step_id") == SOURCE_USER assert result2.get("errors") == {"base": reason} - assert "flow_id" in result2 assert len(mock_setup_entry.mock_calls) == 0 assert len(mock_whois_config_flow.mock_calls) == 1 diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index 96a35d10c40..afde266e8b9 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -198,7 +198,7 @@ class ComponentFactory: const.DOMAIN, context={"source": SOURCE_USER} ) assert result - # pylint: disable=protected-access + state = config_entry_oauth2_flow._encode_jwt( self._hass, { diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index 3917c894b60..8a0bf88f6e9 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -163,7 +163,6 @@ async def test_data_manager_webhook_subscription( WebhookConfig(id="1234", url="http://localhost/api/webhook/1234", enabled=True), ) - # pylint: disable=protected-access data_manager._notify_subscribe_delay = datetime.timedelta(seconds=0) data_manager._notify_unsubscribe_delay = datetime.timedelta(seconds=0) diff --git a/tests/components/withings/test_config_flow.py b/tests/components/withings/test_config_flow.py index 9fcc84dbe83..380f3a79af8 100644 --- a/tests/components/withings/test_config_flow.py +++ b/tests/components/withings/test_config_flow.py @@ -80,7 +80,6 @@ async def test_config_reauth_profile( {}, ) - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/wiz/__init__.py b/tests/components/wiz/__init__.py index 93033d984fa..0f88a1db7b5 100644 --- a/tests/components/wiz/__init__.py +++ b/tests/components/wiz/__init__.py @@ -1,9 +1,9 @@ """Tests for the WiZ Platform integration.""" +from collections.abc import Callable 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 diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index 600bf0eb0d2..0ba8e65942a 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -24,7 +24,6 @@ async def test_full_user_flow_implementation( assert result.get("step_id") == "user" assert result.get("type") == FlowResultType.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"} @@ -65,7 +64,6 @@ async def test_full_zeroconf_flow_implementation( assert result.get("description_placeholders") == {CONF_NAME: "WLED RGB Light"} assert result.get("step_id") == "zeroconf_confirm" assert result.get("type") == FlowResultType.FORM - assert "flow_id" in result result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -271,7 +269,6 @@ async def test_options_flow( assert result.get("type") == FlowResultType.FORM assert result.get("step_id") == "init" - assert "flow_id" in result result2 = await hass.config_entries.options.async_configure( result["flow_id"], diff --git a/tests/components/wled/test_diagnostics.py b/tests/components/wled/test_diagnostics.py index 8f086331f8f..3588ecdb498 100644 --- a/tests/components/wled/test_diagnostics.py +++ b/tests/components/wled/test_diagnostics.py @@ -50,7 +50,10 @@ async def test_diagnostics( "brightness": 127, "nightlight": { "__type": "", - "repr": "Nightlight(duration=60, fade=True, on=False, mode=, target_brightness=0)", + "repr": ( + "Nightlight(duration=60, fade=True, on=False," + " mode=, target_brightness=0)" + ), }, "on": True, "playlist": -1, @@ -58,11 +61,31 @@ async def test_diagnostics( "segments": [ { "__type": "", - "repr": "Segment(brightness=127, clones=-1, color_primary=(255, 159, 0), color_secondary=(0, 0, 0), color_tertiary=(0, 0, 0), effect=Effect(effect_id=0, name='Solid'), intensity=128, length=20, on=True, palette=Palette(name='Default', palette_id=0), reverse=False, segment_id=0, selected=True, speed=32, start=0, stop=19)", + "repr": ( + "Segment(brightness=127, clones=-1," + " color_primary=(255, 159, 0)," + " color_secondary=(0, 0, 0)," + " color_tertiary=(0, 0, 0)," + " effect=Effect(effect_id=0, name='Solid')," + " intensity=128, length=20, on=True," + " palette=Palette(name='Default', palette_id=0)," + " reverse=False, segment_id=0, selected=True," + " speed=32, start=0, stop=19)" + ), }, { "__type": "", - "repr": "Segment(brightness=127, clones=-1, color_primary=(0, 255, 123), color_secondary=(0, 0, 0), color_tertiary=(0, 0, 0), effect=Effect(effect_id=1, name='Blink'), intensity=64, length=10, on=True, palette=Palette(name='Random Cycle', palette_id=1), reverse=True, segment_id=1, selected=True, speed=16, start=20, stop=30)", + "repr": ( + "Segment(brightness=127, clones=-1," + " color_primary=(0, 255, 123)," + " color_secondary=(0, 0, 0)," + " color_tertiary=(0, 0, 0)," + " effect=Effect(effect_id=1, name='Blink')," + " intensity=64, length=10, on=True," + " palette=Palette(name='Random Cycle', palette_id=1)," + " reverse=True, segment_id=1, selected=True," + " speed=16, start=20, stop=30)" + ), }, ], "sync": { diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index 4d9632ff5b8..bb8a3ed94a7 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -10,11 +10,11 @@ from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, - DATA_BYTES, - ELECTRIC_CURRENT_MILLIAMPERE, PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT, STATE_UNKNOWN, + UnitOfElectricCurrent, + UnitOfInformation, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -42,7 +42,8 @@ async def test_sensors( state = hass.states.get("sensor.wled_rgb_light_estimated_current") assert state assert ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ELECTRIC_CURRENT_MILLIAMPERE + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == UnitOfElectricCurrent.MILLIAMPERE ) assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.CURRENT assert state.state == "470" @@ -66,7 +67,7 @@ async def test_sensors( state = hass.states.get("sensor.wled_rgb_light_free_memory") assert state assert state.attributes.get(ATTR_ICON) == "mdi:memory" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == DATA_BYTES + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfInformation.BYTES assert state.state == "14600" assert entry.entity_category is EntityCategory.DIAGNOSTIC diff --git a/tests/components/xiaomi_ble/test_binary_sensor.py b/tests/components/xiaomi_ble/test_binary_sensor.py index eb369f20268..5389a2987f2 100644 --- a/tests/components/xiaomi_ble/test_binary_sensor.py +++ b/tests/components/xiaomi_ble/test_binary_sensor.py @@ -9,12 +9,12 @@ from tests.common import MockConfigEntry from tests.components.bluetooth import inject_bluetooth_service_info_bleak -async def test_smoke_sensor(hass): - """Test setting up a smoke sensor.""" +async def test_door_problem_sensors(hass): + """Test setting up a door binary sensor with additional problem sensors.""" entry = MockConfigEntry( domain=DOMAIN, - unique_id="54:EF:44:E3:9C:BC", - data={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + unique_id="EE:89:73:44:BE:98", + data={"bindkey": "2c3795afa33019a8afdc17ba99e6f217"}, ) entry.add_to_hass(hass) @@ -25,24 +25,72 @@ async def test_smoke_sensor(hass): inject_bluetooth_service_info_bleak( hass, make_advertisement( - "54:EF:44:E3:9C:BC", - b"XY\x97\tf\xbc\x9c\xe3D\xefT\x01" b"\x08\x12\x05\x00\x00\x00q^\xbe\x90", + "EE:89:73:44:BE:98", + b"HU9\x0e3\x9cq\xc0$\x1f\xff\xee\x80S\x00\x00\x02\xb4\xc59", ), ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 3 - smoke_sensor = hass.states.get("binary_sensor.thermometer_9cbc_smoke") - smoke_sensor_attribtes = smoke_sensor.attributes - assert smoke_sensor.state == "on" - assert smoke_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Thermometer 9CBC Smoke" + door_sensor = hass.states.get("binary_sensor.door_lock_be98_door") + door_sensor_attribtes = door_sensor.attributes + assert door_sensor.state == "off" + assert door_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Door" + + door_left_open = hass.states.get("binary_sensor.door_lock_be98_door_left_open") + door_left_open_attribtes = door_left_open.attributes + assert door_left_open.state == "off" + assert ( + door_left_open_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Door left open" + ) + + pry_the_door = hass.states.get("binary_sensor.door_lock_be98_pry_the_door") + pry_the_door_attribtes = pry_the_door.attributes + assert pry_the_door.state == "off" + assert pry_the_door_attribtes[ATTR_FRIENDLY_NAME] == "Door Lock BE98 Pry the door" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_light_motion(hass): + """Test setting up a light and motion binary sensor.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="58:2D:34:35:93:21", + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + "58:2D:34:35:93:21", + b"P \xf6\x07\xda!\x9354-X\x0f\x00\x03\x01\x00\x00", + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + + motion_sensor = hass.states.get("binary_sensor.nightlight_9321_motion") + motion_sensor_attribtes = motion_sensor.attributes + assert motion_sensor.state == "on" + assert motion_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Nightlight 9321 Motion" + + light_sensor = hass.states.get("binary_sensor.nightlight_9321_light") + light_sensor_attribtes = light_sensor.attributes + assert light_sensor.state == "off" + assert light_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Nightlight 9321 Light" assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() async def test_moisture(hass): - """Make sure that formldehyde sensors are correctly mapped.""" + """Test setting up a moisture binary sensor.""" entry = MockConfigEntry( domain=DOMAIN, unique_id="C4:7C:8D:6A:3E:7A", @@ -73,3 +121,125 @@ async def test_moisture(hass): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() + + +async def test_opening(hass): + """Test setting up a opening binary sensor.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="A4:C1:38:66:E5:67", + data={"bindkey": "0fdcc30fe9289254876b5ef7c11ef1f0"}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + "A4:C1:38:66:E5:67", + b"XY\x89\x18\x9ag\xe5f8\xc1\xa4\x9d\xd9z\xf3&\x00\x00\xc8\xa6\x0b\xd5", + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") + opening_sensor_attribtes = opening_sensor.attributes + assert opening_sensor.state == "on" + assert ( + opening_sensor_attribtes[ATTR_FRIENDLY_NAME] + == "Door/Window Sensor E567 Opening" + ) + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_opening_problem_sensors(hass): + """Test setting up a opening binary sensor with additional problem sensors.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="A4:C1:38:66:E5:67", + data={"bindkey": "0fdcc30fe9289254876b5ef7c11ef1f0"}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + "A4:C1:38:66:E5:67", + b"XY\x89\x18ug\xe5f8\xc1\xa4i\xdd\xf3\xa1&\x00\x00\xa2J\x1bE", + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 3 + + opening_sensor = hass.states.get("binary_sensor.door_window_sensor_e567_opening") + opening_sensor_attribtes = opening_sensor.attributes + assert opening_sensor.state == "off" + assert ( + opening_sensor_attribtes[ATTR_FRIENDLY_NAME] + == "Door/Window Sensor E567 Opening" + ) + + door_left_open = hass.states.get( + "binary_sensor.door_window_sensor_e567_door_left_open" + ) + door_left_open_attribtes = door_left_open.attributes + assert door_left_open.state == "off" + assert ( + door_left_open_attribtes[ATTR_FRIENDLY_NAME] + == "Door/Window Sensor E567 Door left open" + ) + + device_forcibly_removed = hass.states.get( + "binary_sensor.door_window_sensor_e567_device_forcibly_removed" + ) + device_forcibly_removed_attribtes = device_forcibly_removed.attributes + assert device_forcibly_removed.state == "off" + assert ( + device_forcibly_removed_attribtes[ATTR_FRIENDLY_NAME] + == "Door/Window Sensor E567 Device forcibly removed" + ) + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_smoke(hass): + """Test setting up a smoke binary sensor.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="54:EF:44:E3:9C:BC", + data={"bindkey": "5b51a7c91cde6707c9ef18dfda143a58"}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0 + inject_bluetooth_service_info_bleak( + hass, + make_advertisement( + "54:EF:44:E3:9C:BC", + b"XY\x97\tf\xbc\x9c\xe3D\xefT\x01" b"\x08\x12\x05\x00\x00\x00q^\xbe\x90", + ), + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 1 + + smoke_sensor = hass.states.get("binary_sensor.thermometer_9cbc_smoke") + smoke_sensor_attribtes = smoke_sensor.attributes + assert smoke_sensor.state == "on" + assert smoke_sensor_attribtes[ATTR_FRIENDLY_NAME] == "Thermometer 9CBC Smoke" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/xiaomi_ble/test_device_trigger.py b/tests/components/xiaomi_ble/test_device_trigger.py new file mode 100644 index 00000000000..7706b80dfe1 --- /dev/null +++ b/tests/components/xiaomi_ble/test_device_trigger.py @@ -0,0 +1,383 @@ +"""Test Xiaomi BLE events.""" +import pytest + +from homeassistant.components import automation +from homeassistant.components.bluetooth.const import DOMAIN as BLUETOOTH_DOMAIN +from homeassistant.components.device_automation import DeviceAutomationType +from homeassistant.components.xiaomi_ble.const import ( + CONF_EVENT_PROPERTIES, + DOMAIN, + EVENT_PROPERTIES, + EVENT_TYPE, + XIAOMI_BLE_EVENT, +) +from homeassistant.const import ( + CONF_ADDRESS, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.core import callback +from homeassistant.helpers import device_registry +from homeassistant.helpers.device_registry import async_get as async_get_dev_reg +from homeassistant.setup import async_setup_component + +from . import make_advertisement + +from tests.common import ( + MockConfigEntry, + async_capture_events, + async_get_device_automations, + async_mock_service, +) +from tests.components.bluetooth import inject_bluetooth_service_info_bleak + + +@callback +def get_device_id(mac: str) -> tuple[str, str]: + """Get device registry identifier for xiaomi_ble.""" + return (BLUETOOTH_DOMAIN, mac) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def _async_setup_xiaomi_device(hass, mac: str): + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=mac, + ) + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return config_entry + + +async def test_event_motion_detected(hass): + """Make sure that a motion detected event is fired.""" + mac = "DE:70:E8:B2:39:0C" + entry = await _async_setup_xiaomi_device(hass, mac) + events = async_capture_events(hass, "xiaomi_ble_event") + + # Emit motion detected event + inject_bluetooth_service_info_bleak( + hass, + make_advertisement(mac, b"@0\xdd\x03$\x03\x00\x01\x01"), + ) + + # wait for the event + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data["address"] == "DE:70:E8:B2:39:0C" + assert events[0].data["event_type"] == "motion_detected" + assert events[0].data["event_properties"] is None + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_get_triggers(hass): + """Test that we get the expected triggers from a Xiaomi BLE motion sensor.""" + mac = "DE:70:E8:B2:39:0C" + entry = await _async_setup_xiaomi_device(hass, mac) + events = async_capture_events(hass, "xiaomi_ble_event") + + # Emit motion detected event so it creates the device in the registry + inject_bluetooth_service_info_bleak( + hass, + make_advertisement(mac, b"@0\xdd\x03$\x03\x00\x01\x01"), + ) + + # wait for the event + await hass.async_block_till_done() + assert len(events) == 1 + + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get_device({get_device_id(mac)}) + assert device + expected_trigger = { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device.id, + CONF_TYPE: "motion_detected", + CONF_EVENT_PROPERTIES: None, + "metadata": {}, + } + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, device.id + ) + assert expected_trigger in triggers + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_get_triggers_for_invalid_xiami_ble_device(hass): + """Test that we don't get triggers for an invalid device.""" + mac = "DE:70:E8:B2:39:0C" + entry = await _async_setup_xiaomi_device(hass, mac) + events = async_capture_events(hass, "xiaomi_ble_event") + + # Emit motion detected event so it creates the device in the registry + inject_bluetooth_service_info_bleak( + hass, + make_advertisement(mac, b"@0\xdd\x03$\x03\x00\x01\x01"), + ) + + # wait for the event + await hass.async_block_till_done() + assert len(events) == 1 + + dev_reg = async_get_dev_reg(hass) + invalid_device = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, "invdevmac")}, + ) + + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, invalid_device.id + ) + assert triggers == [] + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_get_triggers_for_invalid_device_id(hass): + """Test that we don't get triggers when using an invalid device_id.""" + mac = "DE:70:E8:B2:39:0C" + entry = await _async_setup_xiaomi_device(hass, mac) + + # Emit motion detected event so it creates the device in the registry + inject_bluetooth_service_info_bleak( + hass, + make_advertisement(mac, b"@0\xdd\x03$\x03\x00\x01\x01"), + ) + + # wait for the event + await hass.async_block_till_done() + + dev_reg = async_get_dev_reg(hass) + + invalid_device = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + assert invalid_device + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, invalid_device.id + ) + assert triggers == [] + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_if_fires_on_motion_detected(hass, calls): + """Test for motion event trigger firing.""" + mac = "DE:70:E8:B2:39:0C" + entry = await _async_setup_xiaomi_device(hass, mac) + + # Emit motion detected event so it creates the device in the registry + inject_bluetooth_service_info_bleak( + hass, + make_advertisement(mac, b"@0\xdd\x03$\x03\x00\x01\x01"), + ) + + # wait for the event + await hass.async_block_till_done() + + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get_device({get_device_id(mac)}) + device_id = device.id + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device_id, + CONF_TYPE: "motion_detected", + CONF_EVENT_PROPERTIES: None, + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_motion_detected"}, + }, + }, + ] + }, + ) + + message = { + CONF_DEVICE_ID: device_id, + CONF_ADDRESS: "DE:70:E8:B2:39:0C", + EVENT_TYPE: "motion_detected", + EVENT_PROPERTIES: None, + } + + hass.bus.async_fire(XIAOMI_BLE_EVENT, message) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["some"] == "test_trigger_motion_detected" + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_automation_with_invalid_trigger_type(hass, caplog): + """Test for automation with invalid trigger type.""" + mac = "DE:70:E8:B2:39:0C" + entry = await _async_setup_xiaomi_device(hass, mac) + + # Emit motion detected event so it creates the device in the registry + inject_bluetooth_service_info_bleak( + hass, + make_advertisement(mac, b"@0\xdd\x03$\x03\x00\x01\x01"), + ) + + # wait for the event + await hass.async_block_till_done() + + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get_device({get_device_id(mac)}) + device_id = device.id + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device_id, + CONF_TYPE: "invalid", + CONF_EVENT_PROPERTIES: None, + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_motion_detected"}, + }, + }, + ] + }, + ) + # Logs should return message to make sure event type is of one ["motion_detected"] + assert "motion_detected" in caplog.text + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_automation_with_invalid_trigger_event_property(hass, caplog): + """Test for automation with invalid trigger event property.""" + mac = "DE:70:E8:B2:39:0C" + entry = await _async_setup_xiaomi_device(hass, mac) + + # Emit motion detected event so it creates the device in the registry + inject_bluetooth_service_info_bleak( + hass, + make_advertisement(mac, b"@0\xdd\x03$\x03\x00\x01\x01"), + ) + + # wait for the event + await hass.async_block_till_done() + + dev_reg = async_get_dev_reg(hass) + device = dev_reg.async_get_device({get_device_id(mac)}) + device_id = device.id + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device_id, + CONF_TYPE: "motion_detected", + CONF_EVENT_PROPERTIES: "invalid_property", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_motion_detected"}, + }, + }, + ] + }, + ) + # Logs should return message to make sure event property is of one [None] for motion event + assert str([None]) in caplog.text + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + +async def test_triggers_for_invalid__model(hass, calls): + """Test invalid model doesn't return triggers.""" + mac = "DE:70:E8:B2:39:0C" + entry = await _async_setup_xiaomi_device(hass, mac) + + # Emit motion detected event so it creates the device in the registry + inject_bluetooth_service_info_bleak( + hass, + make_advertisement(mac, b"@0\xdd\x03$\x03\x00\x01\x01"), + ) + + # wait for the event + await hass.async_block_till_done() + + dev_reg = async_get_dev_reg(hass) + # modify model to invalid model + invalid_model = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, mac)}, + model="invalid model", + ) + invalid_model_id = invalid_model.id + + # setup automation to validate trigger config + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: invalid_model_id, + CONF_TYPE: "motion_detected", + CONF_EVENT_PROPERTIES: None, + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_motion_detected"}, + }, + }, + ] + }, + ) + + triggers = await async_get_device_automations( + hass, DeviceAutomationType.TRIGGER, invalid_model_id + ) + assert triggers == [] + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/xiaomi_ble/test_sensor.py b/tests/components/xiaomi_ble/test_sensor.py index 25f15938c6c..43c539aeb68 100644 --- a/tests/components/xiaomi_ble/test_sensor.py +++ b/tests/components/xiaomi_ble/test_sensor.py @@ -1,5 +1,4 @@ -"""Test the Xiaomi config flow.""" - +"""Test Xiaomi BLE sensors.""" from homeassistant.components.sensor import ATTR_STATE_CLASS from homeassistant.components.xiaomi_ble.const import DOMAIN diff --git a/tests/components/yolink/test_config_flow.py b/tests/components/yolink/test_config_flow.py index f809596e816..9ed6d3020ad 100644 --- a/tests/components/yolink/test_config_flow.py +++ b/tests/components/yolink/test_config_flow.py @@ -172,7 +172,6 @@ async def test_reauthentication( result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) - # pylint: disable=protected-access state = config_entry_oauth2_flow._encode_jwt( hass, { diff --git a/tests/components/youless/test_config_flows.py b/tests/components/youless/test_config_flows.py index 5bf119681c4..08f38f8eb2c 100644 --- a/tests/components/youless/test_config_flows.py +++ b/tests/components/youless/test_config_flows.py @@ -28,7 +28,6 @@ async def test_full_flow(hass: HomeAssistant) -> None: assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_youless = _get_mock_youless_api( initialize={"homes": [{"id": 1, "name": "myhome"}]} @@ -56,7 +55,6 @@ async def test_not_found(hass: HomeAssistant) -> None: assert result.get("type") == FlowResultType.FORM assert result.get("errors") == {} assert result.get("step_id") == SOURCE_USER - assert "flow_id" in result mock_youless = _get_mock_youless_api(initialize=URLError("")) with patch( diff --git a/tests/components/zamg/test_config_flow.py b/tests/components/zamg/test_config_flow.py index 26939c07f0c..e7df8532e26 100644 --- a/tests/components/zamg/test_config_flow.py +++ b/tests/components/zamg/test_config_flow.py @@ -1,15 +1,14 @@ """Tests for the Zamg config flow.""" from unittest.mock import MagicMock -from zamg.exceptions import ZamgApiError, ZamgStationNotFoundError +from zamg.exceptions import ZamgApiError from homeassistant.components.zamg.const import CONF_STATION_ID, DOMAIN, LOGGER -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER -from homeassistant.const import CONF_NAME +from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .conftest import TEST_STATION_ID, TEST_STATION_NAME +from .conftest import TEST_STATION_ID async def test_full_user_flow_implementation( @@ -26,7 +25,6 @@ async def test_full_user_flow_implementation( assert result.get("type") == FlowResultType.FORM LOGGER.debug(result) assert result.get("data_schema") != "" - assert "flow_id" in result result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_STATION_ID: TEST_STATION_ID}, @@ -68,7 +66,6 @@ async def test_error_update( LOGGER.debug(result) assert result.get("data_schema") != "" mock_zamg.update.side_effect = ZamgApiError - assert "flow_id" in result result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_STATION_ID: TEST_STATION_ID}, @@ -77,21 +74,6 @@ async def test_error_update( assert result.get("reason") == "cannot_connect" -async def test_full_import_flow_implementation( - hass: HomeAssistant, - mock_zamg: MagicMock, - mock_setup_entry: None, -) -> None: - """Test the full import flow from start to finish.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_STATION_ID: TEST_STATION_ID, CONF_NAME: TEST_STATION_NAME}, - ) - assert result.get("type") == FlowResultType.CREATE_ENTRY - assert result.get("data") == {CONF_STATION_ID: TEST_STATION_ID} - - async def test_user_flow_duplicate( hass: HomeAssistant, mock_zamg: MagicMock, @@ -105,7 +87,6 @@ async def test_user_flow_duplicate( assert result.get("step_id") == "user" assert result.get("type") == FlowResultType.FORM - assert "flow_id" in result result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_STATION_ID: TEST_STATION_ID}, @@ -128,116 +109,3 @@ async def test_user_flow_duplicate( ) assert result.get("type") == FlowResultType.ABORT assert result.get("reason") == "already_configured" - - -async def test_import_flow_duplicate( - hass: HomeAssistant, - mock_zamg: MagicMock, - mock_setup_entry: None, -) -> None: - """Test import flow with duplicate entry.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - ) - - assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM - assert "flow_id" in result - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_STATION_ID: TEST_STATION_ID}, - ) - assert result.get("type") == FlowResultType.CREATE_ENTRY - assert "data" in result - assert result["data"][CONF_STATION_ID] == TEST_STATION_ID - assert "result" in result - assert result["result"].unique_id == TEST_STATION_ID - # try to add another instance - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_STATION_ID: TEST_STATION_ID, CONF_NAME: TEST_STATION_NAME}, - ) - assert result.get("type") == FlowResultType.ABORT - assert result.get("reason") == "already_configured" - - -async def test_import_flow_duplicate_after_position( - hass: HomeAssistant, - mock_zamg: MagicMock, - mock_setup_entry: None, -) -> None: - """Test import flow with duplicate entry.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_USER}, - ) - - assert result.get("step_id") == "user" - assert result.get("type") == FlowResultType.FORM - assert "flow_id" in result - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_STATION_ID: TEST_STATION_ID}, - ) - assert result.get("type") == FlowResultType.CREATE_ENTRY - assert "data" in result - assert result["data"][CONF_STATION_ID] == TEST_STATION_ID - assert "result" in result - assert result["result"].unique_id == TEST_STATION_ID - # try to add another instance - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_STATION_ID: "123", CONF_NAME: TEST_STATION_NAME}, - ) - assert result.get("type") == FlowResultType.ABORT - assert result.get("reason") == "already_configured" - - -async def test_import_flow_no_name( - hass: HomeAssistant, - mock_zamg: MagicMock, - mock_setup_entry: None, -) -> None: - """Test import flow without any name.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_STATION_ID: TEST_STATION_ID}, - ) - assert result.get("type") == FlowResultType.CREATE_ENTRY - assert result.get("data") == {CONF_STATION_ID: TEST_STATION_ID} - - -async def test_import_flow_invalid_station( - hass: HomeAssistant, - mock_zamg: MagicMock, - mock_setup_entry: None, -) -> None: - """Test import flow with invalid station.""" - mock_zamg.closest_station.side_effect = ZamgStationNotFoundError - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_STATION_ID: ""}, - ) - assert result.get("type") == FlowResultType.ABORT - assert result.get("reason") == "station_not_found" - - -async def test_import_flow_zamg_error( - hass: HomeAssistant, - mock_zamg: MagicMock, - mock_setup_entry: None, -) -> None: - """Test import flow with error on getting zamg stations.""" - mock_zamg.zamg_stations.side_effect = ZamgApiError - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={CONF_STATION_ID: ""}, - ) - assert result.get("type") == FlowResultType.ABORT - assert result.get("reason") == "cannot_connect" diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index a0684d6e10e..5e499c93fff 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -192,11 +192,26 @@ async def test_setup_with_overly_long_url_and_name(hass, mock_async_zeroconf, ca zeroconf, "HaAsyncServiceBrowser", side_effect=service_update_mock ), patch( "homeassistant.components.zeroconf.get_url", - return_value="https://this.url.is.way.too.long/very/deep/path/that/will/make/us/go/over/the/maximum/string/length/and/would/cause/zeroconf/to/fail/to/startup/because/the/key/and/value/can/only/be/255/bytes/and/this/string/is/a/bit/longer/than/the/maximum/length/that/we/allow/for/a/value", + return_value=( + "https://this.url.is.way.too.long/very/deep/path/that/will/make/us/go/over" + "/the/maximum/string/length/and/would/cause/zeroconf/to/fail/to/startup" + "/because/the/key/and/value/can/only/be/255/bytes/and/this/string/is/a" + "/bit/longer/than/the/maximum/length/that/we/allow/for/a/value" + ), ), patch.object( hass.config, "location_name", - "\u00dcBER \u00dcber German Umlaut long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string", + ( + "\u00dcBER \u00dcber German Umlaut long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string long string long" + " string long string long string long string long string" + ), ), patch( "homeassistant.components.zeroconf.AsyncServiceInfo.request", ): @@ -717,7 +732,9 @@ async def test_homekit_not_paired(hass, mock_async_zeroconf): async def test_homekit_controller_still_discovered_unpaired_for_cloud( hass, mock_async_zeroconf ): - """Test discovery is still passed to homekit controller when unpaired and discovered by cloud integration. + """Test discovery is still passed to homekit controller when unpaired. + + When unpaired and discovered by cloud integration. Since we prefer local control, if the integration that is being discovered is cloud AND the homekit device is unpaired we still want to discovery it @@ -751,7 +768,9 @@ async def test_homekit_controller_still_discovered_unpaired_for_cloud( async def test_homekit_controller_still_discovered_unpaired_for_polling( hass, mock_async_zeroconf ): - """Test discovery is still passed to homekit controller when unpaired and discovered by polling integration. + """Test discovery is still passed to homekit controller when unpaired. + + When unpaired and discovered by polling integration. Since we prefer local push, if the integration that is being discovered is polling AND the homekit device is unpaired we still want to discovery it @@ -938,7 +957,7 @@ _ADAPTER_WITH_DEFAULT_ENABLED = [ async def test_async_detect_interfaces_setting_non_loopback_route( hass, mock_async_zeroconf ): - """Test without default interface config and the route returns a non-loopback address.""" + """Test without default interface and the route returns a non-loopback address.""" with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc, patch.object( hass.config_entries.flow, "async_init" ), patch.object( @@ -1052,7 +1071,7 @@ async def test_async_detect_interfaces_setting_empty_route_linux( async def test_async_detect_interfaces_setting_empty_route_freebsd( hass, mock_async_zeroconf ): - """Test without default interface config and the route returns nothing on freebsd.""" + """Test without default interface and the route returns nothing on freebsd.""" with patch("homeassistant.components.zeroconf.sys.platform", "freebsd"), patch( "homeassistant.components.zeroconf.HaZeroconf" ) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object( diff --git a/tests/components/zeversolar/__init__.py b/tests/components/zeversolar/__init__.py new file mode 100644 index 00000000000..c7e65bc62fd --- /dev/null +++ b/tests/components/zeversolar/__init__.py @@ -0,0 +1 @@ +"""Tests for the Zeversolar integration.""" diff --git a/tests/components/zeversolar/test_config_flow.py b/tests/components/zeversolar/test_config_flow.py new file mode 100644 index 00000000000..f4b0c6b5389 --- /dev/null +++ b/tests/components/zeversolar/test_config_flow.py @@ -0,0 +1,135 @@ +"""Test the Zeversolar config flow.""" +from unittest.mock import MagicMock, patch + +import pytest +from zeversolar.exceptions import ( + ZeverSolarHTTPError, + ZeverSolarHTTPNotFound, + ZeverSolarTimeout, +) + +from homeassistant import config_entries +from homeassistant.components.zeversolar.const import DOMAIN +from homeassistant.const import CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] is None + + await _set_up_zeversolar(hass=hass, flow_id=result["flow_id"]) + + +@pytest.mark.parametrize( + "side_effect,errors", + ( + ( + ZeverSolarHTTPNotFound, + {"base": "invalid_host"}, + ), + ( + ZeverSolarHTTPError, + {"base": "cannot_connect"}, + ), + ( + ZeverSolarTimeout, + {"base": "timeout_connect"}, + ), + ( + RuntimeError, + {"base": "unknown"}, + ), + ), +) +async def test_form_errors( + hass: HomeAssistant, + side_effect: Exception, + errors: dict, +) -> None: + """Test we handle errors.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "zeversolar.ZeverSolarClient.get_data", + side_effect=side_effect, + ): + result2 = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], + user_input={ + CONF_HOST: "test_ip", + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == errors + + await _set_up_zeversolar(hass=hass, flow_id=result["flow_id"]) + + +async def test_abort_already_configured(hass: HomeAssistant) -> None: + """Test we abort when the device is already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Zeversolar", + data={CONF_HOST: "test_ip"}, + unique_id="test_serial", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == FlowResultType.FORM + assert result.get("errors") is None + assert "flow_id" in result + + mock_data = MagicMock() + mock_data.serial_number = "test_serial" + with patch("zeversolar.ZeverSolarClient.get_data", return_value=mock_data), patch( + "homeassistant.components.zeversolar.async_setup_entry", + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], + user_input={ + CONF_HOST: "test_ip", + }, + ) + await hass.async_block_till_done() + + assert result2.get("type") == FlowResultType.ABORT + assert result2.get("reason") == "already_configured" + assert len(mock_setup_entry.mock_calls) == 0 + + +async def _set_up_zeversolar(hass: HomeAssistant, flow_id: str) -> None: + """Reusable successful setup of Zeversolar sensor.""" + mock_data = MagicMock() + mock_data.serial_number = "test_serial" + with patch("zeversolar.ZeverSolarClient.get_data", return_value=mock_data), patch( + "homeassistant.components.zeversolar.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + flow_id=flow_id, + user_input={ + CONF_HOST: "test_ip", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + assert result2["title"] == "Zeversolar" + assert result2["data"] == { + CONF_HOST: "test_ip", + } + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 53935caa435..ff819413fc5 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -179,7 +179,7 @@ def async_find_group_entity_id(hass, domain, group): async def async_enable_traffic(hass, zha_devices, enabled=True): - """Allow traffic to flow through the gateway and the zha device.""" + """Allow traffic to flow through the gateway and the ZHA device.""" for zha_device in zha_devices: zha_device.update_available(enabled) await hass.async_block_till_done() diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index e002d8ce03b..784e6bae731 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -84,6 +84,7 @@ async def config_entry_fixture(hass): zha_const.CUSTOM_CONFIGURATION: { zha_const.ZHA_OPTIONS: { zha_const.CONF_ENABLE_ENHANCED_LIGHT_TRANSITION: True, + zha_const.CONF_GROUP_MEMBERS_ASSUME_STATE: False, }, zha_const.ZHA_ALARM_OPTIONS: { zha_const.CONF_ALARM_ARM_REQUIRES_CODE: False, @@ -228,7 +229,7 @@ def zha_device_joined_restored(request): @pytest.fixture def zha_device_mock(hass, zigpy_device_mock): - """Return a zha Device factory.""" + """Return a ZHA Device factory.""" def _zha_device( endpoints=None, diff --git a/tests/components/zha/data.py b/tests/components/zha/data.py index 8b613ec2971..024a5e75fbc 100644 --- a/tests/components/zha/data.py +++ b/tests/components/zha/data.py @@ -28,6 +28,12 @@ BASE_CUSTOM_CONFIGURATION = { "required": True, "default": True, }, + { + "type": "boolean", + "name": "group_members_assume_state", + "required": True, + "default": True, + }, { "type": "boolean", "name": "enable_identify_on_join", @@ -56,6 +62,7 @@ BASE_CUSTOM_CONFIGURATION = { "default_light_transition": 0, "light_transitioning_flag": True, "always_prefer_xy_color_mode": True, + "group_members_assume_state": False, "enable_identify_on_join": True, "consider_unavailable_mains": 7200, "consider_unavailable_battery": 21600, @@ -91,6 +98,12 @@ CONFIG_WITH_ALARM_OPTIONS = { "required": True, "default": True, }, + { + "type": "boolean", + "name": "group_members_assume_state", + "required": True, + "default": True, + }, { "type": "boolean", "name": "enable_identify_on_join", @@ -140,6 +153,7 @@ CONFIG_WITH_ALARM_OPTIONS = { "default_light_transition": 0, "light_transitioning_flag": True, "always_prefer_xy_color_mode": True, + "group_members_assume_state": False, "enable_identify_on_join": True, "consider_unavailable_mains": 7200, "consider_unavailable_battery": 21600, diff --git a/tests/components/zha/test_alarm_control_panel.py b/tests/components/zha/test_alarm_control_panel.py index 0d3b8ffa9f1..e79bbfca012 100644 --- a/tests/components/zha/test_alarm_control_panel.py +++ b/tests/components/zha/test_alarm_control_panel.py @@ -1,4 +1,4 @@ -"""Test zha alarm control panel.""" +"""Test ZHA alarm control panel.""" from unittest.mock import AsyncMock, call, patch, sentinel import pytest @@ -24,7 +24,7 @@ from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @pytest.fixture(autouse=True) def alarm_control_panel_platform_only(): - """Only setup the alarm_control_panel and required base platforms to speed up tests.""" + """Only set up the alarm_control_panel and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -58,7 +58,7 @@ def zigpy_device(zigpy_device_mock): new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), ) async def test_alarm_control_panel(hass, zha_device_joined_restored, zigpy_device): - """Test zha alarm control panel platform.""" + """Test ZHA alarm control panel platform.""" zha_device = await zha_device_joined_restored(zigpy_device) cluster = zigpy_device.endpoints.get(1).ias_ace diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index defc9842b01..a02273de28d 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -60,7 +60,7 @@ IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" @pytest.fixture(autouse=True) def required_platform_only(): - """Only setup the required and required base platforms to speed up tests.""" + """Only set up the required and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -75,7 +75,7 @@ def required_platform_only(): @pytest.fixture async def device_switch(hass, zigpy_device_mock, zha_device_joined): - """Test zha switch platform.""" + """Test ZHA switch platform.""" zigpy_device = zigpy_device_mock( { @@ -114,7 +114,7 @@ async def device_ias_ace(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_groupable(hass, zigpy_device_mock, zha_device_joined): - """Test zha light platform.""" + """Test ZHA light platform.""" zigpy_device = zigpy_device_mock( { @@ -138,7 +138,7 @@ async def device_groupable(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def zha_client(hass, hass_ws_client, device_switch, device_groupable): - """Test zha switch platform.""" + """Get ZHA WebSocket client.""" # load the ZHA API async_load_api(hass) @@ -216,7 +216,7 @@ async def test_device_cluster_commands(zha_client): async def test_list_devices(zha_client): - """Test getting zha devices.""" + """Test getting ZHA devices.""" await zha_client.send_json({ID: 5, TYPE: "zha/devices"}) msg = await zha_client.receive_json() @@ -249,7 +249,7 @@ async def test_list_devices(zha_client): async def test_get_zha_config(zha_client): - """Test getting zha custom configuration.""" + """Test getting ZHA custom configuration.""" await zha_client.send_json({ID: 5, TYPE: "zha/configuration"}) msg = await zha_client.receive_json() @@ -259,7 +259,7 @@ async def test_get_zha_config(zha_client): async def test_get_zha_config_with_alarm(hass, zha_client, device_ias_ace): - """Test getting zha custom configuration.""" + """Test getting ZHA custom configuration.""" await zha_client.send_json({ID: 5, TYPE: "zha/configuration"}) msg = await zha_client.receive_json() @@ -279,7 +279,7 @@ async def test_get_zha_config_with_alarm(hass, zha_client, device_ias_ace): async def test_update_zha_config(zha_client, zigpy_app_controller): - """Test updating zha custom configuration.""" + """Test updating ZHA custom configuration.""" configuration = deepcopy(CONFIG_WITH_ALARM_OPTIONS) configuration["data"]["zha_options"]["default_light_transition"] = 10 @@ -313,7 +313,7 @@ async def test_device_not_found(zha_client): async def test_list_groups(zha_client): - """Test getting zha zigbee groups.""" + """Test getting ZHA zigbee groups.""" await zha_client.send_json({ID: 7, TYPE: "zha/groups"}) msg = await zha_client.receive_json() @@ -330,7 +330,7 @@ async def test_list_groups(zha_client): async def test_get_group(zha_client): - """Test getting a specific zha zigbee group.""" + """Test getting a specific ZHA zigbee group.""" await zha_client.send_json({ID: 8, TYPE: "zha/group", GROUP_ID: FIXTURE_GRP_ID}) msg = await zha_client.receive_json() @@ -357,7 +357,7 @@ async def test_get_group_not_found(zha_client): async def test_list_groupable_devices(zha_client, device_groupable): - """Test getting zha devices that have a group cluster.""" + """Test getting ZHA devices that have a group cluster.""" await zha_client.send_json({ID: 10, TYPE: "zha/devices/groupable"}) @@ -400,7 +400,7 @@ async def test_list_groupable_devices(zha_client, device_groupable): async def test_add_group(zha_client): - """Test adding and getting a new zha zigbee group.""" + """Test adding and getting a new ZHA zigbee group.""" await zha_client.send_json({ID: 12, TYPE: "zha/group/add", GROUP_NAME: "new_group"}) msg = await zha_client.receive_json() @@ -426,7 +426,7 @@ async def test_add_group(zha_client): async def test_remove_group(zha_client): - """Test removing a new zha zigbee group.""" + """Test removing a new ZHA zigbee group.""" await zha_client.send_json({ID: 14, TYPE: "zha/groups"}) diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index c27c8be16d8..50ad7ffab92 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -1,4 +1,4 @@ -"""Test zha binary sensor.""" +"""Test ZHA binary sensor.""" from unittest.mock import patch import pytest @@ -38,7 +38,7 @@ DEVICE_OCCUPANCY = { @pytest.fixture(autouse=True) def binary_sensor_platform_only(): - """Only setup the binary_sensor and required base platforms to speed up tests.""" + """Only set up the binary_sensor and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( diff --git a/tests/components/zha/test_button.py b/tests/components/zha/test_button.py index 17c8f0ccb28..953dbc5d079 100644 --- a/tests/components/zha/test_button.py +++ b/tests/components/zha/test_button.py @@ -38,7 +38,7 @@ from tests.common import mock_coro @pytest.fixture(autouse=True) def button_platform_only(): - """Only setup the button and required base platforms to speed up tests.""" + """Only set up the button and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -121,7 +121,7 @@ async def tuya_water_valve(hass, zigpy_device_mock, zha_device_joined_restored): @freeze_time("2021-11-04 17:37:00", tz_offset=-1) async def test_button(hass, contact_sensor): - """Test zha button platform.""" + """Test ZHA button platform.""" entity_registry = er.async_get(hass) zha_device, cluster = contact_sensor @@ -161,7 +161,7 @@ async def test_button(hass, contact_sensor): async def test_frost_unlock(hass, tuya_water_valve): - """Test custom frost unlock zha button.""" + """Test custom frost unlock ZHA button.""" entity_registry = er.async_get(hass) zha_device, cluster = tuya_water_valve diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index 6aba5500a2a..0ab905692c2 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -106,35 +106,43 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock): @pytest.mark.parametrize( "cluster_id, bind_count, attrs", [ - (0x0000, 0, {}), - (0x0001, 1, {"battery_voltage", "battery_percentage_remaining"}), - (0x0002, 1, {"current_temperature"}), - (0x0003, 0, {}), - (0x0004, 0, {}), - (0x0005, 1, {}), - (0x0006, 1, {"on_off"}), - (0x0007, 1, {}), - (0x0008, 1, {"current_level"}), - (0x0009, 1, {}), - (0x000C, 1, {"present_value"}), - (0x000D, 1, {"present_value"}), - (0x000E, 1, {"present_value"}), - (0x000D, 1, {"present_value"}), - (0x0010, 1, {"present_value"}), - (0x0011, 1, {"present_value"}), - (0x0012, 1, {"present_value"}), - (0x0013, 1, {"present_value"}), - (0x0014, 1, {"present_value"}), - (0x0015, 1, {}), - (0x0016, 1, {}), - (0x0019, 0, {}), - (0x001A, 1, {}), - (0x001B, 1, {}), - (0x0020, 1, {}), - (0x0021, 0, {}), - (0x0101, 1, {"lock_state"}), + (zigpy.zcl.clusters.general.Basic.cluster_id, 0, {}), ( - 0x0201, + zigpy.zcl.clusters.general.PowerConfiguration.cluster_id, + 1, + {"battery_voltage", "battery_percentage_remaining"}, + ), + ( + zigpy.zcl.clusters.general.DeviceTemperature.cluster_id, + 1, + {"current_temperature"}, + ), + (zigpy.zcl.clusters.general.Identify.cluster_id, 0, {}), + (zigpy.zcl.clusters.general.Groups.cluster_id, 0, {}), + (zigpy.zcl.clusters.general.Scenes.cluster_id, 1, {}), + (zigpy.zcl.clusters.general.OnOff.cluster_id, 1, {"on_off"}), + (zigpy.zcl.clusters.general.OnOffConfiguration.cluster_id, 1, {}), + (zigpy.zcl.clusters.general.LevelControl.cluster_id, 1, {"current_level"}), + (zigpy.zcl.clusters.general.Alarms.cluster_id, 1, {}), + (zigpy.zcl.clusters.general.AnalogInput.cluster_id, 1, {"present_value"}), + (zigpy.zcl.clusters.general.AnalogOutput.cluster_id, 1, {"present_value"}), + (zigpy.zcl.clusters.general.AnalogValue.cluster_id, 1, {"present_value"}), + (zigpy.zcl.clusters.general.AnalogOutput.cluster_id, 1, {"present_value"}), + (zigpy.zcl.clusters.general.BinaryOutput.cluster_id, 1, {"present_value"}), + (zigpy.zcl.clusters.general.BinaryValue.cluster_id, 1, {"present_value"}), + (zigpy.zcl.clusters.general.MultistateInput.cluster_id, 1, {"present_value"}), + (zigpy.zcl.clusters.general.MultistateOutput.cluster_id, 1, {"present_value"}), + (zigpy.zcl.clusters.general.MultistateValue.cluster_id, 1, {"present_value"}), + (zigpy.zcl.clusters.general.Commissioning.cluster_id, 1, {}), + (zigpy.zcl.clusters.general.Partition.cluster_id, 1, {}), + (zigpy.zcl.clusters.general.Ota.cluster_id, 0, {}), + (zigpy.zcl.clusters.general.PowerProfile.cluster_id, 1, {}), + (zigpy.zcl.clusters.general.ApplianceControl.cluster_id, 1, {}), + (zigpy.zcl.clusters.general.PollControl.cluster_id, 1, {}), + (zigpy.zcl.clusters.general.GreenPowerProxy.cluster_id, 0, {}), + (zigpy.zcl.clusters.closures.DoorLock.cluster_id, 1, {"lock_state"}), + ( + zigpy.zcl.clusters.hvac.Thermostat.cluster_id, 1, { "local_temperature", @@ -150,9 +158,9 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock): "pi_heating_demand", }, ), - (0x0202, 1, {"fan_mode"}), + (zigpy.zcl.clusters.hvac.Fan.cluster_id, 1, {"fan_mode"}), ( - 0x0300, + zigpy.zcl.clusters.lighting.Color.cluster_id, 1, { "current_x", @@ -163,16 +171,54 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock): "current_saturation", }, ), - (0x0400, 1, {"measured_value"}), - (0x0401, 1, {"level_status"}), - (0x0402, 1, {"measured_value"}), - (0x0403, 1, {"measured_value"}), - (0x0404, 1, {"measured_value"}), - (0x0405, 1, {"measured_value"}), - (0x0406, 1, {"occupancy"}), - (0x0702, 1, {"instantaneous_demand"}), ( - 0x0B04, + zigpy.zcl.clusters.measurement.IlluminanceMeasurement.cluster_id, + 1, + {"measured_value"}, + ), + ( + zigpy.zcl.clusters.measurement.IlluminanceLevelSensing.cluster_id, + 1, + {"level_status"}, + ), + ( + zigpy.zcl.clusters.measurement.TemperatureMeasurement.cluster_id, + 1, + {"measured_value"}, + ), + ( + zigpy.zcl.clusters.measurement.PressureMeasurement.cluster_id, + 1, + {"measured_value"}, + ), + ( + zigpy.zcl.clusters.measurement.FlowMeasurement.cluster_id, + 1, + {"measured_value"}, + ), + ( + zigpy.zcl.clusters.measurement.RelativeHumidity.cluster_id, + 1, + {"measured_value"}, + ), + (zigpy.zcl.clusters.measurement.OccupancySensing.cluster_id, 1, {"occupancy"}), + ( + zigpy.zcl.clusters.smartenergy.Metering.cluster_id, + 1, + { + "instantaneous_demand", + "current_summ_delivered", + "current_tier1_summ_delivered", + "current_tier2_summ_delivered", + "current_tier3_summ_delivered", + "current_tier4_summ_delivered", + "current_tier5_summ_delivered", + "current_tier6_summ_delivered", + "status", + }, + ), + ( + zigpy.zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id, 1, { "active_power", diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index d00e6bca795..123c3c69ba3 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -1,4 +1,4 @@ -"""Test zha climate.""" +"""Test ZHA climate.""" from unittest.mock import patch import pytest @@ -173,7 +173,7 @@ ZCL_ATTR_PLUG = { @pytest.fixture(autouse=True) def climate_platform_only(): - """Only setup the climate and required base platforms to speed up tests.""" + """Only set up the climate and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index d457e0b6b8c..acff888dfde 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -1,6 +1,7 @@ """Tests for ZHA config flow.""" import copy +from datetime import timedelta import json from unittest.mock import AsyncMock, MagicMock, PropertyMock, create_autospec, patch import uuid @@ -67,12 +68,27 @@ def mock_app(): @pytest.fixture -def backup(): - """Zigpy network backup with non-default settings.""" - backup = zigpy.backups.NetworkBackup() - backup.node_info.ieee = zigpy.types.EUI64.convert("AA:BB:CC:DD:11:22:33:44") +def make_backup(): + """Zigpy network backup factory that creates unique backups with each call.""" + num_calls = 0 - return backup + def inner(*, backup_time_offset=0): + nonlocal num_calls + + backup = zigpy.backups.NetworkBackup() + backup.backup_time += timedelta(seconds=backup_time_offset) + backup.node_info.ieee = zigpy.types.EUI64.convert(f"AABBCCDDEE{num_calls:06X}") + num_calls += 1 + + return backup + + return inner + + +@pytest.fixture +def backup(make_backup): + """Zigpy network backup with non-default settings.""" + return make_backup() def mock_detect_radio_type(radio_type=RadioType.ezsp, ret=True): @@ -1101,6 +1117,56 @@ async def test_formation_strategy_form_new_network(pick_radio, mock_app, hass): assert result2["type"] == FlowResultType.CREATE_ENTRY +async def test_formation_strategy_form_initial_network(pick_radio, mock_app, hass): + """Test forming a new network, with no previous settings on the radio.""" + mock_app.load_network_info = AsyncMock(side_effect=NetworkNotFormed()) + + result, port = await pick_radio(RadioType.ezsp) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"next_step_id": config_flow.FORMATION_FORM_INITIAL_NETWORK}, + ) + await hass.async_block_till_done() + + # A new network will be formed + mock_app.form_network.assert_called_once() + + assert result2["type"] == FlowResultType.CREATE_ENTRY + + +@patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) +async def test_onboarding_auto_formation_new_hardware(mock_app, hass): + """Test auto network formation with new hardware during onboarding.""" + mock_app.load_network_info = AsyncMock(side_effect=NetworkNotFormed()) + discovery_info = usb.UsbServiceInfo( + device="/dev/ttyZIGBEE", + pid="AAAA", + vid="AAAA", + serial_number="1234", + description="zigbee radio", + manufacturer="test", + ) + + with patch( + "homeassistant.components.onboarding.async_is_onboarded", return_value=False + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USB}, data=discovery_info + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "zigbee radio" + assert result["data"] == { + "device": { + "baudrate": 115200, + "flow_control": None, + "path": "/dev/ttyZIGBEE", + }, + CONF_RADIO_TYPE: "znp", + } + + async def test_formation_strategy_reuse_settings(pick_radio, mock_app, hass): """Test reusing existing network settings.""" result, port = await pick_radio(RadioType.ezsp) @@ -1298,13 +1364,13 @@ def test_format_backup_choice(): ) @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) async def test_formation_strategy_restore_automatic_backup_ezsp( - pick_radio, mock_app, hass + pick_radio, mock_app, make_backup, hass ): """Test restoring an automatic backup (EZSP radio).""" mock_app.backups.backups = [ - MagicMock(), - MagicMock(), - MagicMock(), + make_backup(), + make_backup(), + make_backup(), ] backup = mock_app.backups.backups[1] # pick the second one backup.is_compatible_with = MagicMock(return_value=False) @@ -1347,13 +1413,13 @@ async def test_formation_strategy_restore_automatic_backup_ezsp( @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) @pytest.mark.parametrize("is_advanced", [True, False]) async def test_formation_strategy_restore_automatic_backup_non_ezsp( - is_advanced, pick_radio, mock_app, hass + is_advanced, pick_radio, mock_app, make_backup, hass ): """Test restoring an automatic backup (non-EZSP radio).""" mock_app.backups.backups = [ - MagicMock(), - MagicMock(), - MagicMock(), + make_backup(backup_time_offset=5), + make_backup(backup_time_offset=-3), + make_backup(backup_time_offset=2), ] backup = mock_app.backups.backups[1] # pick the second one backup.is_compatible_with = MagicMock(return_value=False) @@ -1375,13 +1441,20 @@ async def test_formation_strategy_restore_automatic_backup_non_ezsp( assert result2["type"] == FlowResultType.FORM assert result2["step_id"] == "choose_automatic_backup" - # We must prompt for overwriting the IEEE address + # We don't prompt for overwriting the IEEE address, since only EZSP needs this assert config_flow.OVERWRITE_COORDINATOR_IEEE not in result2["data_schema"].schema + # The backup choices are ordered by date + assert result2["data_schema"].schema["choose_automatic_backup"].container == [ + f"choice:{mock_app.backups.backups[0]!r}", + f"choice:{mock_app.backups.backups[2]!r}", + f"choice:{mock_app.backups.backups[1]!r}", + ] + result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], user_input={ - config_flow.CHOOSE_AUTOMATIC_BACKUP: "choice:" + repr(backup), + config_flow.CHOOSE_AUTOMATIC_BACKUP: f"choice:{backup!r}", }, ) diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 0f55735ecb2..cb3c4324c9d 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -1,4 +1,4 @@ -"""Test zha cover.""" +"""Test ZHA cover.""" import asyncio from unittest.mock import AsyncMock, patch @@ -41,7 +41,7 @@ from tests.common import async_capture_events, mock_coro, mock_restore_cache @pytest.fixture(autouse=True) def cover_platform_only(): - """Only setup the cover and required base platforms to speed up tests.""" + """Only set up the cover and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -121,7 +121,7 @@ def zigpy_keen_vent(zigpy_device_mock): async def test_cover(hass, zha_device_joined_restored, zigpy_cover_device): - """Test zha cover platform.""" + """Test ZHA cover platform.""" # load up cover domain cluster = zigpy_cover_device.endpoints.get(1).window_covering @@ -212,7 +212,7 @@ async def test_cover(hass, zha_device_joined_restored, zigpy_cover_device): async def test_shade(hass, zha_device_joined_restored, zigpy_shade_device): - """Test zha cover platform for shade device type.""" + """Test ZHA cover platform for shade device type.""" # load up cover domain zha_device = await zha_device_joined_restored(zigpy_shade_device) @@ -418,7 +418,7 @@ async def test_keen_vent(hass, zha_device_joined_restored, zigpy_keen_vent): async def test_cover_remote(hass, zha_device_joined_restored, zigpy_cover_remote): - """Test zha cover remote.""" + """Test ZHA cover remote.""" # load up cover domain await zha_device_joined_restored(zigpy_cover_remote) diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index 733a8e99e4b..eef7953b6d2 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -1,5 +1,6 @@ -"""Test zha device switch.""" +"""Test ZHA device switch.""" from datetime import timedelta +import logging import time from unittest import mock from unittest.mock import patch @@ -26,7 +27,7 @@ from tests.common import async_fire_time_changed @pytest.fixture(autouse=True) def required_platforms_only(): - """Only setup the required platform and required base platforms to speed up tests.""" + """Only set up the required platform and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -86,13 +87,13 @@ def zigpy_device_mains(zigpy_device_mock): @pytest.fixture def device_with_basic_channel(zigpy_device_mains): - """Return a zha device with a basic channel present.""" + """Return a ZHA device with a basic channel present.""" return zigpy_device_mains(with_basic_channel=True) @pytest.fixture def device_without_basic_channel(zigpy_device): - """Return a zha device with a basic channel present.""" + """Return a ZHA device with a basic channel present.""" return zigpy_device(with_basic_channel=False) @@ -130,8 +131,6 @@ async def test_check_available_success( hass, device_with_basic_channel, zha_device_restored ): """Check device availability success on 1st try.""" - - # pylint: disable=protected-access zha_device = await zha_device_restored(device_with_basic_channel) await async_enable_traffic(hass, [zha_device]) basic_ch = device_with_basic_channel.endpoints[3].basic @@ -185,7 +184,6 @@ async def test_check_available_unsuccessful( ): """Check device availability all tries fail.""" - # pylint: disable=protected-access zha_device = await zha_device_restored(device_with_basic_channel) await async_enable_traffic(hass, [zha_device]) basic_ch = device_with_basic_channel.endpoints[3].basic @@ -197,7 +195,7 @@ async def test_check_available_unsuccessful( time.time() - zha_device.consider_unavailable_time - 2 ) - # unsuccessfuly ping zigpy device, but zha_device is still available + # unsuccessfully ping zigpy device, but zha_device is still available _send_time_changed(hass, 91) await hass.async_block_till_done() assert basic_ch.read_attributes.await_count == 1 @@ -227,8 +225,8 @@ async def test_check_available_no_basic_channel( hass, device_without_basic_channel, zha_device_restored, caplog ): """Check device availability for a device without basic cluster.""" + caplog.set_level(logging.DEBUG, logger="homeassistant.components.zha") - # pylint: disable=protected-access zha_device = await zha_device_restored(device_without_basic_channel) await async_enable_traffic(hass, [zha_device]) diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 19125558b52..06285ea8cfc 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -1,4 +1,4 @@ -"""The test for zha device automation actions.""" +"""The test for ZHA device automation actions.""" from unittest.mock import call, patch import pytest @@ -32,7 +32,7 @@ COMMAND_SINGLE = "single" @pytest.fixture(autouse=True) def required_platforms_only(): - """Only setup the required platforms and required base platforms to speed up tests.""" + """Only set up the required platforms and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -102,7 +102,7 @@ async def device_inovelli(hass, zigpy_device_mock, zha_device_joined): async def test_get_actions(hass, device_ias): - """Test we get the expected actions from a zha device.""" + """Test we get the expected actions from a ZHA device.""" ieee_address = str(device_ias[0].ieee) @@ -155,7 +155,7 @@ async def test_get_actions(hass, device_ias): async def test_get_inovelli_actions(hass, device_inovelli): - """Test we get the expected actions from a zha device.""" + """Test we get the expected actions from a ZHA device.""" inovelli_ieee_address = str(device_inovelli[0].ieee) ha_device_registry = dr.async_get(hass) @@ -235,7 +235,7 @@ async def test_get_inovelli_actions(hass, device_inovelli): async def test_action(hass, device_ias, device_inovelli): - """Test for executing a zha device action.""" + """Test for executing a ZHA device action.""" zigpy_device, zha_device = device_ias inovelli_zigpy_device, inovelli_zha_device = device_inovelli diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index 55be7ca9ebd..e6500e2739a 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -27,7 +27,7 @@ from tests.common import async_fire_time_changed @pytest.fixture(autouse=True) def device_tracker_platforms_only(): - """Only setup the device_tracker platforms and required base platforms to speed up tests.""" + """Only set up the device_tracker platforms and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -63,7 +63,7 @@ def zigpy_device_dt(zigpy_device_mock): async def test_device_tracker(hass, zha_device_joined_restored, zigpy_device_dt): - """Test zha device tracker platform.""" + """Test ZHA device tracker platform.""" zha_device = await zha_device_joined_restored(zigpy_device_dt) cluster = zigpy_device_dt.endpoints.get(1).power diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 0081ba04d16..127c2adae12 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -40,7 +40,7 @@ LONG_RELEASE = "remote_button_long_release" @pytest.fixture(autouse=True) def sensor_platforms_only(): - """Only setup the sensor platform and required base platforms to speed up tests.""" + """Only set up the sensor platform and required base platforms to speed up tests.""" with patch("homeassistant.components.zha.PLATFORMS", (Platform.SENSOR,)): yield @@ -83,7 +83,7 @@ async def mock_devices(hass, zigpy_device_mock, zha_device_joined_restored): async def test_triggers(hass, mock_devices): - """Test zha device triggers.""" + """Test ZHA device triggers.""" zigpy_device, zha_device = mock_devices @@ -158,7 +158,7 @@ async def test_triggers(hass, mock_devices): async def test_no_triggers(hass, mock_devices): - """Test zha device with no triggers.""" + """Test ZHA device with no triggers.""" _, zha_device = mock_devices ieee_address = str(zha_device.ieee) @@ -297,7 +297,7 @@ async def test_device_offline_fires( async def test_exception_no_triggers(hass, mock_devices, calls, caplog): - """Test for exception on event triggers firing.""" + """Test for exception when validating device triggers.""" _, zha_device = mock_devices @@ -327,11 +327,14 @@ async def test_exception_no_triggers(hass, mock_devices, calls, caplog): }, ) await hass.async_block_till_done() - assert "Invalid trigger configuration" in caplog.text + assert ( + "Unnamed automation failed to setup triggers and has been disabled: " + "device does not have trigger ('junk', 'junk')" in caplog.text + ) async def test_exception_bad_trigger(hass, mock_devices, calls, caplog): - """Test for exception on event triggers firing.""" + """Test for exception when validating device triggers.""" zigpy_device, zha_device = mock_devices @@ -369,43 +372,7 @@ async def test_exception_bad_trigger(hass, mock_devices, calls, caplog): }, ) await hass.async_block_till_done() - assert "Invalid trigger configuration" in caplog.text - - -@pytest.mark.skip(reason="Temporarily disabled until automation validation is improved") -async def test_exception_no_device(hass, mock_devices, calls, caplog): - """Test for exception on event triggers firing.""" - - zigpy_device, zha_device = mock_devices - - zigpy_device.device_automation_triggers = { - (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, - (DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE}, - (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}, - (LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD}, - (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, - } - - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": { - "device_id": "no_such_device_id", - "domain": "zha", - "platform": "device", - "type": "junk", - "subtype": "junk", - }, - "action": { - "service": "test.automation", - "data": {"message": "service called"}, - }, - } - ] - }, + assert ( + "Unnamed automation failed to setup triggers and has been disabled: " + "device does not have trigger ('junk', 'junk')" in caplog.text ) - await hass.async_block_till_done() - assert "Invalid trigger configuration" in caplog.text diff --git a/tests/components/zha/test_diagnostics.py b/tests/components/zha/test_diagnostics.py index 807cba507af..741189e56dc 100644 --- a/tests/components/zha/test_diagnostics.py +++ b/tests/components/zha/test_diagnostics.py @@ -31,7 +31,7 @@ CONFIG_ENTRY_DIAGNOSTICS_KEYS = [ @pytest.fixture(autouse=True) def required_platforms_only(): - """Only setup the required platform and required base platforms to speed up tests.""" + """Only set up the required platform and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", (Platform.ALARM_CONTROL_PANEL,) ): diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index e3cb53efcd4..9017b728deb 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -1,4 +1,4 @@ -"""Test zha device discovery.""" +"""Test ZHA device discovery.""" import re from unittest import mock @@ -480,7 +480,7 @@ async def test_device_override( async def test_group_probe_cleanup_called( hass_disable_services, setup_zha, config_entry ): - """Test cleanup happens when zha is unloaded.""" + """Test cleanup happens when ZHA is unloaded.""" await setup_zha() disc.GROUP_PROBE.cleanup = mock.Mock(wraps=disc.GROUP_PROBE.cleanup) await config_entry.async_unload(hass_disable_services) diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index d635072fbbe..87f41e2acc2 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -1,4 +1,4 @@ -"""Test zha fan.""" +"""Test ZHA fan.""" from unittest.mock import AsyncMock, call, patch import pytest @@ -52,7 +52,7 @@ IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" @pytest.fixture(autouse=True) def fan_platform_only(): - """Only setup the fan and required base platforms to speed up tests.""" + """Only set up the fan and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -88,7 +88,7 @@ def zigpy_device(zigpy_device_mock): @pytest.fixture async def coordinator(hass, zigpy_device_mock, zha_device_joined): - """Test zha fan platform.""" + """Test ZHA fan platform.""" zigpy_device = zigpy_device_mock( { @@ -110,7 +110,7 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_fan_1(hass, zigpy_device_mock, zha_device_joined): - """Test zha fan platform.""" + """Test ZHA fan platform.""" zigpy_device = zigpy_device_mock( { @@ -135,7 +135,7 @@ async def device_fan_1(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_fan_2(hass, zigpy_device_mock, zha_device_joined): - """Test zha fan platform.""" + """Test ZHA fan platform.""" zigpy_device = zigpy_device_mock( { @@ -160,7 +160,7 @@ async def device_fan_2(hass, zigpy_device_mock, zha_device_joined): async def test_fan(hass, zha_device_joined_restored, zigpy_device): - """Test zha fan platform.""" + """Test ZHA fan platform.""" zha_device = await zha_device_joined_restored(zigpy_device) cluster = zigpy_device.endpoints.get(1).fan @@ -271,7 +271,7 @@ async def async_set_preset_mode(hass, entity_id, preset_mode=None): new=AsyncMock(return_value=zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]), ) @patch( - "homeassistant.components.zha.entity.UPDATE_GROUP_FROM_CHILD_DELAY", + "homeassistant.components.zha.entity.DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY", new=0, ) async def test_zha_group_fan_entity(hass, device_fan_1, device_fan_2, coordinator): @@ -383,7 +383,7 @@ async def test_zha_group_fan_entity(hass, device_fan_1, device_fan_2, coordinato new=AsyncMock(side_effect=ZigbeeException), ) @patch( - "homeassistant.components.zha.entity.UPDATE_GROUP_FROM_CHILD_DELAY", + "homeassistant.components.zha.entity.DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY", new=0, ) async def test_zha_group_fan_entity_failure_state( @@ -460,7 +460,7 @@ async def test_fan_init( expected_state, expected_percentage, ): - """Test zha fan platform.""" + """Test ZHA fan platform.""" cluster = zigpy_device.endpoints.get(1).fan cluster.PLUGGED_ATTR_READS = plug_read @@ -478,7 +478,7 @@ async def test_fan_update_entity( zha_device_joined_restored, zigpy_device, ): - """Test zha fan platform.""" + """Test ZHA fan platform.""" cluster = zigpy_device.endpoints.get(1).fan cluster.PLUGGED_ATTR_READS = {"fan_mode": 0} @@ -548,7 +548,7 @@ def zigpy_device_ikea(zigpy_device_mock): async def test_fan_ikea(hass, zha_device_joined_restored, zigpy_device_ikea): - """Test zha fan Ikea platform.""" + """Test ZHA fan Ikea platform.""" zha_device = await zha_device_joined_restored(zigpy_device_ikea) cluster = zigpy_device_ikea.endpoints.get(1).ikea_airpurifier entity_id = await find_entity_id(Platform.FAN, zha_device, hass) @@ -635,7 +635,7 @@ async def test_fan_ikea_init( ikea_expected_percentage, ikea_preset_mode, ): - """Test zha fan platform.""" + """Test ZHA fan platform.""" cluster = zigpy_device_ikea.endpoints.get(1).ikea_airpurifier cluster.PLUGGED_ATTR_READS = ikea_plug_read @@ -655,7 +655,7 @@ async def test_fan_ikea_update_entity( zha_device_joined_restored, zigpy_device_ikea, ): - """Test zha fan platform.""" + """Test ZHA fan platform.""" cluster = zigpy_device_ikea.endpoints.get(1).ikea_airpurifier cluster.PLUGGED_ATTR_READS = {"fan_mode": 0} diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 4eb95a8441b..ad095ec3e7e 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -36,7 +36,7 @@ def zigpy_dev_basic(zigpy_device_mock): @pytest.fixture(autouse=True) def required_platform_only(): - """Only setup the required and required base platforms to speed up tests.""" + """Only set up the required and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -60,7 +60,7 @@ async def zha_dev_basic(hass, zha_device_restored, zigpy_dev_basic): @pytest.fixture async def coordinator(hass, zigpy_device_mock, zha_device_joined): - """Test zha light platform.""" + """Test ZHA light platform.""" zigpy_device = zigpy_device_mock( { @@ -82,7 +82,7 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_light_1(hass, zigpy_device_mock, zha_device_joined): - """Test zha light platform.""" + """Test ZHA light platform.""" zigpy_device = zigpy_device_mock( { @@ -107,7 +107,7 @@ async def device_light_1(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_light_2(hass, zigpy_device_mock, zha_device_joined): - """Test zha light platform.""" + """Test ZHA light platform.""" zigpy_device = zigpy_device_mock( { diff --git a/tests/components/zha/test_helpers.py b/tests/components/zha/test_helpers.py index 64f8c732ca9..3c71617c384 100644 --- a/tests/components/zha/test_helpers.py +++ b/tests/components/zha/test_helpers.py @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) @pytest.fixture(autouse=True) def light_platform_only(): - """Only setup the light and required base platforms to speed up tests.""" + """Only set up the light and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index d40605f81dc..c5c8574e8dd 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -1,4 +1,4 @@ -"""Test zha light.""" +"""Test ZHA light.""" from datetime import timedelta from unittest.mock import AsyncMock, call, patch, sentinel @@ -16,6 +16,7 @@ from homeassistant.components.light import ( ) from homeassistant.components.zha.core.const import ( CONF_ALWAYS_PREFER_XY_COLOR_MODE, + CONF_GROUP_MEMBERS_ASSUME_STATE, ZHA_OPTIONS, ) from homeassistant.components.zha.core.group import GroupMember @@ -33,6 +34,7 @@ from .common import ( get_zha_gateway, patch_zha_config, send_attributes_report, + update_attribute_cache, ) from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @@ -86,7 +88,7 @@ LIGHT_COLOR = { @pytest.fixture(autouse=True) def light_platform_only(): - """Only setup the light and required base platforms to speed up tests.""" + """Only set up the light and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -104,7 +106,7 @@ def light_platform_only(): @pytest.fixture async def coordinator(hass, zigpy_device_mock, zha_device_joined): - """Test zha light platform.""" + """Test ZHA light platform.""" zigpy_device = zigpy_device_mock( { @@ -126,7 +128,7 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_light_1(hass, zigpy_device_mock, zha_device_joined): - """Test zha light platform.""" + """Test ZHA light platform.""" zigpy_device = zigpy_device_mock( { @@ -158,7 +160,7 @@ async def device_light_1(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_light_2(hass, zigpy_device_mock, zha_device_joined): - """Test zha light platform.""" + """Test ZHA light platform.""" zigpy_device = zigpy_device_mock( { @@ -191,7 +193,7 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_light_3(hass, zigpy_device_mock, zha_device_joined): - """Test zha light platform.""" + """Test ZHA light platform.""" zigpy_device = zigpy_device_mock( { @@ -252,7 +254,7 @@ async def eWeLink_light(hass, zigpy_device_mock, zha_device_joined): async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored): - """Test zha light platform refresh.""" + """Test ZHA light platform refresh.""" # create zigpy devices zigpy_device = zigpy_device_mock(LIGHT_ON_OFF) @@ -312,7 +314,7 @@ async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored async def test_light( hass, zigpy_device_mock, zha_device_joined_restored, device, reporting ): - """Test zha light platform.""" + """Test ZHA light platform.""" # create zigpy devices zigpy_device = zigpy_device_mock(device) @@ -427,7 +429,7 @@ async def test_light_initialization( config_override, expected_state, ): - """Test zha light initialization with cached attributes and color modes.""" + """Test ZHA light initialization with cached attributes and color modes.""" # create zigpy devices zigpy_device = zigpy_device_mock(LIGHT_COLOR) @@ -1198,6 +1200,151 @@ async def test_transitions( assert eWeLink_state.attributes["max_mireds"] == 500 +@patch( + "zigpy.zcl.clusters.lighting.Color.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@patch( + "zigpy.zcl.clusters.general.LevelControl.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@patch( + "zigpy.zcl.clusters.general.OnOff.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +async def test_on_with_off_color(hass, device_light_1): + """Test turning on the light and sending color commands before on/level commands for supporting lights.""" + + device_1_entity_id = await find_entity_id(Platform.LIGHT, device_light_1, hass) + dev1_cluster_on_off = device_light_1.device.endpoints[1].on_off + dev1_cluster_level = device_light_1.device.endpoints[1].level + dev1_cluster_color = device_light_1.device.endpoints[1].light_color + + # Execute_if_off will override the "enhanced turn on from an off-state" config option that's enabled here + dev1_cluster_color.PLUGGED_ATTR_READS = { + "options": lighting.Color.Options.Execute_if_off + } + update_attribute_cache(dev1_cluster_color) + + # turn on via UI + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_level.request.reset_mock() + dev1_cluster_color.request.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_1_entity_id, + "color_temp": 235, + }, + blocking=True, + ) + + assert dev1_cluster_on_off.request.call_count == 1 + assert dev1_cluster_on_off.request.await_count == 1 + assert dev1_cluster_color.request.call_count == 1 + assert dev1_cluster_color.request.await_count == 1 + assert dev1_cluster_level.request.call_count == 0 + assert dev1_cluster_level.request.await_count == 0 + + assert dev1_cluster_on_off.request.call_args_list[0] == call( + False, + dev1_cluster_on_off.commands_by_name["on"].id, + dev1_cluster_on_off.commands_by_name["on"].schema, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_color.request.call_args == call( + False, + dev1_cluster_color.commands_by_name["move_to_color_temp"].id, + dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, + color_temp_mireds=235, + transition_time=0, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_ON + assert light1_state.attributes["color_temp"] == 235 + assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + # now let's turn off the Execute_if_off option and see if the old behavior is restored + dev1_cluster_color.PLUGGED_ATTR_READS = {"options": 0} + update_attribute_cache(dev1_cluster_color) + + # turn off via UI, so the old "enhanced turn on from an off-state" behavior can do something + await async_test_off_from_hass(hass, dev1_cluster_on_off, device_1_entity_id) + + # turn on via UI (with a different color temp, so the "enhanced turn on" does something) + dev1_cluster_on_off.request.reset_mock() + dev1_cluster_level.request.reset_mock() + dev1_cluster_color.request.reset_mock() + + await hass.services.async_call( + LIGHT_DOMAIN, + "turn_on", + { + "entity_id": device_1_entity_id, + "color_temp": 240, + }, + blocking=True, + ) + + assert dev1_cluster_on_off.request.call_count == 0 + assert dev1_cluster_on_off.request.await_count == 0 + assert dev1_cluster_color.request.call_count == 1 + assert dev1_cluster_color.request.await_count == 1 + assert dev1_cluster_level.request.call_count == 2 + assert dev1_cluster_level.request.await_count == 2 + + # first it comes on with no transition at 2 brightness + assert dev1_cluster_level.request.call_args_list[0] == call( + False, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].id, + dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, + level=2, + transition_time=0, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_color.request.call_args == call( + False, + dev1_cluster_color.commands_by_name["move_to_color_temp"].id, + dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, + color_temp_mireds=240, + transition_time=0, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + assert dev1_cluster_level.request.call_args_list[1] == call( + False, + dev1_cluster_level.commands_by_name["move_to_level"].id, + dev1_cluster_level.commands_by_name["move_to_level"].schema, + level=254, + transition_time=0, + expect_reply=True, + manufacturer=None, + tries=1, + tsn=None, + ) + + light1_state = hass.states.get(device_1_entity_id) + assert light1_state.state == STATE_ON + assert light1_state.attributes["brightness"] == 254 + assert light1_state.attributes["color_temp"] == 240 + assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP + + async def async_test_on_off_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light @@ -1410,7 +1557,7 @@ async def async_test_flash_from_hass(hass, cluster, entity_id, flash): new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), ) @patch( - "homeassistant.components.zha.entity.UPDATE_GROUP_FROM_CHILD_DELAY", + "homeassistant.components.zha.entity.DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY", new=0, ) async def test_zha_group_light_entity( @@ -1632,3 +1779,106 @@ async def test_zha_group_light_entity( await zha_gateway.async_remove_zigpy_group(zha_group.group_id) assert hass.states.get(group_entity_id) is None assert zha_gateway.ha_entity_registry.async_get(group_entity_id) is None + + +@patch( + "zigpy.zcl.clusters.general.OnOff.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@patch( + "homeassistant.components.zha.light.ASSUME_UPDATE_GROUP_FROM_CHILD_DELAY", + new=0, +) +async def test_group_member_assume_state( + hass, + zigpy_device_mock, + zha_device_joined, + coordinator, + device_light_1, + device_light_2, +): + """Test the group members assume state function.""" + with patch_zha_config( + "light", {(ZHA_OPTIONS, CONF_GROUP_MEMBERS_ASSUME_STATE): True} + ): + zha_gateway = get_zha_gateway(hass) + assert zha_gateway is not None + zha_gateway.coordinator_zha_device = coordinator + coordinator._zha_gateway = zha_gateway + device_light_1._zha_gateway = zha_gateway + device_light_2._zha_gateway = zha_gateway + member_ieee_addresses = [device_light_1.ieee, device_light_2.ieee] + members = [ + GroupMember(device_light_1.ieee, 1), + GroupMember(device_light_2.ieee, 1), + ] + + assert coordinator.is_coordinator + + # test creating a group with 2 members + zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members) + await hass.async_block_till_done() + + assert zha_group is not None + assert len(zha_group.members) == 2 + for member in zha_group.members: + assert member.device.ieee in member_ieee_addresses + assert member.group == zha_group + assert member.endpoint is not None + + device_1_entity_id = await find_entity_id(Platform.LIGHT, device_light_1, hass) + device_2_entity_id = await find_entity_id(Platform.LIGHT, device_light_2, hass) + + assert device_1_entity_id != device_2_entity_id + + group_entity_id = async_find_group_entity_id(hass, Platform.LIGHT, zha_group) + assert hass.states.get(group_entity_id) is not None + + assert device_1_entity_id in zha_group.member_entity_ids + assert device_2_entity_id in zha_group.member_entity_ids + + group_cluster_on_off = zha_group.endpoint[general.OnOff.cluster_id] + + await async_enable_traffic( + hass, [device_light_1, device_light_2], enabled=False + ) + await async_wait_for_updates(hass) + # test that the lights were created and that they are unavailable + assert hass.states.get(group_entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and device + await async_enable_traffic(hass, [device_light_1, device_light_2]) + await async_wait_for_updates(hass) + + # test that the lights were created and are off + group_state = hass.states.get(group_entity_id) + assert group_state.state == STATE_OFF + + group_cluster_on_off.request.reset_mock() + await async_shift_time(hass) + + # turn on via UI + await hass.services.async_call( + LIGHT_DOMAIN, "turn_on", {"entity_id": group_entity_id}, blocking=True + ) + + # members also instantly assume STATE_ON + assert hass.states.get(device_1_entity_id).state == STATE_ON + assert hass.states.get(device_2_entity_id).state == STATE_ON + assert hass.states.get(group_entity_id).state == STATE_ON + + # turn off via UI + await hass.services.async_call( + LIGHT_DOMAIN, "turn_off", {"entity_id": group_entity_id}, blocking=True + ) + + # members also instantly assume STATE_OFF + assert hass.states.get(device_1_entity_id).state == STATE_OFF + assert hass.states.get(device_2_entity_id).state == STATE_OFF + assert hass.states.get(group_entity_id).state == STATE_OFF + + # remove the group and ensure that there is no entity and that the entity registry is cleaned up + assert zha_gateway.ha_entity_registry.async_get(group_entity_id) is not None + await zha_gateway.async_remove_zigpy_group(zha_group.group_id) + assert hass.states.get(group_entity_id) is None + assert zha_gateway.ha_entity_registry.async_get(group_entity_id) is None diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 08b720b2ad7..02b5d35ae4d 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -1,4 +1,4 @@ -"""Test zha lock.""" +"""Test ZHA lock.""" from unittest.mock import patch import pytest @@ -29,7 +29,7 @@ SET_USER_STATUS = 9 @pytest.fixture(autouse=True) def lock_platform_only(): - """Only setup the lock and required base platforms to speed up tests.""" + """Only set up the lock and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -60,7 +60,7 @@ async def lock(hass, zigpy_device_mock, zha_device_joined_restored): async def test_lock(hass, lock): - """Test zha lock platform.""" + """Test ZHA lock platform.""" zha_device, cluster = lock entity_id = await find_entity_id(Platform.LOCK, zha_device, hass) diff --git a/tests/components/zha/test_logbook.py b/tests/components/zha/test_logbook.py index 373a48c2d47..97a12689c3d 100644 --- a/tests/components/zha/test_logbook.py +++ b/tests/components/zha/test_logbook.py @@ -33,7 +33,7 @@ DOWN = "down" @pytest.fixture(autouse=True) def sensor_platform_only(): - """Only setup the sensor and required base platforms to speed up tests.""" + """Only set up the sensor and required base platforms to speed up tests.""" with patch("homeassistant.components.zha.PLATFORMS", (Platform.SENSOR,)): yield @@ -60,7 +60,7 @@ async def mock_devices(hass, zigpy_device_mock, zha_device_joined): async def test_zha_logbook_event_device_with_triggers(hass, mock_devices): - """Test zha logbook events with device and triggers.""" + """Test ZHA logbook events with device and triggers.""" zigpy_device, zha_device = mock_devices @@ -145,7 +145,7 @@ async def test_zha_logbook_event_device_with_triggers(hass, mock_devices): async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): - """Test zha logbook events with device and without triggers.""" + """Test ZHA logbook events with device and without triggers.""" zigpy_device, zha_device = mock_devices ieee_address = str(zha_device.ieee) @@ -232,7 +232,7 @@ async def test_zha_logbook_event_device_no_triggers(hass, mock_devices): async def test_zha_logbook_event_device_no_device(hass, mock_devices): - """Test zha logbook events without device and without triggers.""" + """Test ZHA logbook events without device and without triggers.""" hass.config.components.add("recorder") assert await async_setup_component(hass, "logbook", {}) diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index 219c77f76d7..483ede70988 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -1,4 +1,4 @@ -"""Test zha analog output.""" +"""Test ZHA analog output.""" from unittest.mock import call, patch import pytest @@ -28,7 +28,7 @@ from tests.common import mock_coro @pytest.fixture(autouse=True) def number_platform_only(): - """Only setup the number and required base platforms to speed up tests.""" + """Only set up the number and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -83,7 +83,7 @@ async def light(zigpy_device_mock): async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_device): - """Test zha number platform.""" + """Test ZHA number platform.""" cluster = zigpy_analog_output_device.endpoints.get(1).analog_output cluster.PLUGGED_ATTR_READS = { @@ -200,7 +200,7 @@ async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_devi async def test_level_control_number( hass, light, zha_device_joined, attr, initial_value, new_value ): - """Test zha level control number entities - new join.""" + """Test ZHA level control number entities - new join.""" entity_registry = er.async_get(hass) level_control_cluster = light.endpoints[1].level @@ -333,7 +333,7 @@ async def test_level_control_number( async def test_color_number( hass, light, zha_device_joined, attr, initial_value, new_value ): - """Test zha color number entities - new join.""" + """Test ZHA color number entities - new join.""" entity_registry = er.async_get(hass) color_cluster = light.endpoints[1].light_color @@ -358,6 +358,7 @@ async def test_color_number( "color_temp_physical_max", "color_capabilities", "start_up_color_temperature", + "options", ], allow_cache=True, only_cache=False, diff --git a/tests/components/zha/test_radio_manager.py b/tests/components/zha/test_radio_manager.py index 671f831fcce..7eb3cddcfe7 100644 --- a/tests/components/zha/test_radio_manager.py +++ b/tests/components/zha/test_radio_manager.py @@ -105,7 +105,7 @@ async def test_migrate_matching_port( mock_connect_zigpy_app, ) -> None: """Test automatic migration.""" - # Setup the config entry + # Set up the config entry config_entry = MockConfigEntry( data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, domain=DOMAIN, @@ -165,7 +165,7 @@ async def test_migrate_matching_port_usb( mock_connect_zigpy_app, ) -> None: """Test automatic migration.""" - # Setup the config entry + # Set up the config entry config_entry = MockConfigEntry( data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, domain=DOMAIN, @@ -212,7 +212,7 @@ async def test_migrate_matching_port_config_entry_not_loaded( mock_connect_zigpy_app, ) -> None: """Test automatic migration.""" - # Setup the config entry + # Set up the config entry config_entry = MockConfigEntry( data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, domain=DOMAIN, @@ -272,7 +272,7 @@ async def test_migrate_matching_port_retry( mock_connect_zigpy_app, ) -> None: """Test automatic migration.""" - # Setup the config entry + # Set up the config entry config_entry = MockConfigEntry( data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, domain=DOMAIN, @@ -329,7 +329,7 @@ async def test_migrate_non_matching_port( mock_connect_zigpy_app, ) -> None: """Test automatic migration.""" - # Setup the config entry + # Set up the config entry config_entry = MockConfigEntry( data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, domain=DOMAIN, diff --git a/tests/components/zha/test_select.py b/tests/components/zha/test_select.py index e9a7f476efb..494d796bc7c 100644 --- a/tests/components/zha/test_select.py +++ b/tests/components/zha/test_select.py @@ -19,7 +19,7 @@ from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE @pytest.fixture(autouse=True) def select_select_only(): - """Only setup the select and required base platforms to speed up tests.""" + """Only set up the select and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -109,7 +109,7 @@ def core_rs(hass_storage): async def test_select(hass, siren): - """Test zha select platform.""" + """Test ZHA select platform.""" entity_registry = er.async_get(hass) zha_device, cluster = siren @@ -161,7 +161,7 @@ async def test_select_restore_state( core_rs, zha_device_restored, ): - """Test zha select entity restore state.""" + """Test ZHA select entity restore state.""" entity_id = "select.fakemanufacturer_fakemodel_default_siren_tone" core_rs(entity_id, state="Burglar") @@ -194,7 +194,7 @@ async def test_select_restore_state( async def test_on_off_select_new_join(hass, light, zha_device_joined): - """Test zha on off select - new join.""" + """Test ZHA on off select - new join.""" entity_registry = er.async_get(hass) on_off_cluster = light.endpoints[1].on_off @@ -253,7 +253,7 @@ async def test_on_off_select_new_join(hass, light, zha_device_joined): async def test_on_off_select_restored(hass, light, zha_device_restored): - """Test zha on off select - restored.""" + """Test ZHA on off select - restored.""" entity_registry = er.async_get(hass) on_off_cluster = light.endpoints[1].on_off @@ -305,7 +305,7 @@ async def test_on_off_select_restored(hass, light, zha_device_restored): async def test_on_off_select_unsupported(hass, light, zha_device_joined_restored): - """Test zha on off select unsupported.""" + """Test ZHA on off select unsupported.""" on_off_cluster = light.endpoints[1].on_off on_off_cluster.add_unsupported_attribute("start_up_on_off") diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index dec065936a1..b8373e4bcae 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -1,4 +1,4 @@ -"""Test zha sensor.""" +"""Test ZHA sensor.""" import math from unittest.mock import patch @@ -18,21 +18,19 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, - ELECTRIC_CURRENT_AMPERE, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, LIGHT_LUX, PERCENTAGE, - POWER_VOLT_AMPERE, - POWER_WATT, - PRESSURE_HPA, STATE_UNAVAILABLE, STATE_UNKNOWN, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - VOLUME_CUBIC_FEET, - VOLUME_CUBIC_METERS, Platform, + UnitOfApparentPower, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfPressure, + UnitOfTemperature, + UnitOfVolume, ) from homeassistant.helpers import restore_state from homeassistant.helpers.entity_component import async_update_entity @@ -53,7 +51,7 @@ ENTITY_ID_PREFIX = "sensor.fakemanufacturer_fakemodel_{}" @pytest.fixture(autouse=True) def sensor_platform_only(): - """Only setup the sensor and required base platforms to speed up tests.""" + """Only set up the sensor and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -114,16 +112,16 @@ async def async_test_humidity(hass, cluster, entity_id): async def async_test_temperature(hass, cluster, entity_id): """Test temperature sensor.""" await send_attributes_report(hass, cluster, {1: 1, 0: 2900, 2: 100}) - assert_state(hass, entity_id, "29.0", TEMP_CELSIUS) + assert_state(hass, entity_id, "29.0", UnitOfTemperature.CELSIUS) async def async_test_pressure(hass, cluster, entity_id): """Test pressure sensor.""" await send_attributes_report(hass, cluster, {1: 1, 0: 1000, 2: 10000}) - assert_state(hass, entity_id, "1000", PRESSURE_HPA) + assert_state(hass, entity_id, "1000", UnitOfPressure.HPA) await send_attributes_report(hass, cluster, {0: 1000, 20: -1, 16: 10000}) - assert_state(hass, entity_id, "1000", PRESSURE_HPA) + assert_state(hass, entity_id, "1000", UnitOfPressure.HPA) async def async_test_illuminance(hass, cluster, entity_id): @@ -159,7 +157,7 @@ async def async_test_smart_energy_summation(hass, cluster, entity_id): await send_attributes_report( hass, cluster, {1025: 1, "current_summ_delivered": 12321, 1026: 100} ) - assert_state(hass, entity_id, "12.32", VOLUME_CUBIC_METERS) + assert_state(hass, entity_id, "12.32", UnitOfVolume.CUBIC_METERS) assert hass.states.get(entity_id).attributes["status"] == "NO_ALARMS" assert hass.states.get(entity_id).attributes["device_type"] == "Electric Metering" assert ( @@ -173,17 +171,17 @@ async def async_test_electrical_measurement(hass, cluster, entity_id): # update divisor cached value await send_attributes_report(hass, cluster, {"ac_power_divisor": 1}) await send_attributes_report(hass, cluster, {0: 1, 1291: 100, 10: 1000}) - assert_state(hass, entity_id, "100", POWER_WATT) + assert_state(hass, entity_id, "100", UnitOfPower.WATT) await send_attributes_report(hass, cluster, {0: 1, 1291: 99, 10: 1000}) - assert_state(hass, entity_id, "99", POWER_WATT) + assert_state(hass, entity_id, "99", UnitOfPower.WATT) await send_attributes_report(hass, cluster, {"ac_power_divisor": 10}) await send_attributes_report(hass, cluster, {0: 1, 1291: 1000, 10: 5000}) - assert_state(hass, entity_id, "100", POWER_WATT) + assert_state(hass, entity_id, "100", UnitOfPower.WATT) await send_attributes_report(hass, cluster, {0: 1, 1291: 99, 10: 5000}) - assert_state(hass, entity_id, "9.9", POWER_WATT) + assert_state(hass, entity_id, "9.9", UnitOfPower.WATT) assert "active_power_max" not in hass.states.get(entity_id).attributes await send_attributes_report(hass, cluster, {0: 1, 0x050D: 88, 10: 5000}) @@ -195,31 +193,31 @@ async def async_test_em_apparent_power(hass, cluster, entity_id): # update divisor cached value await send_attributes_report(hass, cluster, {"ac_power_divisor": 1}) await send_attributes_report(hass, cluster, {0: 1, 0x050F: 100, 10: 1000}) - assert_state(hass, entity_id, "100", POWER_VOLT_AMPERE) + assert_state(hass, entity_id, "100", UnitOfApparentPower.VOLT_AMPERE) await send_attributes_report(hass, cluster, {0: 1, 0x050F: 99, 10: 1000}) - assert_state(hass, entity_id, "99", POWER_VOLT_AMPERE) + assert_state(hass, entity_id, "99", UnitOfApparentPower.VOLT_AMPERE) await send_attributes_report(hass, cluster, {"ac_power_divisor": 10}) await send_attributes_report(hass, cluster, {0: 1, 0x050F: 1000, 10: 5000}) - assert_state(hass, entity_id, "100", POWER_VOLT_AMPERE) + assert_state(hass, entity_id, "100", UnitOfApparentPower.VOLT_AMPERE) await send_attributes_report(hass, cluster, {0: 1, 0x050F: 99, 10: 5000}) - assert_state(hass, entity_id, "9.9", POWER_VOLT_AMPERE) + assert_state(hass, entity_id, "9.9", UnitOfApparentPower.VOLT_AMPERE) async def async_test_em_rms_current(hass, cluster, entity_id): """Test electrical measurement RMS Current sensor.""" await send_attributes_report(hass, cluster, {0: 1, 0x0508: 1234, 10: 1000}) - assert_state(hass, entity_id, "1.2", ELECTRIC_CURRENT_AMPERE) + assert_state(hass, entity_id, "1.2", UnitOfElectricCurrent.AMPERE) await send_attributes_report(hass, cluster, {"ac_current_divisor": 10}) await send_attributes_report(hass, cluster, {0: 1, 0x0508: 236, 10: 1000}) - assert_state(hass, entity_id, "23.6", ELECTRIC_CURRENT_AMPERE) + assert_state(hass, entity_id, "23.6", UnitOfElectricCurrent.AMPERE) await send_attributes_report(hass, cluster, {0: 1, 0x0508: 1236, 10: 1000}) - assert_state(hass, entity_id, "124", ELECTRIC_CURRENT_AMPERE) + assert_state(hass, entity_id, "124", UnitOfElectricCurrent.AMPERE) assert "rms_current_max" not in hass.states.get(entity_id).attributes await send_attributes_report(hass, cluster, {0: 1, 0x050A: 88, 10: 5000}) @@ -230,14 +228,14 @@ async def async_test_em_rms_voltage(hass, cluster, entity_id): """Test electrical measurement RMS Voltage sensor.""" await send_attributes_report(hass, cluster, {0: 1, 0x0505: 1234, 10: 1000}) - assert_state(hass, entity_id, "123", ELECTRIC_POTENTIAL_VOLT) + assert_state(hass, entity_id, "123", UnitOfElectricPotential.VOLT) await send_attributes_report(hass, cluster, {0: 1, 0x0505: 234, 10: 1000}) - assert_state(hass, entity_id, "23.4", ELECTRIC_POTENTIAL_VOLT) + assert_state(hass, entity_id, "23.4", UnitOfElectricPotential.VOLT) await send_attributes_report(hass, cluster, {"ac_voltage_divisor": 100}) await send_attributes_report(hass, cluster, {0: 1, 0x0505: 2236, 10: 1000}) - assert_state(hass, entity_id, "22.4", ELECTRIC_POTENTIAL_VOLT) + assert_state(hass, entity_id, "22.4", UnitOfElectricPotential.VOLT) assert "rms_voltage_max" not in hass.states.get(entity_id).attributes await send_attributes_report(hass, cluster, {0: 1, 0x0507: 888, 10: 5000}) @@ -269,7 +267,7 @@ async def async_test_powerconfiguration2(hass, cluster, entity_id): async def async_test_device_temperature(hass, cluster, entity_id): """Test temperature sensor.""" await send_attributes_report(hass, cluster, {0: 2900}) - assert_state(hass, entity_id, "29.0", TEMP_CELSIUS) + assert_state(hass, entity_id, "29.0", UnitOfTemperature.CELSIUS) @pytest.mark.parametrize( @@ -311,7 +309,7 @@ async def async_test_device_temperature(hass, cluster, entity_id): smartenergy.Metering.cluster_id, "instantaneous_demand", async_test_metering, - 1, + 9, { "demand_formatting": 0xF9, "divisor": 1, @@ -325,7 +323,7 @@ async def async_test_device_temperature(hass, cluster, entity_id): smartenergy.Metering.cluster_id, "summation_delivered", async_test_smart_energy_summation, - 1, + 9, { "demand_formatting": 0xF9, "divisor": 1000, @@ -414,7 +412,7 @@ async def test_sensor( read_plug, unsupported_attrs, ): - """Test zha sensor platform.""" + """Test ZHA sensor platform.""" zigpy_device = zigpy_device_mock( { @@ -517,10 +515,10 @@ def core_rs(hass_storage): @pytest.mark.parametrize( "uom, raw_temp, expected, restore", [ - (TEMP_CELSIUS, 2900, 29, False), - (TEMP_CELSIUS, 2900, 29, True), - (TEMP_FAHRENHEIT, 2900, 84, False), - (TEMP_FAHRENHEIT, 2900, 84, True), + (UnitOfTemperature.CELSIUS, 2900, 29, False), + (UnitOfTemperature.CELSIUS, 2900, 29, True), + (UnitOfTemperature.FAHRENHEIT, 2900, 84, False), + (UnitOfTemperature.FAHRENHEIT, 2900, 84, True), ], ) async def test_temp_uom( @@ -533,14 +531,16 @@ async def test_temp_uom( zigpy_device_mock, zha_device_restored, ): - """Test zha temperature sensor unit of measurement.""" + """Test ZHA temperature sensor unit of measurement.""" entity_id = "sensor.fake1026_fakemodel1026_004f3202_temperature" if restore: core_rs(entity_id, uom, state=(expected - 2)) hass = await hass_ms( - CONF_UNIT_SYSTEM_METRIC if uom == TEMP_CELSIUS else CONF_UNIT_SYSTEM_IMPERIAL + CONF_UNIT_SYSTEM_METRIC + if uom == UnitOfTemperature.CELSIUS + else CONF_UNIT_SYSTEM_IMPERIAL ) zigpy_device = zigpy_device_mock( @@ -717,7 +717,7 @@ async def test_unsupported_attributes_sensor( entity_ids, missing_entity_ids, ): - """Test zha sensor platform.""" + """Test ZHA sensor platform.""" entity_ids = {ENTITY_ID_PREFIX.format(e) for e in entity_ids} missing_entity_ids = {ENTITY_ID_PREFIX.format(e) for e in missing_entity_ids} @@ -753,25 +753,25 @@ async def test_unsupported_attributes_sensor( 1, 12320, "1.23", - VOLUME_CUBIC_METERS, + UnitOfVolume.CUBIC_METERS, ), ( 1, 1232000, "123.20", - VOLUME_CUBIC_METERS, + UnitOfVolume.CUBIC_METERS, ), ( 3, 2340, "0.23", - f"100 {VOLUME_CUBIC_FEET}", + f"100 {UnitOfVolume.CUBIC_FEET}", ), ( 3, 2360, "0.24", - f"100 {VOLUME_CUBIC_FEET}", + f"100 {UnitOfVolume.CUBIC_FEET}", ), ( 8, @@ -783,43 +783,43 @@ async def test_unsupported_attributes_sensor( 0, 9366, "0.937", - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, ), ( 0, 999, "0.1", - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, ), ( 0, 10091, "1.009", - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, ), ( 0, 10099, "1.01", - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, ), ( 0, 100999, "10.1", - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, ), ( 0, 100023, "10.002", - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, ), ( 0, 102456, "10.246", - ENERGY_KILO_WATT_HOUR, + UnitOfEnergy.KILO_WATT_HOUR, ), ), ) @@ -832,7 +832,7 @@ async def test_se_summation_uom( expected_state, expected_uom, ): - """Test zha smart energy summation.""" + """Test ZHA smart energy summation.""" entity_id = ENTITY_ID_PREFIX.format("summation_delivered") zigpy_device = zigpy_device_mock( @@ -886,7 +886,7 @@ async def test_elec_measurement_sensor_type( expected_type, zha_device_joined, ): - """Test zha electrical measurement sensor type.""" + """Test ZHA electrical measurement sensor type.""" entity_id = ENTITY_ID_PREFIX.format("active_power") zigpy_dev = elec_measurement_zigpy_dev @@ -935,7 +935,7 @@ async def test_elec_measurement_skip_unsupported_attribute( elec_measurement_zha_dev, supported_attributes, ): - """Test zha electrical measurement skipping update of unsupported attributes.""" + """Test ZHA electrical measurement skipping update of unsupported attributes.""" entity_id = ENTITY_ID_PREFIX.format("active_power") zha_dev = elec_measurement_zha_dev diff --git a/tests/components/zha/test_siren.py b/tests/components/zha/test_siren.py index 404cdd2ac02..34c98f0da8f 100644 --- a/tests/components/zha/test_siren.py +++ b/tests/components/zha/test_siren.py @@ -30,7 +30,7 @@ from tests.common import async_fire_time_changed, mock_coro @pytest.fixture(autouse=True) def siren_platform_only(): - """Only setup the siren and required base platforms to speed up tests.""" + """Only set up the siren and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 11beec83b9f..f274abdea50 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -1,4 +1,4 @@ -"""Test zha switch.""" +"""Test ZHA switch.""" from unittest.mock import call, patch import pytest @@ -43,7 +43,7 @@ IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" @pytest.fixture(autouse=True) def switch_platform_only(): - """Only setup the switch and required base platforms to speed up tests.""" + """Only set up the switch and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", ( @@ -71,7 +71,7 @@ def zigpy_device(zigpy_device_mock): @pytest.fixture async def coordinator(hass, zigpy_device_mock, zha_device_joined): - """Test zha light platform.""" + """Test ZHA light platform.""" zigpy_device = zigpy_device_mock( { @@ -92,7 +92,7 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_switch_1(hass, zigpy_device_mock, zha_device_joined): - """Test zha switch platform.""" + """Test ZHA switch platform.""" zigpy_device = zigpy_device_mock( { @@ -112,7 +112,7 @@ async def device_switch_1(hass, zigpy_device_mock, zha_device_joined): @pytest.fixture async def device_switch_2(hass, zigpy_device_mock, zha_device_joined): - """Test zha switch platform.""" + """Test ZHA switch platform.""" zigpy_device = zigpy_device_mock( { @@ -131,7 +131,7 @@ async def device_switch_2(hass, zigpy_device_mock, zha_device_joined): async def test_switch(hass, zha_device_joined_restored, zigpy_device): - """Test zha switch platform.""" + """Test ZHA switch platform.""" zha_device = await zha_device_joined_restored(zigpy_device) cluster = zigpy_device.endpoints.get(1).on_off @@ -257,7 +257,7 @@ async def zigpy_device_tuya(hass, zigpy_device_mock, zha_device_joined): @patch( - "homeassistant.components.zha.entity.UPDATE_GROUP_FROM_CHILD_DELAY", + "homeassistant.components.zha.entity.DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY", new=0, ) async def test_zha_group_switch_entity( @@ -297,14 +297,14 @@ async def test_zha_group_switch_entity( await async_enable_traffic(hass, [device_switch_1, device_switch_2], enabled=False) await async_wait_for_updates(hass) - # test that the lights were created and that they are off + # test that the switches were created and that they are off assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and device await async_enable_traffic(hass, [device_switch_1, device_switch_2]) await async_wait_for_updates(hass) - # test that the lights were created and are off + # test that the switches were created and are off assert hass.states.get(entity_id).state == STATE_OFF # turn on from HA @@ -354,30 +354,30 @@ async def test_zha_group_switch_entity( await send_attributes_report(hass, dev2_cluster_on_off, {0: 1}) await async_wait_for_updates(hass) - # test that group light is on + # test that group switch is on assert hass.states.get(entity_id).state == STATE_ON await send_attributes_report(hass, dev1_cluster_on_off, {0: 0}) await async_wait_for_updates(hass) - # test that group light is still on + # test that group switch is still on assert hass.states.get(entity_id).state == STATE_ON await send_attributes_report(hass, dev2_cluster_on_off, {0: 0}) await async_wait_for_updates(hass) - # test that group light is now off + # test that group switch is now off assert hass.states.get(entity_id).state == STATE_OFF await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) await async_wait_for_updates(hass) - # test that group light is now back on + # test that group switch is now back on assert hass.states.get(entity_id).state == STATE_ON async def test_switch_configurable(hass, zha_device_joined_restored, zigpy_device_tuya): - """Test zha configurable switch platform.""" + """Test ZHA configurable switch platform.""" zha_device = await zha_device_joined_restored(zigpy_device_tuya) cluster = zigpy_device_tuya.endpoints.get(1).tuya_manufacturer diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index caa3da9ceef..412007126e6 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -2245,8 +2245,6 @@ DEVICES = [ "sensor.lumi_lumi_plug_maus01_rms_voltage", "sensor.lumi_lumi_plug_maus01_ac_frequency", "sensor.lumi_lumi_plug_maus01_power_factor", - "sensor.lumi_lumi_plug_maus01_analoginput", - "sensor.lumi_lumi_plug_maus01_analoginput_2", "binary_sensor.lumi_lumi_plug_maus01_binaryinput", "switch.lumi_lumi_plug_maus01_switch", "sensor.lumi_lumi_plug_maus01_rssi", @@ -2309,16 +2307,6 @@ DEVICES = [ DEV_SIG_ENT_MAP_CLASS: "LQISensor", DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_lqi", }, - ("sensor", "00:11:22:33:44:55:66:77-2-12"): { - DEV_SIG_CHANNELS: ["analog_input"], - DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_analoginput", - }, - ("sensor", "00:11:22:33:44:55:66:77-3-12"): { - DEV_SIG_CHANNELS: ["analog_input"], - DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_analoginput_2", - }, ("binary_sensor", "00:11:22:33:44:55:66:77-100-15"): { DEV_SIG_CHANNELS: ["binary_input"], DEV_SIG_ENT_MAP_CLASS: "BinaryInput", diff --git a/tests/components/zwave_js/fixtures/config_entry_diagnostics_redacted.json b/tests/components/zwave_js/fixtures/config_entry_diagnostics_redacted.json index 1e68d82d586..dfddc1cb3e0 100644 --- a/tests/components/zwave_js/fixtures/config_entry_diagnostics_redacted.json +++ b/tests/components/zwave_js/fixtures/config_entry_diagnostics_redacted.json @@ -1,1936 +1,1938 @@ -[ - { - "type": "version", - "driverVersion": "8.11.6", - "serverVersion": "1.15.0", - "homeId": "**REDACTED**", - "minSchemaVersion": 0, - "maxSchemaVersion": 15 - }, - { - "type": "result", - "success": true, - "messageId": "api-schema-id", - "result": {} - }, - { - "type": "result", - "success": true, - "messageId": "listen-id", - "result": { - "state": { - "driver": { - "logConfig": { - "enabled": true, - "level": "info", - "logToFile": false, - "filename": "/data/store/zwavejs_%DATE%.log", - "forceConsole": true +{ + "messages": [ + { + "type": "version", + "driverVersion": "8.11.6", + "serverVersion": "1.15.0", + "homeId": "**REDACTED**", + "minSchemaVersion": 0, + "maxSchemaVersion": 15 + }, + { + "type": "result", + "success": true, + "messageId": "api-schema-id", + "result": {} + }, + { + "type": "result", + "success": true, + "messageId": "listen-id", + "result": { + "state": { + "driver": { + "logConfig": { + "enabled": true, + "level": "info", + "logToFile": false, + "filename": "/data/store/zwavejs_%DATE%.log", + "forceConsole": true + }, + "statisticsEnabled": true }, - "statisticsEnabled": true - }, - "controller": { - "libraryVersion": "Z-Wave 6.07", - "type": 1, - "homeId": "**REDACTED**", - "ownNodeId": 1, - "isSecondary": false, - "isUsingHomeIdFromOtherNetwork": false, - "isSISPresent": true, - "wasRealPrimary": true, - "isStaticUpdateController": true, - "isSlave": false, - "serialApiVersion": "1.2", - "manufacturerId": 134, - "productType": 1, - "productId": 90, - "supportedFunctionTypes": [ - 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 16, 17, 18, 19, 20, 21, 22, 23, 28, - 32, 33, 34, 35, 36, 39, 40, 41, 42, 43, 44, 45, 46, 47, 55, 56, 57, - 58, 59, 60, 63, 65, 66, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 79, - 80, 81, 83, 84, 85, 86, 87, 88, 94, 95, 96, 97, 98, 99, 102, 103, - 120, 128, 144, 146, 147, 152, 161, 180, 182, 183, 184, 185, 186, - 189, 190, 191, 208, 209, 210, 211, 212, 238, 239 - ], - "sucNodeId": 1, - "supportsTimers": false, - "isHealNetworkActive": false, - "statistics": { - "messagesTX": 10, - "messagesRX": 734, - "messagesDroppedRX": 0, - "NAK": 0, - "CAN": 0, - "timeoutACK": 0, - "timeoutResponse": 0, - "timeoutCallback": 0, - "messagesDroppedTX": 0 - }, - "inclusionState": 0 - }, - "nodes": [ - { - "nodeId": 1, - "index": 0, - "status": 4, - "ready": true, - "isListening": true, - "isRouting": false, - "isSecure": "unknown", + "controller": { + "libraryVersion": "Z-Wave 6.07", + "type": 1, + "homeId": "**REDACTED**", + "ownNodeId": 1, + "isSecondary": false, + "isUsingHomeIdFromOtherNetwork": false, + "isSISPresent": true, + "wasRealPrimary": true, + "isStaticUpdateController": true, + "isSlave": false, + "serialApiVersion": "1.2", "manufacturerId": 134, - "productId": 90, "productType": 1, - "firmwareVersion": "1.2", - "deviceConfig": { - "filename": "/data/db/devices/0x0086/zw090.json", - "isEmbedded": true, - "manufacturer": "AEON Labs", + "productId": 90, + "supportedFunctionTypes": [ + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 16, 17, 18, 19, 20, 21, 22, 23, + 28, 32, 33, 34, 35, 36, 39, 40, 41, 42, 43, 44, 45, 46, 47, 55, + 56, 57, 58, 59, 60, 63, 65, 66, 68, 69, 70, 71, 72, 73, 74, 75, + 76, 77, 79, 80, 81, 83, 84, 85, 86, 87, 88, 94, 95, 96, 97, 98, + 99, 102, 103, 120, 128, 144, 146, 147, 152, 161, 180, 182, 183, + 184, 185, 186, 189, 190, 191, 208, 209, 210, 211, 212, 238, 239 + ], + "sucNodeId": 1, + "supportsTimers": false, + "isHealNetworkActive": false, + "statistics": { + "messagesTX": 10, + "messagesRX": 734, + "messagesDroppedRX": 0, + "NAK": 0, + "CAN": 0, + "timeoutACK": 0, + "timeoutResponse": 0, + "timeoutCallback": 0, + "messagesDroppedTX": 0 + }, + "inclusionState": 0 + }, + "nodes": [ + { + "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‐Stick 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", - "description": "Z‐Stick Gen5 USB Controller", - "devices": [ + "interviewAttempts": 0, + "endpoints": [ { - "productType": 1, - "productId": 90 - }, - { - "productType": 257, - "productId": 90 - }, - { - "productType": 513, - "productId": 90 + "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": [] } ], - "firmwareVersion": { - "min": "0.0", - "max": "255.255" + "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] }, - "associations": {}, - "paramInformation": { - "_map": {} + "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 }, - "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" - } + "isControllerNode": true, + "keepAwake": false }, - "label": "ZW090", - "interviewAttempts": 0, - "endpoints": [ - { - "nodeId": 1, - "index": 0, - "deviceClass": { - "basic": { - "key": 2, - "label": "Static Controller" + { + "nodeId": 29, + "index": 0, + "status": 4, + "ready": true, + "isListening": false, + "isRouting": true, + "isSecure": true, + "firmwareVersion": "113.22", + "name": "Front Door Lock", + "location": "**REDACTED**", + "interviewAttempts": 0, + "endpoints": [ + { + "nodeId": 29, + "index": 0, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 3, + "label": "Secure Keypad Door Lock" + }, + "mandatorySupportedCCs": [32, 98, 99, 114, 134], + "mandatoryControlledCCs": [] }, - "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 - }, - { - "nodeId": 29, - "index": 0, - "status": 4, - "ready": true, - "isListening": false, - "isRouting": true, - "isSecure": true, - "firmwareVersion": "113.22", - "name": "Front Door Lock", - "location": "**REDACTED**", - "interviewAttempts": 0, - "endpoints": [ - { - "nodeId": 29, - "index": 0, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" - }, - "generic": { - "key": 64, - "label": "Entry Control" - }, - "specific": { - "key": 3, - "label": "Secure Keypad Door Lock" - }, - "mandatorySupportedCCs": [32, 98, 99, 114, 134], - "mandatoryControlledCCs": [] - }, - "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": [ - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "currentMode", - "propertyName": "currentMode", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Current lock mode", - "min": 0, - "max": 255, - "states": { - "0": "Unsecured", - "1": "UnsecuredWithTimeout", - "16": "InsideUnsecured", - "17": "InsideUnsecuredWithTimeout", - "32": "OutsideUnsecured", - "33": "OutsideUnsecuredWithTimeout", - "254": "Unknown", - "255": "Secured" - } - }, - "value": 255 - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "targetMode", - "propertyName": "targetMode", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Target lock mode", - "min": 0, - "max": 255, - "states": { - "0": "Unsecured", - "1": "UnsecuredWithTimeout", - "16": "InsideUnsecured", - "17": "InsideUnsecuredWithTimeout", - "32": "OutsideUnsecured", - "33": "OutsideUnsecuredWithTimeout", - "254": "Unknown", - "255": "Secured" - } - }, - "value": 255 - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "outsideHandlesCanOpenDoor", - "propertyName": "outsideHandlesCanOpenDoor", - "ccVersion": 0, - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Which outside handles can open the door (actual status)" - }, - "value": [false, false, false, false] - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "insideHandlesCanOpenDoor", - "propertyName": "insideHandlesCanOpenDoor", - "ccVersion": 0, - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "Which inside handles can open the door (actual status)" - }, - "value": [false, false, false, false] - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "latchStatus", - "propertyName": "latchStatus", - "ccVersion": 0, - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "The current status of the latch" - }, - "value": "open" - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "boltStatus", - "propertyName": "boltStatus", - "ccVersion": 0, - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "The current status of the bolt" - }, - "value": "locked" - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "doorStatus", - "propertyName": "doorStatus", - "ccVersion": 0, - "metadata": { - "type": "any", - "readable": true, - "writeable": false, - "label": "The current status of the door" - }, - "value": "open" - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "lockTimeout", - "propertyName": "lockTimeout", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Seconds until lock mode times out" + "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 + } + ] } - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "operationType", - "propertyName": "operationType", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Lock operation type", - "min": 0, - "max": 255, - "states": { - "1": "Constant", - "2": "Timed" - } - }, - "value": 1 - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "outsideHandlesCanOpenDoorConfiguration", - "propertyName": "outsideHandlesCanOpenDoorConfiguration", - "ccVersion": 0, - "metadata": { - "type": "any", - "readable": true, - "writeable": true, - "label": "Which outside handles can open the door (configuration)" - }, - "value": [false, false, false, false] - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "insideHandlesCanOpenDoorConfiguration", - "propertyName": "insideHandlesCanOpenDoorConfiguration", - "ccVersion": 0, - "metadata": { - "type": "any", - "readable": true, - "writeable": true, - "label": "Which inside handles can open the door (configuration)" - }, - "value": [false, false, false, false] - }, - { - "endpoint": 0, - "commandClass": 98, - "commandClassName": "Door Lock", - "property": "lockTimeoutConfiguration", - "propertyName": "lockTimeoutConfiguration", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "Duration of timed mode in seconds", - "min": 0, - "max": 65535 - } - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 1, - "propertyName": "userIdStatus", - "propertyKeyName": "1", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (1)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 1 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 1, - "propertyName": "userCode", - "propertyKeyName": "1", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (1)", - "minLength": 4, - "maxLength": 10 - }, - "value": "**REDACTED**" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 2, - "propertyName": "userIdStatus", - "propertyKeyName": "2", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (2)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 1 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 2, - "propertyName": "userCode", - "propertyKeyName": "2", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (2)", - "minLength": 4, - "maxLength": 10 - }, - "value": "**REDACTED**" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 3, - "propertyName": "userIdStatus", - "propertyKeyName": "3", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (3)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 1 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 3, - "propertyName": "userCode", - "propertyKeyName": "3", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (3)", - "minLength": 4, - "maxLength": 10 - }, - "value": "**REDACTED**" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 4, - "propertyName": "userIdStatus", - "propertyKeyName": "4", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (4)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 4, - "propertyName": "userCode", - "propertyKeyName": "4", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (4)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 5, - "propertyName": "userIdStatus", - "propertyKeyName": "5", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (5)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 5, - "propertyName": "userCode", - "propertyKeyName": "5", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (5)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 6, - "propertyName": "userIdStatus", - "propertyKeyName": "6", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (6)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 6, - "propertyName": "userCode", - "propertyKeyName": "6", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (6)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 7, - "propertyName": "userIdStatus", - "propertyKeyName": "7", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (7)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 7, - "propertyName": "userCode", - "propertyKeyName": "7", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (7)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 8, - "propertyName": "userIdStatus", - "propertyKeyName": "8", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (8)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 8, - "propertyName": "userCode", - "propertyKeyName": "8", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (8)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 9, - "propertyName": "userIdStatus", - "propertyKeyName": "9", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (9)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 9, - "propertyName": "userCode", - "propertyKeyName": "9", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (9)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 10, - "propertyName": "userIdStatus", - "propertyKeyName": "10", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (10)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 10, - "propertyName": "userCode", - "propertyKeyName": "10", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (10)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 11, - "propertyName": "userIdStatus", - "propertyKeyName": "11", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (11)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 11, - "propertyName": "userCode", - "propertyKeyName": "11", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (11)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 12, - "propertyName": "userIdStatus", - "propertyKeyName": "12", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (12)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 12, - "propertyName": "userCode", - "propertyKeyName": "12", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (12)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 13, - "propertyName": "userIdStatus", - "propertyKeyName": "13", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (13)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 13, - "propertyName": "userCode", - "propertyKeyName": "13", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (13)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 14, - "propertyName": "userIdStatus", - "propertyKeyName": "14", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (14)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 14, - "propertyName": "userCode", - "propertyKeyName": "14", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (14)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 15, - "propertyName": "userIdStatus", - "propertyKeyName": "15", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (15)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 15, - "propertyName": "userCode", - "propertyKeyName": "15", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (15)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 16, - "propertyName": "userIdStatus", - "propertyKeyName": "16", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (16)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 16, - "propertyName": "userCode", - "propertyKeyName": "16", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (16)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 17, - "propertyName": "userIdStatus", - "propertyKeyName": "17", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (17)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 17, - "propertyName": "userCode", - "propertyKeyName": "17", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (17)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 18, - "propertyName": "userIdStatus", - "propertyKeyName": "18", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (18)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 18, - "propertyName": "userCode", - "propertyKeyName": "18", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (18)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 19, - "propertyName": "userIdStatus", - "propertyKeyName": "19", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (19)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 19, - "propertyName": "userCode", - "propertyKeyName": "19", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (19)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 20, - "propertyName": "userIdStatus", - "propertyKeyName": "20", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (20)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 20, - "propertyName": "userCode", - "propertyKeyName": "20", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (20)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 21, - "propertyName": "userIdStatus", - "propertyKeyName": "21", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (21)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 21, - "propertyName": "userCode", - "propertyKeyName": "21", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (21)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 22, - "propertyName": "userIdStatus", - "propertyKeyName": "22", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (22)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 22, - "propertyName": "userCode", - "propertyKeyName": "22", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (22)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 23, - "propertyName": "userIdStatus", - "propertyKeyName": "23", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (23)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 23, - "propertyName": "userCode", - "propertyKeyName": "23", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (23)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 24, - "propertyName": "userIdStatus", - "propertyKeyName": "24", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (24)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 24, - "propertyName": "userCode", - "propertyKeyName": "24", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (24)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 25, - "propertyName": "userIdStatus", - "propertyKeyName": "25", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (25)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 25, - "propertyName": "userCode", - "propertyKeyName": "25", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (25)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 26, - "propertyName": "userIdStatus", - "propertyKeyName": "26", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (26)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 26, - "propertyName": "userCode", - "propertyKeyName": "26", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (26)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 28, - "propertyName": "userIdStatus", - "propertyKeyName": "28", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (28)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 28, - "propertyName": "userCode", - "propertyKeyName": "28", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (28)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 29, - "propertyName": "userIdStatus", - "propertyKeyName": "29", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (29)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 29, - "propertyName": "userCode", - "propertyKeyName": "29", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (29)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userIdStatus", - "propertyKey": 30, - "propertyName": "userIdStatus", - "propertyKeyName": "30", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": true, - "label": "User ID status (30)", - "states": { - "0": "Available", - "1": "Enabled", - "2": "Disabled" - } - }, - "value": 0 - }, - { - "endpoint": 0, - "commandClass": 99, - "commandClassName": "User Code", - "property": "userCode", - "propertyKey": 30, - "propertyName": "userCode", - "propertyKeyName": "30", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": true, - "label": "User Code (30)", - "minLength": 4, - "maxLength": 10 - }, - "value": "" - }, - { - "endpoint": 0, - "commandClass": 113, - "commandClassName": "Notification", - "property": "Access Control", - "propertyKey": "Lock state", - "propertyName": "Access Control", - "propertyKeyName": "Lock state", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Lock state", - "ccSpecific": { - "notificationType": 6 + ], + "values": [ + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "currentMode", + "propertyName": "currentMode", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current lock mode", + "min": 0, + "max": 255, + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } }, - "min": 0, - "max": 255, - "states": { - "0": "idle", - "11": "Lock jammed" - } + "value": 255 }, - "value": 11 - }, - { - "endpoint": 0, - "commandClass": 113, - "commandClassName": "Notification", - "property": "Access Control", - "propertyKey": "Keypad state", - "propertyName": "Access Control", - "propertyKeyName": "Keypad state", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Keypad state", - "ccSpecific": { - "notificationType": 6 + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "targetMode", + "propertyName": "targetMode", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target lock mode", + "min": 0, + "max": 255, + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } }, - "min": 0, - "max": 255, - "states": { - "0": "idle", - "16": "Keypad temporary disabled" + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "outsideHandlesCanOpenDoor", + "propertyName": "outsideHandlesCanOpenDoor", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which outside handles can open the door (actual status)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "insideHandlesCanOpenDoor", + "propertyName": "insideHandlesCanOpenDoor", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which inside handles can open the door (actual status)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "latchStatus", + "propertyName": "latchStatus", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the latch" + }, + "value": "open" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "boltStatus", + "propertyName": "boltStatus", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the bolt" + }, + "value": "locked" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "doorStatus", + "propertyName": "doorStatus", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the door" + }, + "value": "open" + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "lockTimeout", + "propertyName": "lockTimeout", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Seconds until lock mode times out" } }, - "value": 16 - }, - { - "endpoint": 0, - "commandClass": 113, - "commandClassName": "Notification", - "property": "alarmType", - "propertyName": "alarmType", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Alarm Type", - "min": 0, - "max": 255 - } - }, - { - "endpoint": 0, - "commandClass": 113, - "commandClassName": "Notification", - "property": "alarmLevel", - "propertyName": "alarmLevel", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Alarm Level", - "min": 0, - "max": 255 - } - }, - { - "endpoint": 0, - "commandClass": 114, - "commandClassName": "Manufacturer Specific", - "property": "manufacturerId", - "propertyName": "manufacturerId", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Manufacturer ID", - "min": 0, - "max": 65535 - } - }, - { - "endpoint": 0, - "commandClass": 114, - "commandClassName": "Manufacturer Specific", - "property": "productType", - "propertyName": "productType", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Product type", - "min": 0, - "max": 65535 - } - }, - { - "endpoint": 0, - "commandClass": 114, - "commandClassName": "Manufacturer Specific", - "property": "productId", - "propertyName": "productId", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Product ID", - "min": 0, - "max": 65535 - } - }, - { - "endpoint": 0, - "commandClass": 128, - "commandClassName": "Battery", - "property": "level", - "propertyName": "level", - "ccVersion": 0, - "metadata": { - "type": "number", - "readable": true, - "writeable": false, - "label": "Battery level", - "min": 0, - "max": 100, - "unit": "%" + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "operationType", + "propertyName": "operationType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Lock operation type", + "min": 0, + "max": 255, + "states": { + "1": "Constant", + "2": "Timed" + } + }, + "value": 1 }, - "value": 89 - }, - { - "endpoint": 0, - "commandClass": 128, - "commandClassName": "Battery", - "property": "isLow", - "propertyName": "isLow", - "ccVersion": 0, - "metadata": { - "type": "boolean", - "readable": true, - "writeable": false, - "label": "Low battery level" + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "outsideHandlesCanOpenDoorConfiguration", + "propertyName": "outsideHandlesCanOpenDoorConfiguration", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which outside handles can open the door (configuration)" + }, + "value": [false, false, false, false] }, - "value": false - }, - { - "endpoint": 0, - "commandClass": 134, - "commandClassName": "Version", - "property": "libraryType", - "propertyName": "libraryType", - "ccVersion": 0, - "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" + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "insideHandlesCanOpenDoorConfiguration", + "propertyName": "insideHandlesCanOpenDoorConfiguration", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which inside handles can open the door (configuration)" + }, + "value": [false, false, false, false] + }, + { + "endpoint": 0, + "commandClass": 98, + "commandClassName": "Door Lock", + "property": "lockTimeoutConfiguration", + "propertyName": "lockTimeoutConfiguration", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Duration of timed mode in seconds", + "min": 0, + "max": 65535 } }, - "value": 6 - }, - { - "endpoint": 0, - "commandClass": 134, - "commandClassName": "Version", - "property": "protocolVersion", - "propertyName": "protocolVersion", - "ccVersion": 0, - "metadata": { - "type": "string", - "readable": true, - "writeable": false, - "label": "Z-Wave protocol version" + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 1, + "propertyName": "userIdStatus", + "propertyKeyName": "1", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (1)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 }, - "value": "3.42" - }, - { - "endpoint": 0, - "commandClass": 134, - "commandClassName": "Version", - "property": "firmwareVersions", - "propertyName": "firmwareVersions", - "ccVersion": 0, - "metadata": { - "type": "string[]", - "readable": true, - "writeable": false, - "label": "Z-Wave chip firmware versions" + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 1, + "propertyName": "userCode", + "propertyKeyName": "1", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (1)", + "minLength": 4, + "maxLength": 10 + }, + "value": "**REDACTED**" }, - "value": ["113.22"] - } - ], - "isFrequentListening": "1000ms", - "maxDataRate": 40000, - "supportedDataRates": [40000], - "protocolVersion": 3, - "supportsBeaming": true, - "supportsSecurity": false, - "nodeType": 1, - "deviceClass": { - "basic": { - "key": 4, - "label": "Routing Slave" + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 2, + "propertyName": "userIdStatus", + "propertyKeyName": "2", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (2)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 2, + "propertyName": "userCode", + "propertyKeyName": "2", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (2)", + "minLength": 4, + "maxLength": 10 + }, + "value": "**REDACTED**" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 3, + "propertyName": "userIdStatus", + "propertyKeyName": "3", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (3)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 3, + "propertyName": "userCode", + "propertyKeyName": "3", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (3)", + "minLength": 4, + "maxLength": 10 + }, + "value": "**REDACTED**" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 4, + "propertyName": "userIdStatus", + "propertyKeyName": "4", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (4)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 4, + "propertyName": "userCode", + "propertyKeyName": "4", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (4)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 5, + "propertyName": "userIdStatus", + "propertyKeyName": "5", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (5)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 5, + "propertyName": "userCode", + "propertyKeyName": "5", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (5)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 6, + "propertyName": "userIdStatus", + "propertyKeyName": "6", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (6)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 6, + "propertyName": "userCode", + "propertyKeyName": "6", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (6)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 7, + "propertyName": "userIdStatus", + "propertyKeyName": "7", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (7)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 7, + "propertyName": "userCode", + "propertyKeyName": "7", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (7)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 8, + "propertyName": "userIdStatus", + "propertyKeyName": "8", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (8)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 8, + "propertyName": "userCode", + "propertyKeyName": "8", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (8)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 9, + "propertyName": "userIdStatus", + "propertyKeyName": "9", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (9)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 9, + "propertyName": "userCode", + "propertyKeyName": "9", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (9)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 10, + "propertyName": "userIdStatus", + "propertyKeyName": "10", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (10)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 10, + "propertyName": "userCode", + "propertyKeyName": "10", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (10)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 11, + "propertyName": "userIdStatus", + "propertyKeyName": "11", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (11)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 11, + "propertyName": "userCode", + "propertyKeyName": "11", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (11)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 12, + "propertyName": "userIdStatus", + "propertyKeyName": "12", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (12)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 12, + "propertyName": "userCode", + "propertyKeyName": "12", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (12)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 13, + "propertyName": "userIdStatus", + "propertyKeyName": "13", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (13)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 13, + "propertyName": "userCode", + "propertyKeyName": "13", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (13)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 14, + "propertyName": "userIdStatus", + "propertyKeyName": "14", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (14)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 14, + "propertyName": "userCode", + "propertyKeyName": "14", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (14)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 15, + "propertyName": "userIdStatus", + "propertyKeyName": "15", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (15)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 15, + "propertyName": "userCode", + "propertyKeyName": "15", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (15)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 16, + "propertyName": "userIdStatus", + "propertyKeyName": "16", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (16)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 16, + "propertyName": "userCode", + "propertyKeyName": "16", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (16)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 17, + "propertyName": "userIdStatus", + "propertyKeyName": "17", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (17)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 17, + "propertyName": "userCode", + "propertyKeyName": "17", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (17)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 18, + "propertyName": "userIdStatus", + "propertyKeyName": "18", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (18)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 18, + "propertyName": "userCode", + "propertyKeyName": "18", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (18)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 19, + "propertyName": "userIdStatus", + "propertyKeyName": "19", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (19)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 19, + "propertyName": "userCode", + "propertyKeyName": "19", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (19)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 20, + "propertyName": "userIdStatus", + "propertyKeyName": "20", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (20)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 20, + "propertyName": "userCode", + "propertyKeyName": "20", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (20)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 21, + "propertyName": "userIdStatus", + "propertyKeyName": "21", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (21)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 21, + "propertyName": "userCode", + "propertyKeyName": "21", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (21)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 22, + "propertyName": "userIdStatus", + "propertyKeyName": "22", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (22)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 22, + "propertyName": "userCode", + "propertyKeyName": "22", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (22)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 23, + "propertyName": "userIdStatus", + "propertyKeyName": "23", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (23)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 23, + "propertyName": "userCode", + "propertyKeyName": "23", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (23)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 24, + "propertyName": "userIdStatus", + "propertyKeyName": "24", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (24)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 24, + "propertyName": "userCode", + "propertyKeyName": "24", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (24)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 25, + "propertyName": "userIdStatus", + "propertyKeyName": "25", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (25)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 25, + "propertyName": "userCode", + "propertyKeyName": "25", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (25)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 26, + "propertyName": "userIdStatus", + "propertyKeyName": "26", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (26)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 26, + "propertyName": "userCode", + "propertyKeyName": "26", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (26)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 28, + "propertyName": "userIdStatus", + "propertyKeyName": "28", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (28)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 28, + "propertyName": "userCode", + "propertyKeyName": "28", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (28)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 29, + "propertyName": "userIdStatus", + "propertyKeyName": "29", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (29)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 29, + "propertyName": "userCode", + "propertyKeyName": "29", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (29)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userIdStatus", + "propertyKey": 30, + "propertyName": "userIdStatus", + "propertyKeyName": "30", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (30)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 99, + "commandClassName": "User Code", + "property": "userCode", + "propertyKey": 30, + "propertyName": "userCode", + "propertyKeyName": "30", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "label": "User Code (30)", + "minLength": 4, + "maxLength": 10 + }, + "value": "" + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Access Control", + "propertyKey": "Lock state", + "propertyName": "Access Control", + "propertyKeyName": "Lock state", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Lock state", + "ccSpecific": { + "notificationType": 6 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "11": "Lock jammed" + } + }, + "value": 11 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Access Control", + "propertyKey": "Keypad state", + "propertyName": "Access Control", + "propertyKeyName": "Keypad state", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Keypad state", + "ccSpecific": { + "notificationType": 6 + }, + "min": 0, + "max": 255, + "states": { + "0": "idle", + "16": "Keypad temporary disabled" + } + }, + "value": 16 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Type", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Alarm Level", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Battery level", + "min": 0, + "max": 100, + "unit": "%" + }, + "value": 89 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 0, + "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": 6 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 0, + "metadata": { + "type": "string", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "3.42" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 0, + "metadata": { + "type": "string[]", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["113.22"] + } + ], + "isFrequentListening": "1000ms", + "maxDataRate": 40000, + "supportedDataRates": [40000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 64, + "label": "Entry Control" + }, + "specific": { + "key": 3, + "label": "Secure Keypad Door Lock" + }, + "mandatorySupportedCCs": [32, 98, 99, 114, 134], + "mandatoryControlledCCs": [] }, - "generic": { - "key": 64, - "label": "Entry Control" + "interviewStage": "Complete", + "statistics": { + "commandsTX": 25, + "commandsRX": 42, + "commandsDroppedRX": 0, + "commandsDroppedTX": 0, + "timeoutResponse": 0 }, - "specific": { - "key": 3, - "label": "Secure Keypad Door Lock" - }, - "mandatorySupportedCCs": [32, 98, 99, 114, 134], - "mandatoryControlledCCs": [] - }, - "interviewStage": "Complete", - "statistics": { - "commandsTX": 25, - "commandsRX": 42, - "commandsDroppedRX": 0, - "commandsDroppedTX": 0, - "timeoutResponse": 0 - }, - "highestSecurityClass": 7, - "isControllerNode": false, - "keepAwake": false - } - ] + "highestSecurityClass": 7, + "isControllerNode": false, + "keepAwake": false + } + ] + } } } - } -] + ] +} diff --git a/tests/conftest.py b/tests/conftest.py index 072ce9e112b..e131cf6cdc3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from collections.abc import AsyncGenerator, Callable, Generator from contextlib import asynccontextmanager +import datetime import functools import gc import itertools @@ -34,7 +35,6 @@ from homeassistant import core as ha, loader, runner, util from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY from homeassistant.auth.models import Credentials from homeassistant.auth.providers import homeassistant, legacy_api_password -from homeassistant.components import mqtt, recorder from homeassistant.components.network.models import Adapter, IPv4ConfiguredAddress from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, @@ -44,7 +44,11 @@ from homeassistant.components.websocket_api.auth import ( from homeassistant.components.websocket_api.http import URL from homeassistant.const import HASSIO_USER_NAME from homeassistant.core import CoreState, HomeAssistant -from homeassistant.helpers import config_entry_oauth2_flow, recorder as recorder_helper +from homeassistant.helpers import ( + config_entry_oauth2_flow, + event, + recorder as recorder_helper, +) from homeassistant.helpers.json import json_loads from homeassistant.helpers.typing import ConfigType from homeassistant.setup import async_setup_component @@ -67,21 +71,24 @@ from tests.common import ( # noqa: E402, isort:skip mock_storage as mock_storage, ) from tests.test_util.aiohttp import mock_aiohttp_client # noqa: E402, isort:skip -from tests.components.recorder.common import ( # noqa: E402, isort:skip - async_recorder_block_till_done, -) _LOGGER = logging.getLogger(__name__) -logging.basicConfig(level=logging.DEBUG) -logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) - asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) # Disable fixtures overriding our beautiful policy asyncio.set_event_loop_policy = lambda policy: None +def _utcnow(): + """Make utcnow patchable by freezegun.""" + return datetime.datetime.now(datetime.timezone.utc) + + +dt_util.utcnow = _utcnow +event.time_tracker_utcnow = _utcnow + + def pytest_addoption(parser): """Register custom pytest options.""" parser.addoption("--dburl", action="store", default="sqlite://") @@ -92,6 +99,11 @@ def pytest_configure(config): config.addinivalue_line( "markers", "no_fail_on_log_exception: mark test to not fail on logged exception" ) + if config.getoption("verbose") > 0: + logging.basicConfig(level=logging.DEBUG) + else: + logging.basicConfig(level=logging.INFO) + logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) def pytest_runtest_setup(): @@ -189,6 +201,26 @@ location.async_detect_location_info = check_real(location.async_detect_location_ util.get_local_ip = lambda: "127.0.0.1" +@pytest.fixture(name="caplog") +def caplog_fixture(caplog): + """Set log level to debug for tests using the caplog fixture.""" + caplog.set_level(logging.DEBUG) + yield caplog + + +@pytest.fixture(autouse=True, scope="module") +def garbage_collection(): + """Run garbage collection at known locations. + + This is to mimic the behavior of pytest-aiohttp, and is + required to avoid warnings during garbage collection from + spilling over into next test case. We run it per module which + handles the most common cases and let each module override + to run per test case if needed. + """ + gc.collect() + + @pytest.fixture(autouse=True) def verify_cleanup(event_loop: asyncio.AbstractEventLoop): """Verify that the test has cleaned up resources correctly.""" @@ -204,10 +236,6 @@ def verify_cleanup(event_loop: asyncio.AbstractEventLoop): inst.stop() pytest.exit(f"Detected non stopped instances ({count}), aborting test run") - threads = frozenset(threading.enumerate()) - threads_before - for thread in threads: - assert isinstance(thread, threading._DummyThread) - # Warn and clean-up lingering tasks and timers # before moving on to the next test. tasks = asyncio.all_tasks(event_loop) - tasks_before @@ -217,16 +245,17 @@ def verify_cleanup(event_loop: asyncio.AbstractEventLoop): if tasks: event_loop.run_until_complete(asyncio.wait(tasks)) - for handle in event_loop._scheduled: # pylint: disable=protected-access + for handle in event_loop._scheduled: if not handle.cancelled(): _LOGGER.warning("Lingering timer after test %r", handle) handle.cancel() - # Make sure garbage collect run in same test as allocation - # this is to mimic the behavior of pytest-aiohttp, and is - # required to avoid warnings from spilling over into next - # test case. - gc.collect() + # Verify no threads where left behind. + threads = frozenset(threading.enumerate()) - threads_before + for thread in threads: + assert isinstance(thread, threading._DummyThread) or thread.name.startswith( + "waitpid-" + ) @pytest.fixture(autouse=True) @@ -455,8 +484,10 @@ def mock_device_tracker_conf(): devices.append(entity) with patch( - "homeassistant.components.device_tracker.legacy" - ".DeviceTracker.async_update_config", + ( + "homeassistant.components.device_tracker.legacy" + ".DeviceTracker.async_update_config" + ), side_effect=mock_update_config, ), patch( "homeassistant.components.device_tracker.legacy.async_load_config", @@ -607,7 +638,7 @@ def current_request(): "GET", "/some/request", headers={"Host": "example.com"}, - sslcontext=ssl.SSLContext(ssl.PROTOCOL_TLS), + sslcontext=ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT), ) mock_request_context.get.return_value = mocked_request yield mock_request_context @@ -725,6 +756,10 @@ async def mqtt_mock( @asynccontextmanager async def _mqtt_mock_entry(hass, mqtt_client_mock, mqtt_config_entry_data): """Fixture to mock a delayed setup of the MQTT config entry.""" + # Local import to avoid processing MQTT modules when running a testcase + # which does not use MQTT. + from homeassistant.components import mqtt + if mqtt_config_entry_data is None: mqtt_config_entry_data = { mqtt.CONF_BROKER: "mock-broker", @@ -944,6 +979,10 @@ def hass_recorder( hass_storage, ): """Home Assistant fixture with in-memory recorder.""" + # Local import to avoid processing recorder and SQLite modules when running a + # testcase which does not use the recorder. + from homeassistant.components import recorder + original_tz = dt_util.DEFAULT_TIME_ZONE hass = get_test_home_assistant() @@ -985,6 +1024,10 @@ def hass_recorder( async def _async_init_recorder_component(hass, add_config=None, db_url=None): """Initialize the recorder asynchronously.""" + # Local import to avoid processing recorder and SQLite modules when running a + # testcase which does not use the recorder. + from homeassistant.components import recorder + config = dict(add_config) if add_config else {} if recorder.CONF_DB_URL not in config: config[recorder.CONF_DB_URL] = db_url @@ -1015,6 +1058,12 @@ async def async_setup_recorder_instance( """Yield callable to setup recorder instance.""" assert not hass_fixture_setup + # Local import to avoid processing recorder and SQLite modules when running a + # testcase which does not use the recorder. + from homeassistant.components import recorder + + from tests.components.recorder.common import async_recorder_block_till_done + nightly = recorder.Recorder.async_nightly_tasks if enable_nightly_purge else None stats = recorder.Recorder.async_periodic_statistics if enable_statistics else None stats_validate = ( diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 65db1291f9d..19682c78b46 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -3280,18 +3280,16 @@ async def test_trigger(hass): async def test_platform_async_validate_condition_config(hass): """Test platform.async_validate_condition_config will be called if it exists.""" config = {CONF_DEVICE_ID: "test", CONF_DOMAIN: "test", CONF_CONDITION: "device"} - platform = AsyncMock() with patch( - "homeassistant.components.device_automation.condition.async_get_device_automation_platform", - return_value=platform, - ): - platform.async_validate_condition_config.return_value = config + "homeassistant.components.device_automation.condition.async_validate_condition_config", + AsyncMock(), + ) as device_automation_validate_condition_mock: await condition.async_validate_condition_config(hass, config) - platform.async_validate_condition_config.assert_awaited() + device_automation_validate_condition_mock.assert_awaited() async def test_disabled_condition(hass: HomeAssistant) -> None: - """Test a disabled condition always passes.""" + """Test a disabled condition returns none.""" config = { "enabled": False, "condition": "state", @@ -3303,8 +3301,138 @@ async def test_disabled_condition(hass: HomeAssistant) -> None: test = await condition.async_from_config(hass, config) hass.states.async_set("binary_sensor.test", "on") - assert test(hass) + assert test(hass) is None # Still passses, condition is not enabled hass.states.async_set("binary_sensor.test", "off") + assert test(hass) is None + + +async def test_and_condition_with_disabled_condition(hass): + """Test the 'and' condition with one of the conditions disabled.""" + config = { + "alias": "And Condition", + "condition": "and", + "conditions": [ + { + "enabled": False, + "condition": "state", + "entity_id": "sensor.temperature", + "state": "100", + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": 110, + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) + + hass.states.async_set("sensor.temperature", 120) + assert not test(hass) + assert_condition_trace( + { + "": [{"result": {"result": False}}], + "conditions/0": [{"result": {"result": None}}], + "conditions/1": [{"result": {"result": False}}], + "conditions/1/entity_id/0": [ + { + "result": { + "result": False, + "wanted_state_below": 110.0, + "state": 120.0, + } + } + ], + } + ) + + hass.states.async_set("sensor.temperature", 105) assert test(hass) + assert_condition_trace( + { + "": [{"result": {"result": True}}], + "conditions/0": [{"result": {"result": None}}], + "conditions/1": [{"result": {"result": True}}], + "conditions/1/entity_id/0": [{"result": {"result": True, "state": 105.0}}], + } + ) + + hass.states.async_set("sensor.temperature", 100) + assert test(hass) + assert_condition_trace( + { + "": [{"result": {"result": True}}], + "conditions/0": [{"result": {"result": None}}], + "conditions/1": [{"result": {"result": True}}], + "conditions/1/entity_id/0": [{"result": {"result": True, "state": 100.0}}], + } + ) + + +async def test_or_condition_with_disabled_condition(hass): + """Test the 'or' condition with one of the conditions disabled.""" + config = { + "alias": "Or Condition", + "condition": "or", + "conditions": [ + { + "enabled": False, + "condition": "state", + "entity_id": "sensor.temperature", + "state": "100", + }, + { + "condition": "numeric_state", + "entity_id": "sensor.temperature", + "below": 110, + }, + ], + } + config = cv.CONDITION_SCHEMA(config) + config = await condition.async_validate_condition_config(hass, config) + test = await condition.async_from_config(hass, config) + + hass.states.async_set("sensor.temperature", 120) + assert not test(hass) + assert_condition_trace( + { + "": [{"result": {"result": False}}], + "conditions/0": [{"result": {"result": None}}], + "conditions/1": [{"result": {"result": False}}], + "conditions/1/entity_id/0": [ + { + "result": { + "result": False, + "state": 120.0, + "wanted_state_below": 110.0, + } + } + ], + } + ) + + hass.states.async_set("sensor.temperature", 105) + assert test(hass) + assert_condition_trace( + { + "": [{"result": {"result": True}}], + "conditions/0": [{"result": {"result": None}}], + "conditions/1": [{"result": {"result": True}}], + "conditions/1/entity_id/0": [{"result": {"result": True, "state": 105.0}}], + } + ) + + hass.states.async_set("sensor.temperature", 100) + assert test(hass) + assert_condition_trace( + { + "": [{"result": {"result": True}}], + "conditions/0": [{"result": {"result": None}}], + "conditions/1": [{"result": {"result": True}}], + "conditions/1/entity_id/0": [{"result": {"result": True, "state": 100.0}}], + } + ) diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index f64525ecdd3..3b94f3d80c1 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -726,3 +726,10 @@ async def test_oauth_session_refresh_failure( session = config_entry_oauth2_flow.OAuth2Session(hass, config_entry, local_impl) with pytest.raises(aiohttp.client_exceptions.ClientResponseError): await session.async_request("post", "https://example.com") + + +async def test_oauth2_without_secret_init(local_impl, hass_client_no_auth): + """Check authorize callback without secret initalizated.""" + client = await hass_client_no_auth() + resp = await client.get("/auth/external/callback?code=abcd&state=qwer") + assert resp.status == 400 diff --git a/tests/helpers/test_discovery.py b/tests/helpers/test_discovery.py index 73376cb8580..3a2d0e7fbac 100644 --- a/tests/helpers/test_discovery.py +++ b/tests/helpers/test_discovery.py @@ -166,7 +166,6 @@ async def test_1st_discovers_2nd_component(hass): async def component1_setup(hass, config): """Set up mock component.""" - print("component1 setup") await discovery.async_discover( hass, "test_component2", {}, "test_component2", {} ) diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index d359efd6325..19f5ea242b6 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -1,5 +1,5 @@ """Test the entity helper.""" -# pylint: disable=protected-access + import asyncio import dataclasses from datetime import timedelta @@ -238,7 +238,7 @@ async def test_async_async_request_call_without_lock(hass): job1 = ent_1.async_request_call(ent_1.testhelper(1)) job2 = ent_2.async_request_call(ent_2.testhelper(2)) - await asyncio.wait([job1, job2]) + await asyncio.gather(job1, job2) while True: if len(updates) >= 2: break diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 5b880831572..55784ec1cf5 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -1,5 +1,5 @@ """The tests for the Entity component helper.""" -# pylint: disable=protected-access + from collections import OrderedDict from datetime import timedelta import logging diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 536ccaaac68..2d215aae887 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -1,5 +1,5 @@ """Test event helpers.""" -# pylint: disable=protected-access + import asyncio from datetime import date, datetime, timedelta from unittest.mock import patch diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index 936940869d6..78319acb2da 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -1,5 +1,5 @@ """Test the frame helper.""" -# pylint: disable=protected-access + from unittest.mock import Mock, patch import pytest diff --git a/tests/helpers/test_icon.py b/tests/helpers/test_icon.py index 033a6cd6b69..12972f230e7 100644 --- a/tests/helpers/test_icon.py +++ b/tests/helpers/test_icon.py @@ -16,7 +16,7 @@ def test_battery_icon(): iconbase = "mdi:battery" for level in range(0, 100, 5): - print( + print( # noqa: T201 "Level: %d. icon: %s, charging: %s" % ( level, diff --git a/tests/helpers/test_init.py b/tests/helpers/test_init.py index 5da6b21a3ff..78032dc2394 100644 --- a/tests/helpers/test_init.py +++ b/tests/helpers/test_init.py @@ -1,5 +1,5 @@ """Test component helpers.""" -# pylint: disable=protected-access + from collections import OrderedDict from homeassistant import helpers diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index e328d30ab7a..14ada0b967d 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -3,8 +3,16 @@ import pytest import voluptuous as vol +from homeassistant.components.switch import SwitchDeviceClass +from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import State -from homeassistant.helpers import config_validation as cv, intent +from homeassistant.helpers import ( + area_registry, + config_validation as cv, + device_registry, + entity_registry, + intent, +) class MockIntentHandler(intent.IntentHandler): @@ -15,13 +23,121 @@ class MockIntentHandler(intent.IntentHandler): self.slot_schema = slot_schema -def test_async_match_state(): +async def test_async_match_states(hass): """Test async_match_state helper.""" - state1 = State("light.kitchen", "on") - state2 = State("switch.kitchen", "on") + areas = area_registry.async_get(hass) + area_kitchen = areas.async_get_or_create("kitchen") + areas.async_update(area_kitchen.id, aliases={"food room"}) + area_bedroom = areas.async_get_or_create("bedroom") - state = intent.async_match_state(None, "kitch", [state1, state2]) - assert state is state1 + state1 = State( + "light.kitchen", "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"} + ) + state2 = State( + "switch.bedroom", "on", attributes={ATTR_FRIENDLY_NAME: "bedroom switch"} + ) + + # Put entities into different areas + entities = entity_registry.async_get(hass) + entities.async_get_or_create("light", "demo", "1234", suggested_object_id="kitchen") + entities.async_update_entity(state1.entity_id, area_id=area_kitchen.id) + + entities.async_get_or_create( + "switch", "demo", "5678", suggested_object_id="bedroom" + ) + entities.async_update_entity( + state2.entity_id, + area_id=area_bedroom.id, + device_class=SwitchDeviceClass.OUTLET, + aliases={"kill switch"}, + ) + + # Match on name + assert [state1] == list( + intent.async_match_states(hass, name="kitchen light", states=[state1, state2]) + ) + + # Test alias + assert [state2] == list( + intent.async_match_states(hass, name="kill switch", states=[state1, state2]) + ) + + # Name + area + assert [state1] == list( + intent.async_match_states( + hass, name="kitchen light", area_name="kitchen", states=[state1, state2] + ) + ) + + # Test area alias + assert [state1] == list( + intent.async_match_states( + hass, name="kitchen light", area_name="food room", states=[state1, state2] + ) + ) + + # Wrong area + assert not list( + intent.async_match_states( + hass, name="kitchen light", area_name="bedroom", states=[state1, state2] + ) + ) + + # Domain + area + assert [state2] == list( + intent.async_match_states( + hass, domains={"switch"}, area_name="bedroom", states=[state1, state2] + ) + ) + + # Device class + area + assert [state2] == list( + intent.async_match_states( + hass, + device_classes={SwitchDeviceClass.OUTLET}, + area_name="bedroom", + states=[state1, state2], + ) + ) + + +async def test_match_device_area(hass): + """Test async_match_state with a device in an area.""" + areas = area_registry.async_get(hass) + area_kitchen = areas.async_get_or_create("kitchen") + area_bedroom = areas.async_get_or_create("bedroom") + + devices = device_registry.async_get(hass) + kitchen_device = devices.async_get_or_create( + config_entry_id="1234", connections=set(), identifiers={("demo", "id-1234")} + ) + devices.async_update_device(kitchen_device.id, area_id=area_kitchen.id) + + state1 = State( + "light.kitchen", "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"} + ) + state2 = State( + "light.bedroom", "on", attributes={ATTR_FRIENDLY_NAME: "bedroom light"} + ) + state3 = State( + "light.living_room", "on", attributes={ATTR_FRIENDLY_NAME: "living room light"} + ) + entities = entity_registry.async_get(hass) + entities.async_get_or_create("light", "demo", "1234", suggested_object_id="kitchen") + entities.async_update_entity(state1.entity_id, device_id=kitchen_device.id) + + entities.async_get_or_create("light", "demo", "5678", suggested_object_id="bedroom") + entities.async_update_entity(state2.entity_id, area_id=area_bedroom.id) + + # Match on area/domain + assert [state1] == list( + intent.async_match_states( + hass, + domains={"light"}, + area_name="kitchen", + states=[state1, state2, state3], + ) + ) def test_async_validate_slots(): @@ -38,21 +154,3 @@ def test_async_validate_slots(): handler1.async_validate_slots( {"name": {"value": "kitchen"}, "probability": {"value": "0.5"}} ) - - -def test_fuzzy_match(): - """Test _fuzzymatch.""" - state1 = State("light.living_room_northwest", "off") - state2 = State("light.living_room_north", "off") - state3 = State("light.living_room_northeast", "off") - state4 = State("light.living_room_west", "off") - state5 = State("light.living_room", "off") - states = [state1, state2, state3, state4, state5] - - state = intent._fuzzymatch("Living Room", states, lambda state: state.name) - assert state == state5 - - state = intent._fuzzymatch( - "Living Room Northwest", states, lambda state: state.name - ) - assert state == state1 diff --git a/tests/helpers/test_json.py b/tests/helpers/test_json.py index 1e85338f152..92583fcfba8 100644 --- a/tests/helpers/test_json.py +++ b/tests/helpers/test_json.py @@ -10,6 +10,7 @@ from homeassistant import core from homeassistant.helpers.json import ( ExtendedJSONEncoder, JSONEncoder, + json_bytes_strip_null, json_dumps, json_dumps_sorted, ) @@ -118,3 +119,19 @@ def test_json_dumps_rgb_color_subclass(): rgb = RGBColor(4, 2, 1) assert json_dumps(rgb) == "[4,2,1]" + + +def test_json_bytes_strip_null(): + """Test stripping nul from strings.""" + + assert json_bytes_strip_null("\0") == b'""' + assert json_bytes_strip_null("silly\0stuff") == b'"silly"' + assert json_bytes_strip_null(["one", "two\0", "three"]) == b'["one","two","three"]' + assert ( + json_bytes_strip_null({"k1": "one", "k2": "two\0", "k3": "three"}) + == b'{"k1":"one","k2":"two","k3":"three"}' + ) + assert ( + json_bytes_strip_null([[{"k1": {"k2": ["silly\0stuff"]}}]]) + == b'[[{"k1":{"k2":["silly"]}}]]' + ) diff --git a/tests/helpers/test_schema_config_entry_flow.py b/tests/helpers/test_schema_config_entry_flow.py index 00935677a21..05e8b8c467c 100644 --- a/tests/helpers/test_schema_config_entry_flow.py +++ b/tests/helpers/test_schema_config_entry_flow.py @@ -28,7 +28,7 @@ from tests.common import MockConfigEntry, mock_platform TEST_DOMAIN = "test" -class TestSchemaConfigFlowHandler(SchemaConfigFlowHandler): +class MockSchemaConfigFlowHandler(SchemaConfigFlowHandler): """Bare minimum SchemaConfigFlowHandler.""" config_flow = {} @@ -128,7 +128,7 @@ async def test_config_flow_advanced_option( } @manager.mock_reg_handler("test") - class TestFlow(TestSchemaConfigFlowHandler): + class TestFlow(MockSchemaConfigFlowHandler): config_flow = CONFIG_FLOW # Start flow in basic mode @@ -222,7 +222,7 @@ async def test_options_flow_advanced_option( "init": SchemaFlowFormStep(OPTIONS_SCHEMA) } - class TestFlow(TestSchemaConfigFlowHandler, domain="test"): + class TestFlow(MockSchemaConfigFlowHandler, domain="test"): config_flow = {} options_flow = OPTIONS_FLOW @@ -326,7 +326,7 @@ async def test_menu_step(hass: HomeAssistant) -> None: "option4": SchemaFlowFormStep(vol.Schema({})), } - class TestConfigFlow(TestSchemaConfigFlowHandler, domain=TEST_DOMAIN): + class TestConfigFlow(MockSchemaConfigFlowHandler, domain=TEST_DOMAIN): """Handle a config or options flow for Derivative.""" config_flow = CONFIG_FLOW @@ -375,7 +375,7 @@ async def test_schema_none(hass: HomeAssistant) -> None: "option3": SchemaFlowFormStep(vol.Schema({})), } - class TestConfigFlow(TestSchemaConfigFlowHandler, domain=TEST_DOMAIN): + class TestConfigFlow(MockSchemaConfigFlowHandler, domain=TEST_DOMAIN): """Handle a config or options flow for Derivative.""" config_flow = CONFIG_FLOW @@ -409,7 +409,7 @@ async def test_last_step(hass: HomeAssistant) -> None: "step3": SchemaFlowFormStep(vol.Schema({}), next_step=None), } - class TestConfigFlow(TestSchemaConfigFlowHandler, domain=TEST_DOMAIN): + class TestConfigFlow(MockSchemaConfigFlowHandler, domain=TEST_DOMAIN): """Handle a config or options flow for Derivative.""" config_flow = CONFIG_FLOW @@ -452,7 +452,7 @@ async def test_next_step_function(hass: HomeAssistant) -> None: "step2": SchemaFlowFormStep(vol.Schema({}), next_step=_step2_next_step), } - class TestConfigFlow(TestSchemaConfigFlowHandler, domain=TEST_DOMAIN): + class TestConfigFlow(MockSchemaConfigFlowHandler, domain=TEST_DOMAIN): """Handle a config or options flow for Derivative.""" config_flow = CONFIG_FLOW @@ -509,7 +509,7 @@ async def test_suggested_values( ), } - class TestFlow(TestSchemaConfigFlowHandler, domain="test"): + class TestFlow(MockSchemaConfigFlowHandler, domain="test"): config_flow = {} options_flow = OPTIONS_FLOW @@ -620,7 +620,7 @@ async def test_options_flow_state(hass: HomeAssistant) -> None: ), } - class TestFlow(TestSchemaConfigFlowHandler, domain="test"): + class TestFlow(MockSchemaConfigFlowHandler, domain="test"): config_flow = {} options_flow = OPTIONS_FLOW diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index a5f3cc0cc91..b44e7b7c458 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1,5 +1,5 @@ """The tests for the Script component.""" -# pylint: disable=protected-access + import asyncio from contextlib import contextmanager from datetime import timedelta @@ -4633,14 +4633,12 @@ async def test_breakpoints_2(hass): async def test_platform_async_validate_action_config(hass): """Test platform.async_validate_action_config will be called if it exists.""" config = {CONF_DEVICE_ID: "test", CONF_DOMAIN: "test"} - platform = AsyncMock() with patch( - "homeassistant.components.device_automation.action.async_get_device_automation_platform", - return_value=platform, - ): - platform.async_validate_action_config.return_value = config + "homeassistant.components.device_automation.action.async_validate_action_config", + return_value=AsyncMock(), + ) as device_automation_validate_action_mock: await script.async_validate_action_config(hass, config) - platform.async_validate_action_config.assert_awaited() + device_automation_validate_action_mock.assert_awaited() async def test_stop_action(hass, caplog): diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 4b4072bd06c..470865be2e3 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -419,6 +419,25 @@ def test_text_selector_schema(schema, valid_selections, invalid_selections): ("red", "green"), ("cat", 0, None, ["red"]), ), + ( + { + "options": ["red", "green", "blue"], + "translation_key": "color", + }, + ("red", "green", "blue"), + ("cat", 0, None, ["red"]), + ), + ( + { + "options": [ + {"value": "red", "label": "Ruby Red"}, + {"value": "green", "label": "Emerald Green"}, + ], + "translation_key": "color", + }, + ("red", "green"), + ("cat", 0, None, ["red"]), + ), ( {"options": ["red", "green", "blue"], "multiple": True}, (["red"], ["green", "blue"], []), diff --git a/tests/helpers/test_sun.py b/tests/helpers/test_sun.py index 84545bf43b6..1cad1b49bdc 100644 --- a/tests/helpers/test_sun.py +++ b/tests/helpers/test_sun.py @@ -1,5 +1,5 @@ """The tests for the Sun helpers.""" -# pylint: disable=protected-access + from datetime import datetime, timedelta from unittest.mock import patch @@ -183,12 +183,6 @@ def test_norway_in_june(hass): june = datetime(2016, 6, 1, tzinfo=dt_util.UTC) - print(sun.get_astral_event_date(hass, SUN_EVENT_SUNRISE, datetime(2017, 7, 25))) - print(sun.get_astral_event_date(hass, SUN_EVENT_SUNSET, datetime(2017, 7, 25))) - - print(sun.get_astral_event_date(hass, SUN_EVENT_SUNRISE, datetime(2017, 7, 26))) - print(sun.get_astral_event_date(hass, SUN_EVENT_SUNSET, datetime(2017, 7, 26))) - assert sun.get_astral_event_next(hass, SUN_EVENT_SUNRISE, june) == datetime( 2016, 7, 24, 22, 59, 45, 689645, tzinfo=dt_util.UTC ) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index e19daf00627..cb5445516e5 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -3672,26 +3672,18 @@ async def test_cache_garbage_collection() -> None: (template_string), ) tpl.ensure_valid() - assert template._NO_HASS_ENV.template_cache.get( - template_string - ) # pylint: disable=protected-access + assert template._NO_HASS_ENV.template_cache.get(template_string) tpl2 = template.Template( (template_string), ) tpl2.ensure_valid() - assert template._NO_HASS_ENV.template_cache.get( - template_string - ) # pylint: disable=protected-access + assert template._NO_HASS_ENV.template_cache.get(template_string) del tpl - assert template._NO_HASS_ENV.template_cache.get( - template_string - ) # pylint: disable=protected-access + assert template._NO_HASS_ENV.template_cache.get(template_string) del tpl2 - assert not template._NO_HASS_ENV.template_cache.get( - template_string - ) # pylint: disable=protected-access + assert not template._NO_HASS_ENV.template_cache.get(template_string) def test_is_template_string() -> None: @@ -4078,3 +4070,33 @@ async def test_template_states_can_serialize(hass: HomeAssistant) -> None: template_state = template.TemplateState(hass, state, True) assert template_state.as_dict() is template_state.as_dict() assert json_dumps(template_state) == json_dumps(template_state) + + +@pytest.mark.parametrize( + "seq, value, expected", + [ + ([0], 0, True), + ([1], 0, False), + ([False], 0, True), + ([True], 0, False), + ([0], [0], False), + (["toto", 1], "toto", True), + (["toto", 1], "tata", False), + ([], 0, False), + ([], None, False), + ], +) +def test_contains(hass, seq, value, expected): + """Test contains.""" + assert ( + template.Template("{{ seq | contains(value) }}", hass).async_render( + {"seq": seq, "value": value} + ) + == expected + ) + assert ( + template.Template("{{ seq is contains(value) }}", hass).async_render( + {"seq": seq, "value": value} + ) + == expected + ) diff --git a/tests/pylint/test_enforce_type_hints.py b/tests/pylint/test_enforce_type_hints.py index 665a4d6594c..80029eb704c 100644 --- a/tests/pylint/test_enforce_type_hints.py +++ b/tests/pylint/test_enforce_type_hints.py @@ -1,5 +1,5 @@ """Tests for pylint hass_enforce_type_hints plugin.""" -# pylint:disable=protected-access + from __future__ import annotations import re diff --git a/tests/pylint/test_imports.py b/tests/pylint/test_imports.py index 5c8bad28902..ffa1b032ca7 100644 --- a/tests/pylint/test_imports.py +++ b/tests/pylint/test_imports.py @@ -1,5 +1,5 @@ """Tests for pylint hass_imports plugin.""" -# pylint:disable=protected-access + from __future__ import annotations import astroid diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index c8365c86a9a..9606d1968c4 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -1,5 +1,5 @@ """Test the bootstrapping.""" -# pylint: disable=protected-access + import asyncio import glob import os diff --git a/tests/test_config.py b/tests/test_config.py index ea9c81eae1a..00b0ae63c0b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,5 +1,5 @@ """Test config utils.""" -# pylint: disable=protected-access + from collections import OrderedDict import contextlib import copy @@ -754,7 +754,6 @@ async def test_async_hass_config_yaml_merge(merge_log_err, hass): assert len(conf["light"]) == 1 -# pylint: disable=redefined-outer-name @pytest.fixture def merge_log_err(hass): """Patch _merge_log_error from packages.""" @@ -1177,9 +1176,9 @@ async def test_component_config_exceptions(hass, caplog): ) == {"test_domain": []} assert "ValueError: broken" in caplog.text assert ( - "Unknown error validating test_platform platform config with test_domain component platform schema" - in caplog.text - ) + "Unknown error validating test_platform platform config " + "with test_domain component platform schema" + ) in caplog.text # platform.PLATFORM_SCHEMA caplog.clear() @@ -1204,8 +1203,8 @@ async def test_component_config_exceptions(hass, caplog): ) == {"test_domain": []} assert "ValueError: broken" in caplog.text assert ( - "Unknown error validating config for test_platform platform for test_domain component with PLATFORM_SCHEMA" - in caplog.text + "Unknown error validating config for test_platform platform for test_domain" + " component with PLATFORM_SCHEMA" in caplog.text ) # get_platform("config") raising @@ -1219,7 +1218,10 @@ async def test_component_config_exceptions(hass, caplog): domain="test_domain", get_platform=Mock( side_effect=ImportError( - "ModuleNotFoundError: No module named 'not_installed_something'", + ( + "ModuleNotFoundError: No module named" + " 'not_installed_something'" + ), name="not_installed_something", ) ), @@ -1228,8 +1230,8 @@ async def test_component_config_exceptions(hass, caplog): is None ) assert ( - "Error importing config platform test_domain: ModuleNotFoundError: No module named 'not_installed_something'" - in caplog.text + "Error importing config platform test_domain: ModuleNotFoundError: No module" + " named 'not_installed_something'" in caplog.text ) # get_component raising diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 994c220adc4..087ccaaae28 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -897,9 +897,10 @@ async def test_setup_raise_not_ready(hass, caplog): assert len(mock_call.mock_calls) == 1 assert ( - "Config entry 'test_title' for test integration not ready yet: The internet connection is offline" - in caplog.text - ) + "Config entry 'test_title' for test integration not ready yet:" + " The internet connection is offline" + ) in caplog.text + p_hass, p_wait_time, p_setup = mock_call.mock_calls[0][1] assert p_hass is hass @@ -932,8 +933,8 @@ async def test_setup_raise_not_ready_from_exception(hass, caplog): assert len(mock_call.mock_calls) == 1 assert ( - "Config entry 'test_title' for test integration not ready yet: The device dropped the connection" - in caplog.text + "Config entry 'test_title' for test integration not ready yet: The device" + " dropped the connection" in caplog.text ) @@ -1428,7 +1429,7 @@ async def test_init_custom_integration(hass): "homeassistant.loader.async_get_integration", return_value=integration, ): - await hass.config_entries.flow.async_init("bla") + await hass.config_entries.flow.async_init("bla", context={"source": "user"}) async def test_support_entry_unload(hass): @@ -2950,8 +2951,8 @@ async def test_setup_not_raise_entry_error_from_future_coordinator_update(hass, await entry.async_setup(hass) await hass.async_block_till_done() assert ( - "Config entry setup failed while fetching any data: Incompatible firmware version" - in caplog.text + "Config entry setup failed while fetching any data: Incompatible firmware" + " version" in caplog.text ) assert entry.state is config_entries.ConfigEntryState.LOADED @@ -3537,3 +3538,29 @@ async def test_options_flow_options_not_mutated() -> None: "sub_list": ["one", "two"], } assert entry.options == {"sub_dict": {"1": "one"}, "sub_list": ["one"]} + + +async def test_initializing_flows_canceled_on_shutdown(hass: HomeAssistant, manager): + """Test that initializing flows are canceled on shutdown.""" + + class MockFlowHandler(config_entries.ConfigFlow): + """Define a mock flow handler.""" + + VERSION = 1 + + async def async_step_reauth(self, data): + """Mock Reauth.""" + await asyncio.sleep(1) + + with patch.dict( + config_entries.HANDLERS, {"comp": MockFlowHandler, "test": MockFlowHandler} + ): + + task = asyncio.create_task( + manager.flow.async_init("test", context={"source": "reauth"}) + ) + await hass.async_block_till_done() + await manager.flow.async_shutdown() + + with pytest.raises(asyncio.exceptions.CancelledError): + await task diff --git a/tests/test_core.py b/tests/test_core.py index 2f8db7fc0d6..765c33cef40 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,7 +1,6 @@ """Test to verify that Home Assistant core works.""" from __future__ import annotations -# pylint: disable=protected-access import array import asyncio from datetime import datetime, timedelta @@ -400,6 +399,58 @@ def test_state_as_dict(): assert state.as_dict() is as_dict_1 +def test_state_as_compressed_state(): + """Test a State as compressed state.""" + last_time = datetime(1984, 12, 8, 12, 0, 0, tzinfo=dt_util.UTC) + state = ha.State( + "happy.happy", + "on", + {"pig": "dog"}, + last_updated=last_time, + last_changed=last_time, + ) + expected = { + "a": {"pig": "dog"}, + "c": state.context.id, + "lc": last_time.timestamp(), + "s": "on", + } + as_compressed_state = state.as_compressed_state() + # We are not too concerned about these being ReadOnlyDict + # since we don't expect them to be called by external callers + assert as_compressed_state == expected + # 2nd time to verify cache + assert state.as_compressed_state() == expected + assert state.as_compressed_state() is as_compressed_state + + +def test_state_as_compressed_state_unique_last_updated(): + """Test a State as compressed state where last_changed is not last_updated.""" + last_changed = datetime(1984, 12, 8, 11, 0, 0, tzinfo=dt_util.UTC) + last_updated = datetime(1984, 12, 8, 12, 0, 0, tzinfo=dt_util.UTC) + state = ha.State( + "happy.happy", + "on", + {"pig": "dog"}, + last_updated=last_updated, + last_changed=last_changed, + ) + expected = { + "a": {"pig": "dog"}, + "c": state.context.id, + "lc": last_changed.timestamp(), + "lu": last_updated.timestamp(), + "s": "on", + } + as_compressed_state = state.as_compressed_state() + # We are not too concerned about these being ReadOnlyDict + # since we don't expect them to be called by external callers + assert as_compressed_state == expected + # 2nd time to verify cache + assert state.as_compressed_state() == expected + assert state.as_compressed_state() is as_compressed_state + + async def test_eventbus_add_remove_listener(hass): """Test remove_listener method.""" old_count = len(hass.bus.async_listeners()) @@ -670,8 +721,7 @@ def test_state_repr(): datetime(1984, 12, 8, 12, 0, 0), ) ) - == "" + == "" ) @@ -1128,7 +1178,12 @@ async def test_service_executed_with_subservices(hass): call2 = hass.services.async_call( "test", "inner", blocking=True, context=call.context ) - await asyncio.wait([call1, call2]) + await asyncio.wait( + [ + hass.async_create_task(call1), + hass.async_create_task(call2), + ] + ) calls.append(call) hass.services.async_register("test", "outer", handle_outer) diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index f0bcd2b5fd6..b39635e0ca5 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -1,5 +1,4 @@ """Test the flow classes.""" -import asyncio import logging from unittest.mock import Mock, patch @@ -181,7 +180,7 @@ async def test_abort_calls_async_remove_with_exception(manager, caplog): with caplog.at_level(logging.ERROR): await manager.async_init("test") - assert "Error removing test config flow: error" in caplog.text + assert "Error removing test flow: error" in caplog.text TestFlow.async_remove.assert_called_once() @@ -419,22 +418,6 @@ async def test_abort_flow_exception(manager): assert form["description_placeholders"] == {"placeholder": "yo"} -async def test_initializing_flows_canceled_on_shutdown(hass, manager): - """Test that initializing flows are canceled on shutdown.""" - - @manager.mock_reg_handler("test") - class TestFlow(data_entry_flow.FlowHandler): - async def async_step_init(self, user_input=None): - await asyncio.sleep(1) - - task = asyncio.create_task(manager.async_init("test")) - await hass.async_block_till_done() - await manager.async_shutdown() - - with pytest.raises(asyncio.exceptions.CancelledError): - await task - - async def test_init_unknown_flow(manager): """Test that UnknownFlow is raised when async_create_flow returns None.""" diff --git a/tests/test_loader.py b/tests/test_loader.py index da788e0db75..b3ba5d29724 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -24,21 +24,13 @@ async def test_component_dependencies(hass): mock_integration(hass, MockModule("mod1", ["mod3"])) with pytest.raises(loader.CircularDependency): - print( - await loader._async_component_dependencies( - hass, "mod_3", mod_3, set(), set() - ) - ) + await loader._async_component_dependencies(hass, "mod_3", mod_3, set(), set()) # Depend on non-existing component mod_1 = mock_integration(hass, MockModule("mod1", ["nonexisting"])) with pytest.raises(loader.IntegrationNotFound): - print( - await loader._async_component_dependencies( - hass, "mod_1", mod_1, set(), set() - ) - ) + await loader._async_component_dependencies(hass, "mod_1", mod_1, set(), set()) # Having an after dependency 2 deps down that is circular mod_1 = mock_integration( @@ -46,11 +38,7 @@ async def test_component_dependencies(hass): ) with pytest.raises(loader.CircularDependency): - print( - await loader._async_component_dependencies( - hass, "mod_3", mod_3, set(), set() - ) - ) + await loader._async_component_dependencies(hass, "mod_3", mod_3, set(), set()) def test_component_loader(hass): @@ -138,16 +126,16 @@ async def test_custom_integration_version_not_valid( await loader.async_get_integration(hass, "test_no_version") assert ( - "The custom integration 'test_no_version' does not have a version key in the manifest file and was blocked from loading." - in caplog.text - ) + "The custom integration 'test_no_version' does not have a version key in the" + " manifest file and was blocked from loading." + ) in caplog.text with pytest.raises(loader.IntegrationNotFound): await loader.async_get_integration(hass, "test2") assert ( - "The custom integration 'test_bad_version' does not have a valid version key (bad) in the manifest file and was blocked from loading." - in caplog.text - ) + "The custom integration 'test_bad_version' does not have a valid version key" + " (bad) in the manifest file and was blocked from loading." + ) in caplog.text async def test_get_integration(hass): diff --git a/tests/test_setup.py b/tests/test_setup.py index fc07c68bad2..82878ab217c 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -1,5 +1,5 @@ """Test component/platform setup.""" -# pylint: disable=protected-access + import asyncio import datetime import threading diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 4ed81a3a577..bd77ca2b5b9 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -266,7 +266,7 @@ class AiohttpClientMockResponse: raise ClientResponseError( request_info=request_info, history=None, - code=self.status, + status=self.status, headers=self.headers, ) diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index 9b3911313a6..55ce53c9702 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -98,6 +98,11 @@ class MockSensor(MockEntity, SensorEntity): """Return the last_reset of this sensor.""" return self._handle("last_reset") + @property + def native_precision(self): + """Return the number of digits after the decimal point.""" + return self._handle("native_precision") + @property def native_unit_of_measurement(self): """Return the native unit_of_measurement of this sensor.""" diff --git a/tests/testing_config/custom_sentences/en/beer.yaml b/tests/testing_config/custom_sentences/en/beer.yaml new file mode 100644 index 00000000000..cedaae42ed1 --- /dev/null +++ b/tests/testing_config/custom_sentences/en/beer.yaml @@ -0,0 +1,11 @@ +language: "en" +intents: + OrderBeer: + data: + - sentences: + - "I'd like to order a {beer_style} [please]" +lists: + beer_style: + values: + - "stout" + - "lager" diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index a1fd8440971..7bb1613761a 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -7,7 +7,6 @@ from unittest.mock import patch import pytest -from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE from homeassistant.core import callback, is_callback import homeassistant.util.logging as logging_util @@ -66,17 +65,16 @@ async def test_logging_with_queue_handler(): async def test_migrate_log_handler(hass): """Test migrating log handlers.""" - original_handlers = logging.root.handlers - logging_util.async_activate_log_queue_handler(hass) assert len(logging.root.handlers) == 1 assert isinstance(logging.root.handlers[0], logging_util.HomeAssistantQueueHandler) - hass.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE) - await hass.async_block_till_done() - - assert logging.root.handlers == original_handlers + # Test that the close hook shuts down the queue handler's thread + listener_thread = logging.root.handlers[0].listener._thread + assert listener_thread.is_alive() + logging.root.handlers[0].close() + assert not listener_thread.is_alive() @pytest.mark.no_fail_on_log_exception diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index 83aaf6224b5..648db8420d3 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -1,9 +1,15 @@ -"""Test Home Assistant eneergy utility functions.""" +"""Test Home Assistant unit conversion utility functions.""" +from __future__ import annotations + +import inspect + import pytest from homeassistant.const import ( + PERCENTAGE, UnitOfDataRate, UnitOfElectricCurrent, + UnitOfElectricPotential, UnitOfEnergy, UnitOfInformation, UnitOfLength, @@ -16,11 +22,13 @@ from homeassistant.const import ( UnitOfVolumetricFlux, ) from homeassistant.exceptions import HomeAssistantError +from homeassistant.util import unit_conversion from homeassistant.util.unit_conversion import ( BaseUnitConverter, DataRateConverter, DistanceConverter, ElectricCurrentConverter, + ElectricPotentialConverter, EnergyConverter, InformationConverter, MassConverter, @@ -28,185 +36,77 @@ from homeassistant.util.unit_conversion import ( PressureConverter, SpeedConverter, TemperatureConverter, + UnitlessRatioConverter, VolumeConverter, ) INVALID_SYMBOL = "bob" -@pytest.mark.parametrize( - "converter,valid_unit", - [ - (DataRateConverter, UnitOfDataRate.GIBIBYTES_PER_SECOND), - (DistanceConverter, UnitOfLength.KILOMETERS), - (DistanceConverter, UnitOfLength.METERS), - (DistanceConverter, UnitOfLength.CENTIMETERS), - (DistanceConverter, UnitOfLength.MILLIMETERS), - (DistanceConverter, UnitOfLength.MILES), - (DistanceConverter, UnitOfLength.YARDS), - (DistanceConverter, UnitOfLength.FEET), - (DistanceConverter, UnitOfLength.INCHES), - (ElectricCurrentConverter, UnitOfElectricCurrent.AMPERE), - (ElectricCurrentConverter, UnitOfElectricCurrent.MILLIAMPERE), - (EnergyConverter, UnitOfEnergy.WATT_HOUR), - (EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR), - (EnergyConverter, UnitOfEnergy.MEGA_WATT_HOUR), - (EnergyConverter, UnitOfEnergy.GIGA_JOULE), - (InformationConverter, UnitOfInformation.GIGABYTES), - (MassConverter, UnitOfMass.GRAMS), - (MassConverter, UnitOfMass.KILOGRAMS), - (MassConverter, UnitOfMass.MICROGRAMS), - (MassConverter, UnitOfMass.MILLIGRAMS), - (MassConverter, UnitOfMass.OUNCES), - (MassConverter, UnitOfMass.POUNDS), - (PowerConverter, UnitOfPower.WATT), - (PowerConverter, UnitOfPower.KILO_WATT), - (PressureConverter, UnitOfPressure.PA), - (PressureConverter, UnitOfPressure.HPA), - (PressureConverter, UnitOfPressure.MBAR), - (PressureConverter, UnitOfPressure.INHG), - (PressureConverter, UnitOfPressure.KPA), - (PressureConverter, UnitOfPressure.CBAR), - (PressureConverter, UnitOfPressure.MMHG), - (PressureConverter, UnitOfPressure.PSI), - (SpeedConverter, UnitOfVolumetricFlux.INCHES_PER_DAY), - (SpeedConverter, UnitOfVolumetricFlux.INCHES_PER_HOUR), - (SpeedConverter, UnitOfVolumetricFlux.MILLIMETERS_PER_DAY), - (SpeedConverter, UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR), - (SpeedConverter, UnitOfSpeed.FEET_PER_SECOND), - (SpeedConverter, UnitOfSpeed.KILOMETERS_PER_HOUR), - (SpeedConverter, UnitOfSpeed.KNOTS), - (SpeedConverter, UnitOfSpeed.METERS_PER_SECOND), - (SpeedConverter, UnitOfSpeed.MILES_PER_HOUR), - (TemperatureConverter, UnitOfTemperature.CELSIUS), - (TemperatureConverter, UnitOfTemperature.FAHRENHEIT), - (TemperatureConverter, UnitOfTemperature.KELVIN), - (VolumeConverter, UnitOfVolume.LITERS), - (VolumeConverter, UnitOfVolume.MILLILITERS), - (VolumeConverter, UnitOfVolume.GALLONS), - (VolumeConverter, UnitOfVolume.FLUID_OUNCES), - ], -) -def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) -> None: - """Test conversion from any valid unit to same unit.""" - assert converter.convert(2, valid_unit, valid_unit) == 2 +# Dict containing all converters that need to be tested. +# The VALID_UNITS are sorted to ensure that pytest runs are consistent +# and avoid `different tests were collected between gw0 and gw1` +_ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = { + converter: sorted(converter.VALID_UNITS, key=lambda x: (x is None, x)) + for converter in ( + DataRateConverter, + DistanceConverter, + ElectricCurrentConverter, + ElectricPotentialConverter, + EnergyConverter, + InformationConverter, + MassConverter, + PowerConverter, + PressureConverter, + SpeedConverter, + TemperatureConverter, + UnitlessRatioConverter, + VolumeConverter, + ) +} +# Dict containing all converters with a corresponding unit ratio. +_GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, float]] = { + DataRateConverter: ( + UnitOfDataRate.BITS_PER_SECOND, + UnitOfDataRate.BYTES_PER_SECOND, + 8, + ), + DistanceConverter: (UnitOfLength.KILOMETERS, UnitOfLength.METERS, 0.001), + ElectricCurrentConverter: ( + UnitOfElectricCurrent.AMPERE, + UnitOfElectricCurrent.MILLIAMPERE, + 0.001, + ), + ElectricPotentialConverter: ( + UnitOfElectricPotential.MILLIVOLT, + UnitOfElectricPotential.VOLT, + 1000, + ), + EnergyConverter: (UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, 1000), + InformationConverter: (UnitOfInformation.BITS, UnitOfInformation.BYTES, 8), + MassConverter: (UnitOfMass.STONES, UnitOfMass.KILOGRAMS, 0.157473), + PowerConverter: (UnitOfPower.WATT, UnitOfPower.KILO_WATT, 1000), + PressureConverter: (UnitOfPressure.HPA, UnitOfPressure.INHG, 33.86389), + SpeedConverter: ( + UnitOfSpeed.KILOMETERS_PER_HOUR, + UnitOfSpeed.MILES_PER_HOUR, + 1.609343, + ), + TemperatureConverter: ( + UnitOfTemperature.CELSIUS, + UnitOfTemperature.FAHRENHEIT, + 0.555556, + ), + UnitlessRatioConverter: (PERCENTAGE, None, 100), + VolumeConverter: (UnitOfVolume.GALLONS, UnitOfVolume.LITERS, 0.264172), +} -@pytest.mark.parametrize( - "converter,valid_unit", - [ - (DataRateConverter, UnitOfDataRate.GIBIBYTES_PER_SECOND), - (DistanceConverter, UnitOfLength.KILOMETERS), - (ElectricCurrentConverter, UnitOfElectricCurrent.AMPERE), - (EnergyConverter, UnitOfEnergy.KILO_WATT_HOUR), - (InformationConverter, UnitOfInformation.GIBIBYTES), - (MassConverter, UnitOfMass.GRAMS), - (PowerConverter, UnitOfPower.WATT), - (PressureConverter, UnitOfPressure.PA), - (SpeedConverter, UnitOfSpeed.KILOMETERS_PER_HOUR), - (TemperatureConverter, UnitOfTemperature.CELSIUS), - (TemperatureConverter, UnitOfTemperature.FAHRENHEIT), - (TemperatureConverter, UnitOfTemperature.KELVIN), - (VolumeConverter, UnitOfVolume.LITERS), - ], -) -def test_convert_invalid_unit( - converter: type[BaseUnitConverter], valid_unit: str -) -> None: - """Test exception is thrown for invalid units.""" - with pytest.raises(HomeAssistantError, match="is not a recognized .* unit"): - converter.convert(5, INVALID_SYMBOL, valid_unit) - - with pytest.raises(HomeAssistantError, match="is not a recognized .* unit"): - converter.convert(5, valid_unit, INVALID_SYMBOL) - - -@pytest.mark.parametrize( - "converter,from_unit,to_unit", - [ - ( - DataRateConverter, - UnitOfDataRate.BYTES_PER_SECOND, - UnitOfDataRate.BITS_PER_SECOND, - ), - (DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS), - (EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR), - ( - InformationConverter, - UnitOfInformation.GIBIBYTES, - UnitOfInformation.GIGABYTES, - ), - (MassConverter, UnitOfMass.GRAMS, UnitOfMass.KILOGRAMS), - (PowerConverter, UnitOfPower.WATT, UnitOfPower.KILO_WATT), - (PressureConverter, UnitOfPressure.HPA, UnitOfPressure.INHG), - (SpeedConverter, UnitOfSpeed.KILOMETERS_PER_HOUR, UnitOfSpeed.MILES_PER_HOUR), - (TemperatureConverter, UnitOfTemperature.CELSIUS, UnitOfTemperature.FAHRENHEIT), - (VolumeConverter, UnitOfVolume.GALLONS, UnitOfVolume.LITERS), - ], -) -def test_convert_nonnumeric_value( - converter: type[BaseUnitConverter], from_unit: str, to_unit: str -) -> None: - """Test exception is thrown for nonnumeric type.""" - with pytest.raises(TypeError): - converter.convert("a", from_unit, to_unit) - - -@pytest.mark.parametrize( - "converter,from_unit,to_unit,expected", - [ - ( - DataRateConverter, - UnitOfDataRate.BITS_PER_SECOND, - UnitOfDataRate.BYTES_PER_SECOND, - 8, - ), - (DistanceConverter, UnitOfLength.KILOMETERS, UnitOfLength.METERS, 1 / 1000), - ( - ElectricCurrentConverter, - UnitOfElectricCurrent.AMPERE, - UnitOfElectricCurrent.MILLIAMPERE, - 1 / 1000, - ), - (EnergyConverter, UnitOfEnergy.WATT_HOUR, UnitOfEnergy.KILO_WATT_HOUR, 1000), - (InformationConverter, UnitOfInformation.BITS, UnitOfInformation.BYTES, 8), - (PowerConverter, UnitOfPower.WATT, UnitOfPower.KILO_WATT, 1000), - ( - PressureConverter, - UnitOfPressure.HPA, - UnitOfPressure.INHG, - pytest.approx(33.86389), - ), - ( - SpeedConverter, - UnitOfSpeed.KILOMETERS_PER_HOUR, - UnitOfSpeed.MILES_PER_HOUR, - pytest.approx(1.609343), - ), - ( - TemperatureConverter, - UnitOfTemperature.CELSIUS, - UnitOfTemperature.FAHRENHEIT, - 1 / 1.8, - ), - ( - VolumeConverter, - UnitOfVolume.GALLONS, - UnitOfVolume.LITERS, - pytest.approx(0.264172), - ), - ], -) -def test_get_unit_ratio( - converter: type[BaseUnitConverter], from_unit: str, to_unit: str, expected: float -) -> None: - """Test unit ratio.""" - assert converter.get_unit_ratio(from_unit, to_unit) == expected - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ +# Dict containing a conversion test for every know unit. +_CONVERTED_VALUE: dict[ + type[BaseUnitConverter], list[tuple[float, str | None, float, str | None]] +] = { + DataRateConverter: [ (8e3, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.KILOBITS_PER_SECOND), (8e6, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.MEGABITS_PER_SECOND), (8e9, UnitOfDataRate.BITS_PER_SECOND, 8, UnitOfDataRate.GIGABITS_PER_SECOND), @@ -233,155 +133,73 @@ def test_get_unit_ratio( UnitOfDataRate.GIBIBYTES_PER_SECOND, ), ], -) -def test_data_rate_convert( - value: float, - from_unit: str, - expected: float, - to_unit: str, -) -> None: - """Test conversion to other units.""" - assert DataRateConverter.convert(value, from_unit, to_unit) == pytest.approx( - expected - ) - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ + DistanceConverter: [ + (5, UnitOfLength.MILES, 8.04672, UnitOfLength.KILOMETERS), + (5, UnitOfLength.MILES, 8046.72, UnitOfLength.METERS), + (5, UnitOfLength.MILES, 804672.0, UnitOfLength.CENTIMETERS), + (5, UnitOfLength.MILES, 8046720.0, UnitOfLength.MILLIMETERS), + (5, UnitOfLength.MILES, 8800.0, UnitOfLength.YARDS), + (5, UnitOfLength.MILES, 26400.0008448, UnitOfLength.FEET), + (5, UnitOfLength.MILES, 316800.171072, UnitOfLength.INCHES), + (5, UnitOfLength.YARDS, 0.004572, UnitOfLength.KILOMETERS), + (5, UnitOfLength.YARDS, 4.572, UnitOfLength.METERS), + (5, UnitOfLength.YARDS, 457.2, UnitOfLength.CENTIMETERS), + (5, UnitOfLength.YARDS, 4572, UnitOfLength.MILLIMETERS), + (5, UnitOfLength.YARDS, 0.002840908212, UnitOfLength.MILES), + (5, UnitOfLength.YARDS, 15.00000048, UnitOfLength.FEET), + (5, UnitOfLength.YARDS, 180.0000972, UnitOfLength.INCHES), + (5000, UnitOfLength.FEET, 1.524, UnitOfLength.KILOMETERS), + (5000, UnitOfLength.FEET, 1524, UnitOfLength.METERS), + (5000, UnitOfLength.FEET, 152400.0, UnitOfLength.CENTIMETERS), + (5000, UnitOfLength.FEET, 1524000.0, UnitOfLength.MILLIMETERS), + (5000, UnitOfLength.FEET, 0.946969404, UnitOfLength.MILES), + (5000, UnitOfLength.FEET, 1666.66667, UnitOfLength.YARDS), + (5000, UnitOfLength.FEET, 60000.0324, UnitOfLength.INCHES), + (5000, UnitOfLength.INCHES, 0.127, UnitOfLength.KILOMETERS), + (5000, UnitOfLength.INCHES, 127.0, UnitOfLength.METERS), + (5000, UnitOfLength.INCHES, 12700.0, UnitOfLength.CENTIMETERS), + (5000, UnitOfLength.INCHES, 127000.0, UnitOfLength.MILLIMETERS), + (5000, UnitOfLength.INCHES, 0.078914117, UnitOfLength.MILES), + (5000, UnitOfLength.INCHES, 138.88889, UnitOfLength.YARDS), + (5000, UnitOfLength.INCHES, 416.66668, UnitOfLength.FEET), + (5, UnitOfLength.KILOMETERS, 5000, UnitOfLength.METERS), + (5, UnitOfLength.KILOMETERS, 500000, UnitOfLength.CENTIMETERS), + (5, UnitOfLength.KILOMETERS, 5000000, UnitOfLength.MILLIMETERS), + (5, UnitOfLength.KILOMETERS, 3.106855, UnitOfLength.MILES), + (5, UnitOfLength.KILOMETERS, 5468.066, UnitOfLength.YARDS), + (5, UnitOfLength.KILOMETERS, 16404.2, UnitOfLength.FEET), + (5, UnitOfLength.KILOMETERS, 196850.5, UnitOfLength.INCHES), + (5000, UnitOfLength.METERS, 5, UnitOfLength.KILOMETERS), + (5000, UnitOfLength.METERS, 500000, UnitOfLength.CENTIMETERS), + (5000, UnitOfLength.METERS, 5000000, UnitOfLength.MILLIMETERS), + (5000, UnitOfLength.METERS, 3.106855, UnitOfLength.MILES), + (5000, UnitOfLength.METERS, 5468.066, UnitOfLength.YARDS), + (5000, UnitOfLength.METERS, 16404.2, UnitOfLength.FEET), + (5000, UnitOfLength.METERS, 196850.5, UnitOfLength.INCHES), + (500000, UnitOfLength.CENTIMETERS, 5, UnitOfLength.KILOMETERS), + (500000, UnitOfLength.CENTIMETERS, 5000, UnitOfLength.METERS), + (500000, UnitOfLength.CENTIMETERS, 5000000, UnitOfLength.MILLIMETERS), + (500000, UnitOfLength.CENTIMETERS, 3.106855, UnitOfLength.MILES), + (500000, UnitOfLength.CENTIMETERS, 5468.066, UnitOfLength.YARDS), + (500000, UnitOfLength.CENTIMETERS, 16404.2, UnitOfLength.FEET), + (500000, UnitOfLength.CENTIMETERS, 196850.5, UnitOfLength.INCHES), + (5000000, UnitOfLength.MILLIMETERS, 5, UnitOfLength.KILOMETERS), + (5000000, UnitOfLength.MILLIMETERS, 5000, UnitOfLength.METERS), + (5000000, UnitOfLength.MILLIMETERS, 500000, UnitOfLength.CENTIMETERS), + (5000000, UnitOfLength.MILLIMETERS, 3.106855, UnitOfLength.MILES), + (5000000, UnitOfLength.MILLIMETERS, 5468.066, UnitOfLength.YARDS), + (5000000, UnitOfLength.MILLIMETERS, 16404.2, UnitOfLength.FEET), + (5000000, UnitOfLength.MILLIMETERS, 196850.5, UnitOfLength.INCHES), + ], + ElectricCurrentConverter: [ (5, UnitOfElectricCurrent.AMPERE, 5000, UnitOfElectricCurrent.MILLIAMPERE), (5, UnitOfElectricCurrent.MILLIAMPERE, 0.005, UnitOfElectricCurrent.AMPERE), ], -) -def test_electric_current_convert( - value: float, - from_unit: str, - expected: float, - to_unit: str, -) -> None: - """Test conversion to other units.""" - assert ElectricCurrentConverter.convert(value, from_unit, to_unit) == expected - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ - (5, UnitOfLength.MILES, pytest.approx(8.04672), UnitOfLength.KILOMETERS), - (5, UnitOfLength.MILES, pytest.approx(8046.72), UnitOfLength.METERS), - (5, UnitOfLength.MILES, pytest.approx(804672.0), UnitOfLength.CENTIMETERS), - (5, UnitOfLength.MILES, pytest.approx(8046720.0), UnitOfLength.MILLIMETERS), - (5, UnitOfLength.MILES, pytest.approx(8800.0), UnitOfLength.YARDS), - (5, UnitOfLength.MILES, pytest.approx(26400.0008448), UnitOfLength.FEET), - (5, UnitOfLength.MILES, pytest.approx(316800.171072), UnitOfLength.INCHES), - ( - 5, - UnitOfLength.YARDS, - pytest.approx(0.0045720000000000005), - UnitOfLength.KILOMETERS, - ), - (5, UnitOfLength.YARDS, pytest.approx(4.572), UnitOfLength.METERS), - (5, UnitOfLength.YARDS, pytest.approx(457.2), UnitOfLength.CENTIMETERS), - (5, UnitOfLength.YARDS, pytest.approx(4572), UnitOfLength.MILLIMETERS), - (5, UnitOfLength.YARDS, pytest.approx(0.002840908212), UnitOfLength.MILES), - (5, UnitOfLength.YARDS, pytest.approx(15.00000048), UnitOfLength.FEET), - (5, UnitOfLength.YARDS, pytest.approx(180.0000972), UnitOfLength.INCHES), - (5000, UnitOfLength.FEET, pytest.approx(1.524), UnitOfLength.KILOMETERS), - (5000, UnitOfLength.FEET, pytest.approx(1524), UnitOfLength.METERS), - (5000, UnitOfLength.FEET, pytest.approx(152400.0), UnitOfLength.CENTIMETERS), - (5000, UnitOfLength.FEET, pytest.approx(1524000.0), UnitOfLength.MILLIMETERS), - ( - 5000, - UnitOfLength.FEET, - pytest.approx(0.9469694040000001), - UnitOfLength.MILES, - ), - (5000, UnitOfLength.FEET, pytest.approx(1666.66667), UnitOfLength.YARDS), - ( - 5000, - UnitOfLength.FEET, - pytest.approx(60000.032400000004), - UnitOfLength.INCHES, - ), - (5000, UnitOfLength.INCHES, pytest.approx(0.127), UnitOfLength.KILOMETERS), - (5000, UnitOfLength.INCHES, pytest.approx(127.0), UnitOfLength.METERS), - (5000, UnitOfLength.INCHES, pytest.approx(12700.0), UnitOfLength.CENTIMETERS), - (5000, UnitOfLength.INCHES, pytest.approx(127000.0), UnitOfLength.MILLIMETERS), - (5000, UnitOfLength.INCHES, pytest.approx(0.078914117), UnitOfLength.MILES), - (5000, UnitOfLength.INCHES, pytest.approx(138.88889), UnitOfLength.YARDS), - (5000, UnitOfLength.INCHES, pytest.approx(416.66668), UnitOfLength.FEET), - (5, UnitOfLength.KILOMETERS, pytest.approx(5000), UnitOfLength.METERS), - (5, UnitOfLength.KILOMETERS, pytest.approx(500000), UnitOfLength.CENTIMETERS), - (5, UnitOfLength.KILOMETERS, pytest.approx(5000000), UnitOfLength.MILLIMETERS), - (5, UnitOfLength.KILOMETERS, pytest.approx(3.106855), UnitOfLength.MILES), - (5, UnitOfLength.KILOMETERS, pytest.approx(5468.066), UnitOfLength.YARDS), - (5, UnitOfLength.KILOMETERS, pytest.approx(16404.2), UnitOfLength.FEET), - (5, UnitOfLength.KILOMETERS, pytest.approx(196850.5), UnitOfLength.INCHES), - (5000, UnitOfLength.METERS, pytest.approx(5), UnitOfLength.KILOMETERS), - (5000, UnitOfLength.METERS, pytest.approx(500000), UnitOfLength.CENTIMETERS), - (5000, UnitOfLength.METERS, pytest.approx(5000000), UnitOfLength.MILLIMETERS), - (5000, UnitOfLength.METERS, pytest.approx(3.106855), UnitOfLength.MILES), - (5000, UnitOfLength.METERS, pytest.approx(5468.066), UnitOfLength.YARDS), - (5000, UnitOfLength.METERS, pytest.approx(16404.2), UnitOfLength.FEET), - (5000, UnitOfLength.METERS, pytest.approx(196850.5), UnitOfLength.INCHES), - (500000, UnitOfLength.CENTIMETERS, pytest.approx(5), UnitOfLength.KILOMETERS), - (500000, UnitOfLength.CENTIMETERS, pytest.approx(5000), UnitOfLength.METERS), - ( - 500000, - UnitOfLength.CENTIMETERS, - pytest.approx(5000000), - UnitOfLength.MILLIMETERS, - ), - (500000, UnitOfLength.CENTIMETERS, pytest.approx(3.106855), UnitOfLength.MILES), - (500000, UnitOfLength.CENTIMETERS, pytest.approx(5468.066), UnitOfLength.YARDS), - (500000, UnitOfLength.CENTIMETERS, pytest.approx(16404.2), UnitOfLength.FEET), - ( - 500000, - UnitOfLength.CENTIMETERS, - pytest.approx(196850.5), - UnitOfLength.INCHES, - ), - (5000000, UnitOfLength.MILLIMETERS, pytest.approx(5), UnitOfLength.KILOMETERS), - (5000000, UnitOfLength.MILLIMETERS, pytest.approx(5000), UnitOfLength.METERS), - ( - 5000000, - UnitOfLength.MILLIMETERS, - pytest.approx(500000), - UnitOfLength.CENTIMETERS, - ), - ( - 5000000, - UnitOfLength.MILLIMETERS, - pytest.approx(3.106855), - UnitOfLength.MILES, - ), - ( - 5000000, - UnitOfLength.MILLIMETERS, - pytest.approx(5468.066), - UnitOfLength.YARDS, - ), - (5000000, UnitOfLength.MILLIMETERS, pytest.approx(16404.2), UnitOfLength.FEET), - ( - 5000000, - UnitOfLength.MILLIMETERS, - pytest.approx(196850.5), - UnitOfLength.INCHES, - ), + ElectricPotentialConverter: [ + (5, UnitOfElectricPotential.VOLT, 5000, UnitOfElectricPotential.MILLIVOLT), + (5, UnitOfElectricPotential.MILLIVOLT, 0.005, UnitOfElectricPotential.VOLT), ], -) -def test_distance_convert( - value: float, - from_unit: str, - expected: float, - to_unit: str, -) -> None: - """Test conversion to other units.""" - assert DistanceConverter.convert(value, from_unit, to_unit) == expected - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ + EnergyConverter: [ (10, UnitOfEnergy.WATT_HOUR, 0.01, UnitOfEnergy.KILO_WATT_HOUR), (10, UnitOfEnergy.WATT_HOUR, 0.00001, UnitOfEnergy.MEGA_WATT_HOUR), (10, UnitOfEnergy.KILO_WATT_HOUR, 10000, UnitOfEnergy.WATT_HOUR), @@ -390,21 +208,10 @@ def test_distance_convert( (10, UnitOfEnergy.MEGA_WATT_HOUR, 10000, UnitOfEnergy.KILO_WATT_HOUR), (10, UnitOfEnergy.GIGA_JOULE, 10000 / 3.6, UnitOfEnergy.KILO_WATT_HOUR), (10, UnitOfEnergy.GIGA_JOULE, 10 / 3.6, UnitOfEnergy.MEGA_WATT_HOUR), + (10, UnitOfEnergy.MEGA_JOULE, 10 / 3.6, UnitOfEnergy.KILO_WATT_HOUR), + (10, UnitOfEnergy.MEGA_JOULE, 0.010 / 3.6, UnitOfEnergy.MEGA_WATT_HOUR), ], -) -def test_energy_convert( - value: float, - from_unit: str, - expected: float, - to_unit: str, -) -> None: - """Test conversion to other units.""" - assert EnergyConverter.convert(value, from_unit, to_unit) == expected - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ + InformationConverter: [ (8e3, UnitOfInformation.BITS, 8, UnitOfInformation.KILOBITS), (8e6, UnitOfInformation.BITS, 8, UnitOfInformation.MEGABITS), (8e9, UnitOfInformation.BITS, 8, UnitOfInformation.GIGABITS), @@ -426,47 +233,27 @@ def test_energy_convert( (8 * 2**70, UnitOfInformation.BITS, 1, UnitOfInformation.ZEBIBYTES), (8 * 2**80, UnitOfInformation.BITS, 1, UnitOfInformation.YOBIBYTES), ], -) -def test_information_convert( - value: float, - from_unit: str, - expected: float, - to_unit: str, -) -> None: - """Test conversion to other units.""" - assert InformationConverter.convert(value, from_unit, to_unit) == pytest.approx( - expected - ) - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ + MassConverter: [ (10, UnitOfMass.KILOGRAMS, 10000, UnitOfMass.GRAMS), (10, UnitOfMass.KILOGRAMS, 10000000, UnitOfMass.MILLIGRAMS), (10, UnitOfMass.KILOGRAMS, 10000000000, UnitOfMass.MICROGRAMS), - (10, UnitOfMass.KILOGRAMS, pytest.approx(352.73961), UnitOfMass.OUNCES), - (10, UnitOfMass.KILOGRAMS, pytest.approx(22.046226), UnitOfMass.POUNDS), + (10, UnitOfMass.KILOGRAMS, 352.73961, UnitOfMass.OUNCES), + (10, UnitOfMass.KILOGRAMS, 22.046226, UnitOfMass.POUNDS), (10, UnitOfMass.GRAMS, 0.01, UnitOfMass.KILOGRAMS), (10, UnitOfMass.GRAMS, 10000, UnitOfMass.MILLIGRAMS), (10, UnitOfMass.GRAMS, 10000000, UnitOfMass.MICROGRAMS), - (10, UnitOfMass.GRAMS, pytest.approx(0.35273961), UnitOfMass.OUNCES), - (10, UnitOfMass.GRAMS, pytest.approx(0.022046226), UnitOfMass.POUNDS), + (10, UnitOfMass.GRAMS, 0.35273961, UnitOfMass.OUNCES), + (10, UnitOfMass.GRAMS, 0.022046226, UnitOfMass.POUNDS), (10, UnitOfMass.MILLIGRAMS, 0.00001, UnitOfMass.KILOGRAMS), (10, UnitOfMass.MILLIGRAMS, 0.01, UnitOfMass.GRAMS), (10, UnitOfMass.MILLIGRAMS, 10000, UnitOfMass.MICROGRAMS), - (10, UnitOfMass.MILLIGRAMS, pytest.approx(0.00035273961), UnitOfMass.OUNCES), - (10, UnitOfMass.MILLIGRAMS, pytest.approx(0.000022046226), UnitOfMass.POUNDS), + (10, UnitOfMass.MILLIGRAMS, 0.00035273961, UnitOfMass.OUNCES), + (10, UnitOfMass.MILLIGRAMS, 0.000022046226, UnitOfMass.POUNDS), (10000, UnitOfMass.MICROGRAMS, 0.00001, UnitOfMass.KILOGRAMS), (10000, UnitOfMass.MICROGRAMS, 0.01, UnitOfMass.GRAMS), (10000, UnitOfMass.MICROGRAMS, 10, UnitOfMass.MILLIGRAMS), - (10000, UnitOfMass.MICROGRAMS, pytest.approx(0.00035273961), UnitOfMass.OUNCES), - ( - 10000, - UnitOfMass.MICROGRAMS, - pytest.approx(0.000022046226), - UnitOfMass.POUNDS, - ), + (10000, UnitOfMass.MICROGRAMS, 0.00035273961, UnitOfMass.OUNCES), + (10000, UnitOfMass.MICROGRAMS, 0.000022046226, UnitOfMass.POUNDS), (1, UnitOfMass.POUNDS, 0.45359237, UnitOfMass.KILOGRAMS), (1, UnitOfMass.POUNDS, 453.59237, UnitOfMass.GRAMS), (1, UnitOfMass.POUNDS, 453592.37, UnitOfMass.MILLIGRAMS), @@ -477,91 +264,48 @@ def test_information_convert( (16, UnitOfMass.OUNCES, 453592.37, UnitOfMass.MILLIGRAMS), (16, UnitOfMass.OUNCES, 453592370, UnitOfMass.MICROGRAMS), (16, UnitOfMass.OUNCES, 1, UnitOfMass.POUNDS), - (1, UnitOfMass.STONES, pytest.approx(6.350293), UnitOfMass.KILOGRAMS), - (1, UnitOfMass.STONES, pytest.approx(6350.293), UnitOfMass.GRAMS), - (1, UnitOfMass.STONES, pytest.approx(6350293), UnitOfMass.MILLIGRAMS), - (1, UnitOfMass.STONES, pytest.approx(14), UnitOfMass.POUNDS), - (1, UnitOfMass.STONES, pytest.approx(224), UnitOfMass.OUNCES), + (1, UnitOfMass.STONES, 6.350293, UnitOfMass.KILOGRAMS), + (1, UnitOfMass.STONES, 6350.293, UnitOfMass.GRAMS), + (1, UnitOfMass.STONES, 6350293, UnitOfMass.MILLIGRAMS), + (1, UnitOfMass.STONES, 14, UnitOfMass.POUNDS), + (1, UnitOfMass.STONES, 224, UnitOfMass.OUNCES), ], -) -def test_mass_convert( - value: float, - from_unit: str, - expected: float, - to_unit: str, -) -> None: - """Test conversion to other units.""" - assert MassConverter.convert(value, from_unit, to_unit) == expected - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ + PowerConverter: [ (10, UnitOfPower.KILO_WATT, 10000, UnitOfPower.WATT), (10, UnitOfPower.WATT, 0.01, UnitOfPower.KILO_WATT), ], -) -def test_power_convert( - value: float, - from_unit: str, - expected: float, - to_unit: str, -) -> None: - """Test conversion to other units.""" - assert PowerConverter.convert(value, from_unit, to_unit) == expected - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ - (1000, UnitOfPressure.HPA, pytest.approx(14.5037743897), UnitOfPressure.PSI), - (1000, UnitOfPressure.HPA, pytest.approx(29.5299801647), UnitOfPressure.INHG), - (1000, UnitOfPressure.HPA, pytest.approx(100000), UnitOfPressure.PA), - (1000, UnitOfPressure.HPA, pytest.approx(100), UnitOfPressure.KPA), - (1000, UnitOfPressure.HPA, pytest.approx(1000), UnitOfPressure.MBAR), - (1000, UnitOfPressure.HPA, pytest.approx(100), UnitOfPressure.CBAR), - (100, UnitOfPressure.KPA, pytest.approx(14.5037743897), UnitOfPressure.PSI), - (100, UnitOfPressure.KPA, pytest.approx(29.5299801647), UnitOfPressure.INHG), - (100, UnitOfPressure.KPA, pytest.approx(100000), UnitOfPressure.PA), - (100, UnitOfPressure.KPA, pytest.approx(1000), UnitOfPressure.HPA), - (100, UnitOfPressure.KPA, pytest.approx(1000), UnitOfPressure.MBAR), - (100, UnitOfPressure.KPA, pytest.approx(100), UnitOfPressure.CBAR), - (30, UnitOfPressure.INHG, pytest.approx(14.7346266155), UnitOfPressure.PSI), - (30, UnitOfPressure.INHG, pytest.approx(101.59167), UnitOfPressure.KPA), - (30, UnitOfPressure.INHG, pytest.approx(1015.9167), UnitOfPressure.HPA), - (30, UnitOfPressure.INHG, pytest.approx(101591.67), UnitOfPressure.PA), - (30, UnitOfPressure.INHG, pytest.approx(1015.9167), UnitOfPressure.MBAR), - (30, UnitOfPressure.INHG, pytest.approx(101.59167), UnitOfPressure.CBAR), - (30, UnitOfPressure.INHG, pytest.approx(762), UnitOfPressure.MMHG), - (30, UnitOfPressure.MMHG, pytest.approx(0.580103), UnitOfPressure.PSI), - (30, UnitOfPressure.MMHG, pytest.approx(3.99967), UnitOfPressure.KPA), - (30, UnitOfPressure.MMHG, pytest.approx(39.9967), UnitOfPressure.HPA), - (30, UnitOfPressure.MMHG, pytest.approx(3999.67), UnitOfPressure.PA), - (30, UnitOfPressure.MMHG, pytest.approx(39.9967), UnitOfPressure.MBAR), - (30, UnitOfPressure.MMHG, pytest.approx(3.99967), UnitOfPressure.CBAR), - (30, UnitOfPressure.MMHG, pytest.approx(1.181102), UnitOfPressure.INHG), + PressureConverter: [ + (1000, UnitOfPressure.HPA, 14.5037743897, UnitOfPressure.PSI), + (1000, UnitOfPressure.HPA, 29.5299801647, UnitOfPressure.INHG), + (1000, UnitOfPressure.HPA, 100000, UnitOfPressure.PA), + (1000, UnitOfPressure.HPA, 100, UnitOfPressure.KPA), + (1000, UnitOfPressure.HPA, 1000, UnitOfPressure.MBAR), + (1000, UnitOfPressure.HPA, 100, UnitOfPressure.CBAR), + (100, UnitOfPressure.KPA, 14.5037743897, UnitOfPressure.PSI), + (100, UnitOfPressure.KPA, 29.5299801647, UnitOfPressure.INHG), + (100, UnitOfPressure.KPA, 100000, UnitOfPressure.PA), + (100, UnitOfPressure.KPA, 1000, UnitOfPressure.HPA), + (100, UnitOfPressure.KPA, 1000, UnitOfPressure.MBAR), + (100, UnitOfPressure.KPA, 100, UnitOfPressure.CBAR), + (30, UnitOfPressure.INHG, 14.7346266155, UnitOfPressure.PSI), + (30, UnitOfPressure.INHG, 101.59167, UnitOfPressure.KPA), + (30, UnitOfPressure.INHG, 1015.9167, UnitOfPressure.HPA), + (30, UnitOfPressure.INHG, 101591.67, UnitOfPressure.PA), + (30, UnitOfPressure.INHG, 1015.9167, UnitOfPressure.MBAR), + (30, UnitOfPressure.INHG, 101.59167, UnitOfPressure.CBAR), + (30, UnitOfPressure.INHG, 762, UnitOfPressure.MMHG), + (30, UnitOfPressure.MMHG, 0.580103, UnitOfPressure.PSI), + (30, UnitOfPressure.MMHG, 3.99967, UnitOfPressure.KPA), + (30, UnitOfPressure.MMHG, 39.9967, UnitOfPressure.HPA), + (30, UnitOfPressure.MMHG, 3999.67, UnitOfPressure.PA), + (30, UnitOfPressure.MMHG, 39.9967, UnitOfPressure.MBAR), + (30, UnitOfPressure.MMHG, 3.99967, UnitOfPressure.CBAR), + (30, UnitOfPressure.MMHG, 1.181102, UnitOfPressure.INHG), + (5, UnitOfPressure.BAR, 72.51887, UnitOfPressure.PSI), ], -) -def test_pressure_convert( - value: float, - from_unit: str, - expected: float, - to_unit: str, -) -> None: - """Test conversion to other units.""" - assert PressureConverter.convert(value, from_unit, to_unit) == expected - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ + SpeedConverter: [ # 5 km/h / 1.609 km/mi = 3.10686 mi/h - ( - 5, - UnitOfSpeed.KILOMETERS_PER_HOUR, - pytest.approx(3.106856), - UnitOfSpeed.MILES_PER_HOUR, - ), + (5, UnitOfSpeed.KILOMETERS_PER_HOUR, 3.106856, UnitOfSpeed.MILES_PER_HOUR), # 5 mi/h * 1.609 km/mi = 8.04672 km/h (5, UnitOfSpeed.MILES_PER_HOUR, 8.04672, UnitOfSpeed.KILOMETERS_PER_HOUR), # 5 in/day * 25.4 mm/in = 127 mm/day @@ -575,14 +319,14 @@ def test_pressure_convert( ( 5, UnitOfVolumetricFlux.MILLIMETERS_PER_DAY, - pytest.approx(0.1968504), + 0.1968504, UnitOfVolumetricFlux.INCHES_PER_DAY, ), # 48 mm/day = 2 mm/h ( 48, UnitOfVolumetricFlux.MILLIMETERS_PER_DAY, - pytest.approx(2), + 2, UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR, ), # 5 in/hr * 24 hr/day = 3048 mm/day @@ -596,106 +340,34 @@ def test_pressure_convert( ( 5, UnitOfSpeed.METERS_PER_SECOND, - pytest.approx(708661.42), + 708661.42, UnitOfVolumetricFlux.INCHES_PER_HOUR, ), # 5000 in/h / 39.3701 in/m / 3600 s/h = 0.03528 m/s ( 5000, UnitOfVolumetricFlux.INCHES_PER_HOUR, - pytest.approx(0.0352778), + 0.0352778, UnitOfSpeed.METERS_PER_SECOND, ), # 5 kt * 1852 m/nmi / 3600 s/h = 2.5722 m/s - (5, UnitOfSpeed.KNOTS, pytest.approx(2.57222), UnitOfSpeed.METERS_PER_SECOND), + (5, UnitOfSpeed.KNOTS, 2.57222, UnitOfSpeed.METERS_PER_SECOND), # 5 ft/s * 0.3048 m/ft = 1.524 m/s - ( - 5, - UnitOfSpeed.FEET_PER_SECOND, - pytest.approx(1.524), - UnitOfSpeed.METERS_PER_SECOND, - ), + (5, UnitOfSpeed.FEET_PER_SECOND, 1.524, UnitOfSpeed.METERS_PER_SECOND), ], -) -def test_speed_convert( - value: float, - from_unit: str, - expected: float, - to_unit: str, -) -> None: - """Test conversion to other units.""" - assert SpeedConverter.convert(value, from_unit, to_unit) == expected - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ + TemperatureConverter: [ (100, UnitOfTemperature.CELSIUS, 212, UnitOfTemperature.FAHRENHEIT), (100, UnitOfTemperature.CELSIUS, 373.15, UnitOfTemperature.KELVIN), - ( - 100, - UnitOfTemperature.FAHRENHEIT, - pytest.approx(37.77777777777778), - UnitOfTemperature.CELSIUS, - ), - ( - 100, - UnitOfTemperature.FAHRENHEIT, - pytest.approx(310.92777777777775), - UnitOfTemperature.KELVIN, - ), - ( - 100, - UnitOfTemperature.KELVIN, - pytest.approx(-173.15), - UnitOfTemperature.CELSIUS, - ), - ( - 100, - UnitOfTemperature.KELVIN, - pytest.approx(-279.66999999999996), - UnitOfTemperature.FAHRENHEIT, - ), + (100, UnitOfTemperature.FAHRENHEIT, 37.7778, UnitOfTemperature.CELSIUS), + (100, UnitOfTemperature.FAHRENHEIT, 310.9277, UnitOfTemperature.KELVIN), + (100, UnitOfTemperature.KELVIN, -173.15, UnitOfTemperature.CELSIUS), + (100, UnitOfTemperature.KELVIN, -279.6699, UnitOfTemperature.FAHRENHEIT), ], -) -def test_temperature_convert( - value: float, from_unit: str, expected: float, to_unit: str -) -> None: - """Test conversion to other units.""" - assert TemperatureConverter.convert(value, from_unit, to_unit) == expected - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ - (100, UnitOfTemperature.CELSIUS, 180, UnitOfTemperature.FAHRENHEIT), - (100, UnitOfTemperature.CELSIUS, 100, UnitOfTemperature.KELVIN), - ( - 100, - UnitOfTemperature.FAHRENHEIT, - pytest.approx(55.55555555555556), - UnitOfTemperature.CELSIUS, - ), - ( - 100, - UnitOfTemperature.FAHRENHEIT, - pytest.approx(55.55555555555556), - UnitOfTemperature.KELVIN, - ), - (100, UnitOfTemperature.KELVIN, 100, UnitOfTemperature.CELSIUS), - (100, UnitOfTemperature.KELVIN, 180, UnitOfTemperature.FAHRENHEIT), + UnitlessRatioConverter: [ + (5, None, 500, PERCENTAGE), + (5, PERCENTAGE, 0.05, None), ], -) -def test_temperature_convert_with_interval( - value: float, from_unit: str, expected: float, to_unit: str -) -> None: - """Test conversion to other units.""" - assert TemperatureConverter.convert_interval(value, from_unit, to_unit) == expected - - -@pytest.mark.parametrize( - "value,from_unit,expected,to_unit", - [ + VolumeConverter: [ (5, UnitOfVolume.LITERS, 1.32086, UnitOfVolume.GALLONS), (5, UnitOfVolume.GALLONS, 18.92706, UnitOfVolume.LITERS), (5, UnitOfVolume.CUBIC_METERS, 176.5733335, UnitOfVolume.CUBIC_FEET), @@ -736,12 +408,142 @@ def test_temperature_convert_with_interval( (5, UnitOfVolume.CENTUM_CUBIC_FEET, 3740.26, UnitOfVolume.GALLONS), (5, UnitOfVolume.CENTUM_CUBIC_FEET, 14158.42, UnitOfVolume.LITERS), ], +} + + +@pytest.mark.parametrize( + "converter", + [ + # Generate list of all converters available in + # `homeassistant.util.unit_conversion` to ensure + # that we don't miss any in the tests. + obj + for _, obj in inspect.getmembers(unit_conversion) + if inspect.isclass(obj) + and issubclass(obj, BaseUnitConverter) + and obj != BaseUnitConverter + ], ) -def test_volume_convert( +def test_all_converters(converter: type[BaseUnitConverter]) -> None: + """Ensure all unit converters are tested.""" + assert converter in _ALL_CONVERTERS, "converter is not present in _ALL_CONVERTERS" + + assert converter in _GET_UNIT_RATIO, "converter is not present in _GET_UNIT_RATIO" + unit_ratio_item = _GET_UNIT_RATIO[converter] + assert unit_ratio_item[0] != unit_ratio_item[1], "ratio units should be different" + + assert converter in _CONVERTED_VALUE, "converter is not present in _CONVERTED_VALUE" + converted_value_items = _CONVERTED_VALUE[converter] + for valid_unit in converter.VALID_UNITS: + assert any( + item + for item in converted_value_items + # item[1] is from_unit, item[3] is to_unit + if valid_unit in {item[1], item[3]} + ), f"Unit `{valid_unit}` is not tested in _CONVERTED_VALUE" + + +@pytest.mark.parametrize( + "converter,valid_unit", + [ + # Ensure all units are tested + (converter, valid_unit) + for converter, valid_units in _ALL_CONVERTERS.items() + for valid_unit in valid_units + ], +) +def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str) -> None: + """Test conversion from any valid unit to same unit.""" + assert converter.convert(2, valid_unit, valid_unit) == 2 + + +@pytest.mark.parametrize( + "converter,valid_unit", + [ + # Ensure all units are tested + (converter, valid_unit) + for converter, valid_units in _ALL_CONVERTERS.items() + for valid_unit in valid_units + ], +) +def test_convert_invalid_unit( + converter: type[BaseUnitConverter], valid_unit: str +) -> None: + """Test exception is thrown for invalid units.""" + with pytest.raises(HomeAssistantError, match="is not a recognized .* unit"): + converter.convert(5, INVALID_SYMBOL, valid_unit) + + with pytest.raises(HomeAssistantError, match="is not a recognized .* unit"): + converter.convert(5, valid_unit, INVALID_SYMBOL) + + +@pytest.mark.parametrize( + "converter,from_unit,to_unit", + [ + # Pick any two units + (converter, valid_units[0], valid_units[1]) + for converter, valid_units in _ALL_CONVERTERS.items() + ], +) +def test_convert_nonnumeric_value( + converter: type[BaseUnitConverter], from_unit: str, to_unit: str +) -> None: + """Test exception is thrown for nonnumeric type.""" + with pytest.raises(TypeError): + converter.convert("a", from_unit, to_unit) + + +@pytest.mark.parametrize( + "converter,from_unit,to_unit,expected", + [ + # Process all items in _GET_UNIT_RATIO + (converter, item[0], item[1], item[2]) + for converter, item in _GET_UNIT_RATIO.items() + ], +) +def test_get_unit_ratio( + converter: type[BaseUnitConverter], from_unit: str, to_unit: str, expected: float +) -> None: + """Test unit ratio.""" + ratio = converter.get_unit_ratio(from_unit, to_unit) + assert ratio == pytest.approx(expected) + assert converter.get_unit_ratio(to_unit, from_unit) == pytest.approx(1 / ratio) + + +@pytest.mark.parametrize( + "converter,value,from_unit,expected,to_unit", + [ + # Process all items in _CONVERTED_VALUE + (converter, list_item[0], list_item[1], list_item[2], list_item[3]) + for converter, item in _CONVERTED_VALUE.items() + for list_item in item + ], +) +def test_unit_conversion( + converter: type[BaseUnitConverter], value: float, from_unit: str, expected: float, to_unit: str, ) -> None: """Test conversion to other units.""" - assert VolumeConverter.convert(value, from_unit, to_unit) == pytest.approx(expected) + assert converter.convert(value, from_unit, to_unit) == pytest.approx(expected) + + +@pytest.mark.parametrize( + "value,from_unit,expected,to_unit", + [ + (100, UnitOfTemperature.CELSIUS, 180, UnitOfTemperature.FAHRENHEIT), + (100, UnitOfTemperature.CELSIUS, 100, UnitOfTemperature.KELVIN), + (100, UnitOfTemperature.FAHRENHEIT, 55.5556, UnitOfTemperature.CELSIUS), + (100, UnitOfTemperature.FAHRENHEIT, 55.5556, UnitOfTemperature.KELVIN), + (100, UnitOfTemperature.KELVIN, 100, UnitOfTemperature.CELSIUS), + (100, UnitOfTemperature.KELVIN, 180, UnitOfTemperature.FAHRENHEIT), + ], +) +def test_temperature_convert_with_interval( + value: float, from_unit: str, expected: float, to_unit: str +) -> None: + """Test conversion to other units.""" + expected = pytest.approx(expected) + assert TemperatureConverter.convert_interval(value, from_unit, to_unit) == expected diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 8d1b7c1adf1..f544723c881 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -324,7 +324,7 @@ def load_yaml(fname, string, secrets=None): class TestSecrets(unittest.TestCase): """Test the secrets parameter in the yaml utility.""" - # pylint: disable=protected-access,invalid-name + # pylint: disable=invalid-name def setUp(self): """Create & load secrets file."""